diff --git a/index.d.ts b/index.d.ts index 52b7d2f9b3..1196e2b6c4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1024,6 +1024,14 @@ declare namespace dashjs { value: string; } + export interface ThroughputDictValue { + downloadTimeInMs: number, + downloadedBytes: number, + latencyInMs: number + serviceLocation: string, + value: number, + } + /** * Dash **/ @@ -1630,7 +1638,6 @@ declare namespace dashjs { applyServiceDescription?: boolean, applyProducerReferenceTime?: boolean, applyContentSteering?: boolean, - eventControllerRefreshDelay?: number, enableManifestDurationMismatchFix?: boolean, parseInbandPrft?: boolean, enableManifestTimescaleMismatchFix?: boolean, @@ -1643,6 +1650,10 @@ declare namespace dashjs { filterHDRMetadataFormatEssentialProperties?: boolean, filterVideoColometryEssentialProperties?: boolean }, + events?: { + eventControllerRefreshDelay?: number, + deleteEventMessageDataAfterEventStarted?: boolean + } timeShiftBuffer?: { calcFromSegmentTimeline?: boolean fallbackToSegmentTimeline?: boolean @@ -2049,6 +2060,8 @@ declare namespace dashjs { getAvailableLocations(): MpdLocation[]; + getAverageLatency(type: MediaType, calculationMode?: string | null, sampleSize?: number): number; + getAverageThroughput(type: MediaType, calculationMode?: string | null, sampleSize?: number): number; getBufferLength(type: MediaType): number; @@ -2085,8 +2098,12 @@ declare namespace dashjs { getProtectionController(): ProtectionController; + getRawThroughputData(type: MediaType): ThroughputDictValue[]; + getRepresentationsByType(type: MediaType, streamId?: string | null): Representation[]; + getSafeAverageThroughput(type: MediaType, calculationMode?: string | null, sampleSize?: number): number; + getSettings(): MediaPlayerSettingClass; getSource(): string | object; @@ -5499,11 +5516,21 @@ declare namespace dashjs { } export class IsoBoxSearchInfo { - constructor(lastCompletedOffset: number, found: boolean, size: number); + constructor(found: boolean, + sizeOfLastCompletedBox: number, + sizeOfLastFoundTargetBox: number, + startOffsetOfLastCompletedBox: number, + startOffsetOfLastFoundTargetBox: number, + typeOfLastCompletedBox: string, + typeOfLastTargetBox: string); found: boolean; - lastCompletedOffset: number; - size: number; + sizeOfLastCompletedBox: number; + sizeOfLastFoundTargetBox: number; + startOffsetOfLastCompletedBox: number; + startOffsetOfLastFoundTargetBox: number; + typeOfLastCompletedBox: string | null; + typeOfLastTargetBox: string | null; } export class MetricsList { diff --git a/samples/advanced/ext-url-query-info.html b/samples/advanced/ext-url-query-info.html index 3397656506..128d76f71c 100644 --- a/samples/advanced/ext-url-query-info.html +++ b/samples/advanced/ext-url-query-info.html @@ -153,7 +153,7 @@

Flexible Insertion of URL Parameters Sample

- + diff --git a/samples/dash-if-reference-player/app/sources.json b/samples/dash-if-reference-player/app/sources.json index 72ba3638fa..b7729ac63d 100644 --- a/samples/dash-if-reference-player/app/sources.json +++ b/samples/dash-if-reference-player/app/sources.json @@ -280,6 +280,11 @@ "name": "SCTE35 events", "provider": "dashif" }, + { + "url": "https://livesim.dashif.org/livesim2/annexI_dashjs=rocks/testpic_6s/Manifest.mpd?dashjs=rocks", + "name": "Annex I query parameters in the Video AdaptationSet", + "provider": "dashif" + }, { "url": "https://demo.unified-streaming.com/k8s/live/scte35.isml/.mpd", "name": "Unified Streaming reference stream with scte35 markers", diff --git a/src/core/Settings.js b/src/core/Settings.js index 1c75759be2..d1976785ec 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -67,7 +67,6 @@ import Events from './events/Events.js'; * applyServiceDescription: true, * applyProducerReferenceTime: true, * applyContentSteering: true, - * eventControllerRefreshDelay: 100, * enableManifestDurationMismatchFix: true, * parseInbandPrft: false, * enableManifestTimescaleMismatchFix: false, @@ -86,6 +85,10 @@ import Events from './events/Events.js'; * filterVideoColorimetryEssentialProperties: false, * filterHDRMetadataFormatEssentialProperties: false * }, + * events: { + * eventControllerRefreshDelay: 100, + * deleteEventMessageDataAfterEventStarted: true + * } * timeShiftBuffer: { * calcFromSegmentTimeline: false, * fallbackToSegmentTimeline: true @@ -264,7 +267,7 @@ import Events from './events/Events.js'; * useResourceTimingApi: true, * useNetworkInformationApi: { * xhr: false, - * fetch: true + * fetch: false * }, * useDeadTimeLatency: true, * bandwidthSafetyFactor: 0.9, @@ -281,7 +284,8 @@ import Events from './events/Events.js'; * throughputSlowHalfLifeSeconds: 8, * throughputFastHalfLifeSeconds: 3, * latencySlowHalfLifeCount: 2, - * latencyFastHalfLifeCount: 1 + * latencyFastHalfLifeCount: 1, + * weightDownloadTimeMultiplicationFactor: 0.0015 * } * }, * maxBitrate: { @@ -342,6 +346,16 @@ import Events from './events/Events.js'; * In case the MPD uses \. */ +/** + * @typedef {Object} EventSettings + * @property {number} [eventControllerRefreshDelay=100] + * Interval timer used by the EventController to check if events need to be triggered or removed. + * @property {boolean} [deleteEventMessageDataAfterEventStarted=true] + * If this flag is enabled the EventController will delete the message data of events after they have been started. This is to save memory in case events have a long duration and need to be persisted in the EventController. + * Note: Applications will receive a copy of the original event data when they subscribe to an event. This copy contains the original message data and is not affected by this setting. + * Only if an event is dispatched for the second time (e.g. when the user seeks back) the message data will not be included in the dispatched event. + */ + /** * @typedef {Object} LiveDelay * @property {number} [liveDelayFragmentCount=NaN] @@ -827,7 +841,7 @@ import Events from './events/Events.js'; * @property {boolean} [useResourceTimingApi=true] * If set to true the ResourceTimingApi is used to derive the download time and the number of downloaded bytes. * This option has no effect for low latency streaming as the download time equals the segment duration in most of the cases and therefor does not provide reliable values - * @property {object} [useNetworkInformationApi = { xhr=false, fetch=true}] + * @property {object} [useNetworkInformationApi = { xhr=false, fetch=false}] * If set to true the NetworkInformationApi is used to derive the current throughput. Browser support is limited, only available in Chrome and Edge. * Applies to standard (XHR requests) and/or low latency streaming (Fetch API requests). * @property {boolean} [useDeadTimeLatency=true] @@ -847,12 +861,13 @@ import Events from './events/Events.js'; * - `increaseScale`: Increase sample size by one if the ratio of current and previous sample is higher or equal this value * - `maxMeasurementsToKeep`: Number of samples to keep before sliding samples out of the window * - `averageLatencySampleAmount`: Number of latency samples to use (sample size) - * @property {object} [ewma={throughputSlowHalfLifeSeconds=8,throughputFastHalfLifeSeconds=3,latencySlowHalfLifeCount=2,latencyFastHalfLifeCount=1}] + * @property {object} [ewma={throughputSlowHalfLifeSeconds=8,throughputFastHalfLifeSeconds=3,latencySlowHalfLifeCount=2,latencyFastHalfLifeCount=1, weightDownloadTimeMultiplicationFactor=0.0015}] * When deriving the throughput based on the exponential weighted moving average these settings define: * - `throughputSlowHalfLifeSeconds`: Number by which the weight of the current throughput measurement is divided, see ThroughputModel._updateEwmaValues * - `throughputFastHalfLifeSeconds`: Number by which the weight of the current throughput measurement is divided, see ThroughputModel._updateEwmaValues * - `latencySlowHalfLifeCount`: Number by which the weight of the current latency is divided, see ThroughputModel._updateEwmaValues * - `latencyFastHalfLifeCount`: Number by which the weight of the current latency is divided, see ThroughputModel._updateEwmaValues + * - `weightDownloadTimeMultiplicationFactor`: This value is multiplied with the download time in milliseconds to derive the weight for the EWMA calculation. */ /** @@ -937,8 +952,6 @@ import Events from './events/Events.js'; * Set to true if dash.js should use the parameters defined in ProducerReferenceTime elements in combination with ServiceDescription elements. * @property {boolean} [applyContentSteering=true] * Set to true if dash.js should apply content steering during playback. - * @property {number} [eventControllerRefreshDelay=100] - * For multi-period streams, overwrite the manifest mediaPresentationDuration attribute with the sum of period durations if the manifest mediaPresentationDuration is greater than the sum of period durations * @property {boolean} [enableManifestDurationMismatchFix=true] * Overwrite the manifest segments base information timescale attributes with the timescale set in initialization segments * @property {boolean} [enableManifestTimescaleMismatchFix=false] @@ -947,6 +960,7 @@ import Events from './events/Events.js'; * Set to true if dash.js should parse inband prft boxes (ProducerReferenceTime) and trigger events. * @property {module:Settings~Metrics} metrics Metric settings * @property {module:Settings~LiveDelay} delay Live Delay settings + * @property {module:Settings~EventSettings} events Event settings * @property {module:Settings~TimeShiftBuffer} timeShiftBuffer TimeShiftBuffer settings * @property {module:Settings~Protection} protection DRM related settings * @property {module:Settings~Capabilities} capabilities Capability related settings @@ -1078,7 +1092,6 @@ function Settings() { applyServiceDescription: true, applyProducerReferenceTime: true, applyContentSteering: true, - eventControllerRefreshDelay: 100, enableManifestDurationMismatchFix: true, parseInbandPrft: false, enableManifestTimescaleMismatchFix: false, @@ -1099,6 +1112,10 @@ function Settings() { filterVideoColorimetryEssentialProperties: false, filterHDRMetadataFormatEssentialProperties: false }, + events: { + eventControllerRefreshDelay: 100, + deleteEventMessageDataAfterEventStarted: true + }, timeShiftBuffer: { calcFromSegmentTimeline: false, fallbackToSegmentTimeline: true @@ -1290,7 +1307,7 @@ function Settings() { useResourceTimingApi: true, useNetworkInformationApi: { xhr: false, - fetch: true + fetch: false }, useDeadTimeLatency: true, bandwidthSafetyFactor: 0.9, diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js index 98a474ab74..587463bc9c 100644 --- a/src/streaming/MediaPlayer.js +++ b/src/streaming/MediaPlayer.js @@ -1232,6 +1232,20 @@ function MediaPlayer() { customParametersModel.restoreDefaultUTCTimingSources(); } + /** + * Returns the average latency computed in the ThroughputController in milliseconds + * + * @param {MediaType} type + * @param {string} calculationMode + * @param {number} sampleSize + * @return {number} value + * @memberof module:MediaPlayer + * @instance + */ + function getAverageLatency(type, calculationMode = null, sampleSize = NaN) { + return throughputController ? throughputController.getAverageLatency(type, calculationMode, sampleSize) : 0; + } + /** * Returns the average throughput computed in the ThroughputController in kbit/s * @@ -1246,6 +1260,32 @@ function MediaPlayer() { return throughputController ? throughputController.getAverageThroughput(type, calculationMode, sampleSize) : 0; } + /** + * Returns the safe average throughput computed in the ThroughputController in kbit/s. The safe average throughput is the average throughput multiplied by bandwidthSafetyFactor + * + * @param {MediaType} type + * @param {string} calculationMode + * @param {number} sampleSize + * @return {number} value + * @memberof module:MediaPlayer + * @instance + */ + function getSafeAverageThroughput(type, calculationMode = null, sampleSize = NaN) { + return throughputController ? throughputController.getSafeAverageThroughput(type, calculationMode, sampleSize) : 0; + } + + /** + * Returns the raw throughput data without calculating the average. This can be used to calculate the current throughput yourself. + * + * @param {MediaType} type + * @return {Array} value + * @memberof module:MediaPlayer + * @instance + */ + function getRawThroughputData(type) { + return throughputController ? throughputController.getRawThroughputData(type) : []; + } + /** * Sets whether withCredentials on XHR requests for a particular request * type is true or false @@ -2759,6 +2799,7 @@ function MediaPlayer() { getAutoPlay, getAvailableBaseUrls, getAvailableLocations, + getAverageLatency, getAverageThroughput, getBufferLength, getCurrentLiveLatency, @@ -2777,7 +2818,9 @@ function MediaPlayer() { getOfflineController, getPlaybackRate, getProtectionController, + getRawThroughputData, getRepresentationsByType, + getSafeAverageThroughput, getSettings, getSource, getStreamsFromManifest, diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js index dad44879c5..3724dbcade 100644 --- a/src/streaming/Stream.js +++ b/src/streaming/Stream.js @@ -552,6 +552,10 @@ function Stream(config) { if (textController) { textController.deactivateStream(streamInfo); } + if (thumbnailController) { + thumbnailController.reset(); + thumbnailController = null; + } streamProcessors = []; isActive = false; hasFinishedBuffering = false; @@ -624,6 +628,10 @@ function Stream(config) { segmentBlacklistController = null; } + if (textController && streamInfo) { + textController.clearDataForStream(streamInfo.id); + } + resetInitialSettings(keepBuffers); streamInfo = null; diff --git a/src/streaming/controllers/BlacklistController.js b/src/streaming/controllers/BlacklistController.js index a5aa6f0683..e4f7df2d4e 100644 --- a/src/streaming/controllers/BlacklistController.js +++ b/src/streaming/controllers/BlacklistController.js @@ -78,6 +78,9 @@ function BlackListController(config) { } function reset() { + if (addBlacklistEventName) { + eventBus.off(addBlacklistEventName, onAddBlackList, instance); + } blacklist = []; } diff --git a/src/streaming/controllers/EventController.js b/src/streaming/controllers/EventController.js index 837bcabf2e..01864a9121 100644 --- a/src/streaming/controllers/EventController.js +++ b/src/streaming/controllers/EventController.js @@ -118,7 +118,7 @@ function EventController() { try { checkConfig(); logger.debug('Start Event Controller'); - const refreshDelay = settings.get().streaming.eventControllerRefreshDelay; + const refreshDelay = settings.get().streaming.events.eventControllerRefreshDelay; if (!isStarted && !isNaN(refreshDelay)) { isStarted = true; eventInterval = setInterval(_onEventTimer, refreshDelay); @@ -474,7 +474,7 @@ function EventController() { if (mode === MediaPlayerEvents.EVENT_MODE_ON_RECEIVE && !event.triggeredReceivedEvent) { logger.debug(`Received event ${eventId}`); event.triggeredReceivedEvent = true; - eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode }); + eventBus.trigger(event.eventStream.schemeIdUri, { event: JSON.parse(JSON.stringify(event)) }, { mode }); return; } @@ -490,7 +490,11 @@ function EventController() { _sendCallbackRequest(event.messageData); } else { logger.debug(`Starting event ${eventId} from period ${event.eventStream.period.id} at ${currentVideoTime}`); - eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode }); + eventBus.trigger(event.eventStream.schemeIdUri, { event: JSON.parse(JSON.stringify(event)) }, { mode }); + if (settings.get().streaming.events.deleteEventMessageDataAfterEventStarted) { + delete event.messageData; + delete event.parsedMessageData; + } } event.triggeredStartEvent = true; } diff --git a/src/streaming/net/FetchLoader.js b/src/streaming/net/FetchLoader.js index 5190ec30b4..9f68414036 100644 --- a/src/streaming/net/FetchLoader.js +++ b/src/streaming/net/FetchLoader.js @@ -32,7 +32,7 @@ import FactoryMaker from '../../core/FactoryMaker.js'; import Settings from '../../core/Settings.js'; import Constants from '../constants/Constants.js'; -import AastLowLatencyThroughputModel from '../models/AastLowLatencyThroughputModel.js'; +import Debug from '../../core/Debug.js'; /** * @module FetchLoader @@ -42,312 +42,308 @@ import AastLowLatencyThroughputModel from '../models/AastLowLatencyThroughputMod function FetchLoader() { const context = this.context; - const aastLowLatencyThroughputModel = AastLowLatencyThroughputModel(context).getInstance(); const settings = Settings(context).getInstance(); - let instance, dashMetrics, boxParser; + let instance, boxParser, logger; function setConfig(cfg) { - dashMetrics = cfg.dashMetrics; boxParser = cfg.boxParser } + function setup() { + logger = Debug(context).getInstance().getLogger(instance); + } + /** * Load request - * @param {CommonMediaRequest} httpRequest - * @param {CommonMediaResponse} httpResponse + * With HTTP responses that use chunked transfer encoding, the promise returned by fetch will resolve as soon as the response's headers are received. + * @param {CommonMediaRequest} commonMediaRequest + * @param {CommonMediaResponse} commonMediaResponse */ - function load(httpRequest, httpResponse) { - // Variables will be used in the callback functions - const fragmentRequest = httpRequest.customData.request; + function load(commonMediaRequest, commonMediaResponse) { + const headers = _getHeaders(commonMediaRequest); + const abortController = _setupAbortMechanism(commonMediaRequest); + const fetchResourceRequestObject = _getFetchResourceRequestObject(commonMediaRequest, headers, abortController); + + fetch(fetchResourceRequestObject) + .then((fetchResponse) => { + _handleFetchResponse(fetchResponse, commonMediaRequest, commonMediaResponse); + }) + .catch(() => { + _handleFetchError(commonMediaRequest); + }) + } - const headers = new Headers(); - if (httpRequest.headers) { - for (let header in httpRequest.headers) { - let value = httpRequest.headers[header]; - if (value) { - headers.append(header, value); - } - } - } + function _handleFetchResponse(fetchResponse, commonMediaRequest, commonMediaResponse) { + _updateCommonMediaResponseInstance(commonMediaResponse, fetchResponse); - let abortController; - if (typeof window.AbortController === 'function') { - abortController = new AbortController(); /*jshint ignore:line*/ - httpRequest.customData.abortController = abortController; - abortController.signal.onabort = httpRequest.customData.onabort; + if (!fetchResponse.ok) { + commonMediaRequest.customData.onloadend(); } - httpRequest.customData.abort = abort.bind(httpRequest); + let totalBytesReceived = 0; + let signaledFirstByte = false; + let receivedData = new Uint8Array(); + let endPositionOfLastProcessedBoxInReceivedData = 0; - const reqOptions = { - method: httpRequest.method, - headers: headers, - credentials: httpRequest.credentials, - signal: abortController ? abortController.signal : undefined - }; + commonMediaRequest.customData.reader = fetchResponse.body.getReader(); + let downloadedData = []; + let moofStartTimeData = []; + let mdatEndTimeData = []; + let lastChunkWasFinished = true; const calculationMode = settings.get().streaming.abr.throughput.lowLatencyDownloadTimeCalculationMode; - const requestTime = performance.now(); - let throughputCapacityDelayMS = 0; - - new Promise((resolve) => { - if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.AAST && aastLowLatencyThroughputModel) { - throughputCapacityDelayMS = aastLowLatencyThroughputModel.getThroughputCapacityDelayMS(fragmentRequest, dashMetrics.getCurrentBufferLevel(fragmentRequest.mediaType) * 1000); - if (throughputCapacityDelayMS) { - // safely delay the "fetch" call a bit to be able to measure the throughput capacity of the line. - // this will lead to first few chunks downloaded at max network speed - return setTimeout(resolve, throughputCapacityDelayMS); - } + + /** + * Callback function for ReadableStreamDefaultReader + * @param value - chunk data. Always undefined when done is true. + * @param done - true if the stream has already given you all its data. + */ + const _processResult = ({ value, done }) => { + + if (done) { + _handleRequestComplete() + return; } - resolve(); - }) - .then(() => { - let markBeforeFetch = performance.now(); - - fetch(httpRequest.url, reqOptions) - .then((response) => { - httpResponse.status = response.status; - httpResponse.statusText = response.statusText; - httpResponse.url = response.url; - - if (!response.ok) { - httpRequest.customData.onloadend(); - } - const responseHeaders = {}; - for (const key of response.headers.keys()) { - responseHeaders[key] = response.headers.get(key); - } - httpResponse.headers = responseHeaders; + if (value && value.length > 0) { + _handleReceivedChunkData(value) + } - const totalBytes = parseInt(response.headers.get('Content-Length'), 10); - let bytesReceived = 0; - let signaledFirstByte = false; - let receivedData = new Uint8Array(); - let offset = 0; + _readResponseBody(commonMediaRequest, commonMediaResponse, _processResult); + }; - if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.AAST && aastLowLatencyThroughputModel) { - _aastProcessResponse(markBeforeFetch, httpRequest, requestTime, throughputCapacityDelayMS, responseHeaders, response); - } else { - httpRequest.customData.reader = response.body.getReader(); - } + /** + * Once a request is completed throw final progress event with the calculated bytes and download time + * @private + */ + function _handleRequestComplete() { + if (receivedData) { + const calculatedDownloadTime = _calculateDownloadTime(); + + // In this case we push an entry to the traces. + // This is the only entry we push as the other calls to onprogress use noTrace = true + commonMediaRequest.customData.onprogress({ + loaded: totalBytesReceived, + total: totalBytesReceived, + lengthComputable: true, + time: calculatedDownloadTime + }); - let downloadedData = []; - let moofStartTimeData = []; - let mdatEndTimeData = []; - let lastChunkWasFinished = true; - - /** - * Callback function for the reader. - * @param value - some data. Always undefined when done is true. - * @param done - true if the stream has already given you all its data. - */ - const _processResult = ({ value, done }) => { // Bug fix Parse whenever data is coming [value] better than 1ms looking that increase CPU - - if (done) { - _handleRequestComplete() - return; - } - - if (value && value.length > 0) { - _handleDataReceived(value) - } - - _read(httpRequest, httpResponse, _processResult); - }; - - /** - * Once a request is completed throw final progress event with the calculated bytes and download time - * @private - */ - function _handleRequestComplete() { - if (receivedData) { - if (calculationMode !== Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.AAST) { - // If there is pending data, call progress so network metrics - // are correctly generated - // Same structure as https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/ - let calculatedThroughput = null; - let calculatedTime = null; - if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.MOOF_PARSING) { - calculatedThroughput = _calculateThroughputByChunkData(moofStartTimeData, mdatEndTimeData); - if (calculatedThroughput) { - calculatedTime = bytesReceived * 8 / calculatedThroughput; - } - } else if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.DOWNLOADED_DATA) { - calculatedTime = calculateDownloadedTime(downloadedData, bytesReceived); - } - - httpRequest.customData.onprogress({ - loaded: bytesReceived, - total: isNaN(totalBytes) ? bytesReceived : totalBytes, - lengthComputable: true, - time: calculatedTime - }); - } - - httpResponse.data = receivedData.buffer; - } - httpRequest.customData.onloadend(); - } + commonMediaResponse.data = receivedData.buffer; + } + commonMediaRequest.customData.onloadend(); + } - /** - * Called every time we received data - * @param value - * @private - */ - function _handleDataReceived(value) { - receivedData = _concatTypedArray(receivedData, value); - bytesReceived += value.length; - - downloadedData.push({ - ts: performance.now(), - bytes: value.length - }); - - if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.MOOF_PARSING && lastChunkWasFinished) { - // Parse the payload and capture the 'moof' box - const boxesInfo = boxParser.findLastTopIsoBoxCompleted(['moof'], receivedData, offset); - if (boxesInfo.found) { - // Store the beginning time of each chunk download in array StartTimeData - lastChunkWasFinished = false; - moofStartTimeData.push({ - ts: performance.now(), - bytes: value.length - }); - } - } - - const boxesInfo = boxParser.findLastTopIsoBoxCompleted(['moov', 'mdat'], receivedData, offset); - if (boxesInfo.found) { - const endOfLastBox = boxesInfo.lastCompletedOffset + boxesInfo.size; - - // Store the end time of each chunk download with its size in array EndTimeData - if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.MOOF_PARSING && !lastChunkWasFinished) { - lastChunkWasFinished = true; - mdatEndTimeData.push({ - ts: performance.now(), - bytes: receivedData.length - }); - } - - // Make the data that we received available for playback - // If we are going to pass full buffer, avoid copying it and pass - // complete buffer. Otherwise, clone the part of the buffer that is completed - // and adjust remaining buffer. A clone is needed because ArrayBuffer of a typed-array - // keeps a reference to the original data - let data; - if (endOfLastBox === receivedData.length) { - data = receivedData; - receivedData = new Uint8Array(); - } else { - data = new Uint8Array(receivedData.subarray(0, endOfLastBox)); - receivedData = receivedData.subarray(endOfLastBox); - } - - // Announce progress but don't track traces. Throughput measures are quite unstable - // when they are based in small amount of data - httpRequest.customData.onprogress({ - data: data.buffer, - lengthComputable: false, - noTrace: true - }); - - offset = 0; - } else { - offset = boxesInfo.lastCompletedOffset; - // Call progress, so it generates traces that will be later used to know when the first byte - // were received - if (!signaledFirstByte) { - httpRequest.customData.onprogress({ - lengthComputable: false, - noTrace: true - }); - signaledFirstByte = true; - } - } - } + function _calculateDownloadTime() { + // If there is pending data, call progress so network metrics + // are correctly generated + // Same structure as https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/ + let downloadTime = null; + if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.MOOF_PARSING) { + downloadTime = _getDownloadTimeForMoofParsing(); + } else if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.DOWNLOADED_DATA) { + downloadTime = _getDownloadTimeForDownloadedData() + } - _read(httpRequest, httpResponse, _processResult); - }) - .catch(function () { - if (httpRequest.customData.onloadend) { - httpRequest.customData.onloadend(); - } - }); + return downloadTime; + } + + function _getDownloadTimeForMoofParsing() { + const calculatedThroughput = _calculateThroughputByMoofMdatTimes(moofStartTimeData, mdatEndTimeData); + + if (calculatedThroughput) { + return totalBytesReceived * 8 / calculatedThroughput; + } + + return null; + } + + function _getDownloadTimeForDownloadedData() { + return calculateDownloadedTime(downloadedData, totalBytesReceived); + } + + /** + * Called every time we received data if the request is not completed + * @param value + * @private + */ + function _handleReceivedChunkData(value) { + receivedData = _concatTypedArray(receivedData, value); + totalBytesReceived += value.length; + + downloadedData.push({ + timestamp: _getCurrentTimestamp(), + bytes: value.length }); - } + if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.MOOF_PARSING && lastChunkWasFinished) { + _findMoofBoxInChunkData(); + } - function _aastProcessResponse(markBeforeFetch, httpRequest, requestTime, throughputCapacityDelayMS, responseHeaders, response) { - let markA = markBeforeFetch; - let markB = 0; - - function fetchMeassurement(stream) { - const reader = stream.getReader(); - const measurement = []; - const fragmentRequest = httpRequest.customData.request; - - reader.read() - .then(function processFetch(args) { - const value = args.value; - const done = args.done; - markB = performance.now(); - - if (value && value.length) { - const chunkDownloadDurationMS = markB - markA; - const chunkBytes = value.length; - measurement.push({ - chunkDownloadTimeRelativeMS: markB - markBeforeFetch, - chunkDownloadDurationMS, - chunkBytes, - kbps: Math.round(8 * chunkBytes / (chunkDownloadDurationMS / 1000)), - bufferLevel: dashMetrics.getCurrentBufferLevel(fragmentRequest.mediaType) - }); - } + const boxesInfo = boxParser.findLastTopIsoBoxCompleted(['moov', 'mdat'], receivedData, endPositionOfLastProcessedBoxInReceivedData); + if (boxesInfo.found) { + _handleTopIsoBoxCompleted(boxesInfo); + } else { + _handleNoCompletedTopIsoBox(boxesInfo); + } + } - if (done) { + function _findMoofBoxInChunkData() { + const boxesInfo = boxParser.findLastTopIsoBoxCompleted(['moof'], receivedData, endPositionOfLastProcessedBoxInReceivedData); - const fetchDuration = markB - markBeforeFetch; - const bytesAllChunks = measurement.reduce((prev, curr) => prev + curr.chunkBytes, 0); + if (boxesInfo.found) { + lastChunkWasFinished = false; + moofStartTimeData.push({ + timestamp: _getCurrentTimestamp() + }); + } + } - aastLowLatencyThroughputModel.addMeasurement(fragmentRequest, fetchDuration, measurement, requestTime, throughputCapacityDelayMS, responseHeaders); + function _handleTopIsoBoxCompleted(boxesInfo) { + const endPositionOfLastTargetBox = boxesInfo.startOffsetOfLastFoundTargetBox + boxesInfo.sizeOfLastFoundTargetBox; + const data = _getDataForMediaSourceBufferAndAdjustReceivedData(endPositionOfLastTargetBox); - httpRequest.progress({ - loaded: bytesAllChunks, - total: bytesAllChunks, - lengthComputable: true, - time: aastLowLatencyThroughputModel.getEstimatedDownloadDurationMS(fragmentRequest) - }); - return; - } - markA = performance.now(); - return reader.read().then(processFetch); + // Store the end time of each chunk download with its size in array EndTimeData + if (calculationMode === Constants.LOW_LATENCY_DOWNLOAD_TIME_CALCULATION_MODE.MOOF_PARSING && !lastChunkWasFinished) { + lastChunkWasFinished = true; + mdatEndTimeData.push({ + timestamp: _getCurrentTimestamp(), + bytes: data.length }); + } + + // Announce progress but don't track traces. Throughput measures are quite unstable + // when they are based in small amount of data + commonMediaRequest.customData.onprogress({ + data: data.buffer, + lengthComputable: false, + noTrace: true + }); + + endPositionOfLastProcessedBoxInReceivedData = 0; } - // tee'ing streams is supported by all current major browsers - // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/tee - const [forMeasure, forConsumer] = response.body.tee(); - fetchMeassurement(forMeasure); - httpRequest.customData.reader = forConsumer.getReader(); + /** + * Make the data that we received available for playback + * If we are going to pass full buffer, avoid copying it and pass + * complete buffer. Otherwise, clone the part of the buffer that is completed + * and adjust remaining buffer. A clone is needed because ArrayBuffer of a typed-array + * keeps a reference to the original data + * @param endPositionOfLastTargetBox + * @returns {Uint8Array} + * @private + */ + function _getDataForMediaSourceBufferAndAdjustReceivedData(endPositionOfLastTargetBox) { + let data; + + if (endPositionOfLastTargetBox === receivedData.length) { + data = receivedData; + receivedData = new Uint8Array(); + } else { + data = new Uint8Array(receivedData.subarray(0, endPositionOfLastTargetBox)); + receivedData = receivedData.subarray(endPositionOfLastTargetBox); + } + + return data + } + + function _handleNoCompletedTopIsoBox(boxesInfo) { + endPositionOfLastProcessedBoxInReceivedData = boxesInfo.startOffsetOfLastCompletedBox + boxesInfo.sizeOfLastCompletedBox; + // Call progress, so it generates traces that will be later used to know when the first byte + // were received + if (!signaledFirstByte) { + commonMediaRequest.customData.onprogress({ + lengthComputable: false, + noTrace: true + }); + signaledFirstByte = true; + } + } + + _readResponseBody(commonMediaRequest, commonMediaResponse, _processResult); } /** * Reads the response of the request. For details refer to https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read - * @param httpRequest + * @param {CommonMediaRequest} commonMediaRequest + * @param {CommonMediaResponse} commonMediaResponse * @param processResult * @private */ - function _read(httpRequest, httpResponse, processResult) { - httpRequest.customData.reader.read() + function _readResponseBody(commonMediaRequest, commonMediaResponse, processResult) { + commonMediaRequest.customData.reader.read() .then(processResult) .catch(function () { - if (httpRequest.customData.onloadend) { - httpRequest.customData.onloadend(); - } + _handleFetchError(commonMediaRequest); }); } + function _handleFetchError(commonMediaRequest) { + if (commonMediaRequest.customData.onloadend) { + commonMediaRequest.customData.onloadend(); + } + } + + function _updateCommonMediaResponseInstance(commonMediaResponse, fetchResponse) { + commonMediaResponse.status = fetchResponse.status; + commonMediaResponse.statusText = fetchResponse.statusText; + commonMediaResponse.url = fetchResponse.url; + + const responseHeaders = {}; + for (const key of fetchResponse.headers.keys()) { + responseHeaders[key] = fetchResponse.headers.get(key); + } + commonMediaResponse.headers = responseHeaders; + } + + function _getHeaders(commonMediaRequest) { + const headers = new Headers(); + + if (commonMediaRequest.headers) { + for (let header in commonMediaRequest.headers) { + let value = commonMediaRequest.headers[header]; + if (value) { + headers.append(header, value); + } + } + } + + return headers + } + + function _setupAbortMechanism(commonMediaRequest) { + let abortController; + + if (typeof window.AbortController === 'function') { + abortController = new AbortController(); + commonMediaRequest.customData.abortController = abortController; + abortController.signal.onabort = commonMediaRequest.customData.onabort; + } + + commonMediaRequest.customData.abort = abort.bind(commonMediaRequest); + + return abortController + } + + function _getFetchResourceRequestObject(commonMediaRequest, headers, abortController) { + const fetchResourceRequestObject = new Request(commonMediaRequest.url, { + method: commonMediaRequest.method, + headers: headers, + credentials: commonMediaRequest.credentials, + signal: abortController ? abortController.signal : undefined + }); + + return fetchResourceRequestObject + } + + function _getCurrentTimestamp() { + if (typeof performance !== 'undefined' && typeof performance.now === 'function') { + return performance.now(); + } else { + return Date.now(); + } + } + /** * Creates a new Uint8 array and adds the existing data as well as new data * @param receivedData @@ -405,12 +401,12 @@ function FetchLoader() { downloadedData = downloadedData.filter(data => data.bytes > ((bytesReceived / 4) / downloadedData.length)); if (downloadedData.length > 1) { let time = 0; - const avgTimeDistance = (downloadedData[downloadedData.length - 1].ts - downloadedData[0].ts) / downloadedData.length; + const avgTimeDistance = (downloadedData[downloadedData.length - 1].timestamp - downloadedData[0].timestamp) / downloadedData.length; downloadedData.forEach((data, index) => { // To be counted the data has to be over a threshold const next = downloadedData[index + 1]; if (next) { - const distance = next.ts - data.ts; + const distance = next.timestamp - data.timestamp; time += distance < avgTimeDistance ? distance : 0; } }); @@ -422,52 +418,56 @@ function FetchLoader() { } } - /** - * Moof based throughput calculation - * @param startTimeData - * @param endTimeData - * @returns {number|null} - * @private - */ - function _calculateThroughputByChunkData(startTimeData, endTimeData) { + function _calculateThroughputByMoofMdatTimes(moofStartTimeData, mdatEndTimeData) { try { - let datum, datumE; + let filteredMoofStartTimeData, + filteredMdatEndTimeData; + // Filter the last chunks in a segment in both arrays [StartTimeData and EndTimeData] - datum = startTimeData.filter((data, i) => i < startTimeData.length - 1); - datumE = endTimeData.filter((dataE, i) => i < endTimeData.length - 1); - let chunkThroughputs = []; - // Compute the average throughput of the filtered chunk data - if (datum.length > 1) { - let shortDurationBytesReceived = 0; - let shortDurationStartTime = 0; - for (let i = 0; i < datum.length; i++) { - if (datum[i] && datumE[i]) { - let chunkDownloadTime = datumE[i].ts - datum[i].ts; - if (chunkDownloadTime > 1) { - chunkThroughputs.push((8 * datumE[i].bytes) / chunkDownloadTime); + filteredMoofStartTimeData = moofStartTimeData.slice(0, -1); + filteredMdatEndTimeData = mdatEndTimeData.slice(0, -1); + + if (filteredMoofStartTimeData.length !== filteredMdatEndTimeData.length) { + logger.warn(`[FetchLoader] Moof and Mdat data arrays have different lengths. Moof: ${filteredMoofStartTimeData.length}, Mdat: ${filteredMdatEndTimeData.length}`); + } + + if (filteredMoofStartTimeData.length <= 1) { + return null; + } + + let chunkThroughputValues = []; + let shortDurationBytesReceived = 0; + let shortDurationStartTime = 0; + + for (let i = 0; i < filteredMoofStartTimeData.length; i++) { + if (filteredMoofStartTimeData[i] && filteredMdatEndTimeData[i]) { + let chunkDownloadTime = filteredMdatEndTimeData[i].timestamp - filteredMoofStartTimeData[i].timestamp; + if (chunkDownloadTime > 1) { + const throughput = _getThroughputInBitPerMs(filteredMdatEndTimeData[i].bytes, chunkDownloadTime); + chunkThroughputValues.push(throughput); + shortDurationStartTime = 0; + } else { + if (shortDurationStartTime === 0) { + shortDurationStartTime = filteredMoofStartTimeData[i].timestamp; + shortDurationBytesReceived = 0; + } + let cumulatedChunkDownloadTime = filteredMdatEndTimeData[i].timestamp - shortDurationStartTime; + if (cumulatedChunkDownloadTime > 1) { + shortDurationBytesReceived += filteredMdatEndTimeData[i].bytes; + const throughput = _getThroughputInBitPerMs(shortDurationBytesReceived, cumulatedChunkDownloadTime); + chunkThroughputValues.push(throughput); shortDurationStartTime = 0; } else { - if (shortDurationStartTime === 0) { - shortDurationStartTime = datum[i].ts; - shortDurationBytesReceived = 0; - } - let cumulatedChunkDownloadTime = datumE[i].ts - shortDurationStartTime; - if (cumulatedChunkDownloadTime > 1) { - shortDurationBytesReceived += datumE[i].bytes; - chunkThroughputs.push((8 * shortDurationBytesReceived) / cumulatedChunkDownloadTime); - shortDurationStartTime = 0; - } else { - // continue cumulating short duration data - shortDurationBytesReceived += datumE[i].bytes; - } + // continue cumulating short duration data + shortDurationBytesReceived += filteredMdatEndTimeData[i].bytes; } } } + } - if (chunkThroughputs.length > 0) { - const sumOfChunkThroughputs = chunkThroughputs.reduce((a, b) => a + b, 0); - return sumOfChunkThroughputs / chunkThroughputs.length; - } + if (chunkThroughputValues.length > 0) { + const sumOfChunkThroughputValues = chunkThroughputValues.reduce((a, b) => a + b, 0); + return sumOfChunkThroughputValues / chunkThroughputValues.length; } return null; @@ -476,6 +476,12 @@ function FetchLoader() { } } + function _getThroughputInBitPerMs(bytes, timeInMs) { + return (8 * bytes) / timeInMs + } + + setup(); + instance = { abort, calculateDownloadedTime, diff --git a/src/streaming/net/HTTPLoader.js b/src/streaming/net/HTTPLoader.js index e69a0823d8..2df9e9833f 100644 --- a/src/streaming/net/HTTPLoader.js +++ b/src/streaming/net/HTTPLoader.js @@ -45,6 +45,8 @@ import CustomParametersModel from '../models/CustomParametersModel.js'; import CommonAccessTokenController from '../controllers/CommonAccessTokenController.js'; import ClientDataReportingController from '../controllers/ClientDataReportingController.js'; import ExtUrlQueryInfoController from '../controllers/ExtUrlQueryInfoController.js'; +import CommonMediaRequest from '../vo/CommonMediaRequest.js'; +import CommonMediaResponse from '../vo/CommonMediaResponse.js'; /** * @module HTTPLoader @@ -167,16 +169,16 @@ function HTTPLoader(cfg) { if (!event.lengthComputable || (event.lengthComputable && event.total !== event.loaded)) { requestObject.firstByteDate = currentTime; - httpResponse.resourceTiming.responseStart = currentTime.getTime(); + commonMediaResponse.resourceTiming.responseStart = currentTime.getTime(); } } // lengthComputable indicating if the resource concerned by the ProgressEvent has a length that can be calculated. If not, the ProgressEvent.total property has no significant value. if (event.lengthComputable) { - requestObject.bytesLoaded = httpResponse.length = event.loaded; - requestObject.bytesTotal = httpResponse.resourceTiming.encodedBodySize = event.total; - httpResponse.length = event.total; - httpResponse.resourceTiming.encodedBodySize = event.loaded; + requestObject.bytesLoaded = commonMediaResponse.length = event.loaded; + requestObject.bytesTotal = commonMediaResponse.resourceTiming.encodedBodySize = event.total; + commonMediaResponse.length = event.total; + commonMediaResponse.resourceTiming.encodedBodySize = event.loaded; } if (!event.noTrace) { @@ -200,8 +202,8 @@ function HTTPLoader(cfg) { if (settings.get().streaming.fragmentRequestProgressTimeout > 0) { progressTimeout = setTimeout(function () { // No more progress => abort request and treat as an error - logger.warn('Abort request ' + httpRequest.url + ' due to progress timeout'); - loader.abort(httpRequest); + logger.warn('Abort request ' + commonMediaRequest.url + ' due to progress timeout'); + loader.abort(commonMediaRequest); _onloadend(); }, settings.get().streaming.fragmentRequestProgressTimeout); } @@ -236,8 +238,8 @@ function HTTPLoader(cfg) { const _onRequestEnd = function (aborted = false) { // Remove the request from our list of requests - if (httpRequests.indexOf(httpRequest) !== -1) { - httpRequests.splice(httpRequests.indexOf(httpRequest), 1); + if (httpRequests.indexOf(commonMediaRequest) !== -1) { + httpRequests.splice(httpRequests.indexOf(commonMediaRequest), 1); } if (progressTimeout) { @@ -245,15 +247,15 @@ function HTTPLoader(cfg) { progressTimeout = null; } - commonAccessTokenController.processResponseHeaders(httpResponse); + commonAccessTokenController.processResponseHeaders(commonMediaResponse); _updateRequestTimingInfo(); _updateResourceTimingInfo(); - _applyResponseInterceptors(httpResponse).then((_httpResponse) => { - httpResponse = _httpResponse; + _applyResponseInterceptors(commonMediaResponse).then((_httpResponse) => { + commonMediaResponse = _httpResponse; - _addHttpRequestMetric(httpRequest, httpResponse, traces); + _addHttpRequestMetric(commonMediaRequest, commonMediaResponse, traces); // Ignore aborted requests if (aborted) { @@ -268,18 +270,18 @@ function HTTPLoader(cfg) { eventBus.trigger(Events.MANIFEST_LOADING_FINISHED, { requestObject }); } - if (httpResponse.status >= 200 && httpResponse.status <= 299 && httpResponse.data) { + if (commonMediaResponse.status >= 200 && commonMediaResponse.status <= 299 && commonMediaResponse.data) { if (config.success) { - config.success(httpResponse.data, httpResponse.statusText, httpResponse.url); + config.success(commonMediaResponse.data, commonMediaResponse.statusText, commonMediaResponse.url); } if (config.complete) { - config.complete(requestObject, httpResponse.statusText); + config.complete(requestObject, commonMediaResponse.statusText); } } else { // If we get a 404 to a media segment we should check the client clock again and perform a UTC sync in the background. try { - if (httpResponse.status === 404 && settings.get().streaming.utcSynchronization.enableBackgroundSyncAfterSegmentDownloadError && requestObject.type === HTTPRequest.MEDIA_SEGMENT_TYPE) { + if (commonMediaResponse.status === 404 && settings.get().streaming.utcSynchronization.enableBackgroundSyncAfterSegmentDownloadError && requestObject.type === HTTPRequest.MEDIA_SEGMENT_TYPE) { // Only trigger a sync if the loading failed for the first time const initialNumberOfAttempts = mediaPlayerModel.getRetryAttemptsForType(HTTPRequest.MEDIA_SEGMENT_TYPE); if (initialNumberOfAttempts === remainingAttempts) { @@ -292,7 +294,6 @@ function HTTPLoader(cfg) { _retriggerRequest(); } }); - }; const _updateRequestTimingInfo = function () { @@ -302,11 +303,11 @@ function HTTPLoader(cfg) { } const _updateResourceTimingInfo = function () { - httpResponse.resourceTiming.responseEnd = Date.now(); + commonMediaResponse.resourceTiming.responseEnd = Date.now(); // If enabled the ResourceTimingApi we add the corresponding information to the request object. // These values are more accurate and can be used by the ThroughputController later - _addResourceTimingValues(httpRequest, httpResponse); + _addResourceTimingValues(commonMediaRequest, commonMediaResponse); } const _loadRequest = function (loader, httpRequest, httpResponse) { @@ -350,15 +351,15 @@ function HTTPLoader(cfg) { errHandler.error(new DashJSError(downloadErrorToRequestTypeMap[requestObject.type], requestObject.url + ' is not available', { request: requestObject, - response: httpResponse + response: commonMediaResponse })); if (config.error) { - config.error(requestObject, 'error', httpResponse.statusText, httpResponse); + config.error(requestObject, 'error', commonMediaResponse.statusText, commonMediaResponse); } if (config.complete) { - config.complete(requestObject, httpResponse.statusText); + config.complete(requestObject, commonMediaResponse.statusText); } } } @@ -368,8 +369,8 @@ function HTTPLoader(cfg) { const traces = []; let firstProgress, requestStartTime, lastTraceTime, lastTraceReceivedCount, progressTimeout; - let httpRequest; // CommonMediaRequest - let httpResponse; // CommonMediaResponse + let commonMediaRequest; + let commonMediaResponse; requestObject.bytesLoaded = NaN; requestObject.bytesTotal = NaN; @@ -396,7 +397,8 @@ function HTTPLoader(cfg) { } const withCredentials = customParametersModel.getXHRWithCredentialsForType(requestObject.type); - httpRequest = /* CommonMediaRequest */{ + + commonMediaRequest = new CommonMediaRequest({ url: requestObject.url, method: HTTPRequest.GET, responseType: requestObject.responseType, @@ -405,29 +407,28 @@ function HTTPLoader(cfg) { timeout: requestTimeout, cmcd: cmcdModel.getCmcdData(requestObject), customData: { request: requestObject } - }; + }); - // Init response (CommoneMediaResponse) - httpResponse = { - request: httpRequest, + commonMediaResponse = new CommonMediaResponse({ + request: commonMediaRequest, resourceTiming: { startTime: Date.now(), encodedBodySize: 0 }, status: 0 - }; + }); // Adds the ability to delay single fragment loading time to control buffer. let now = new Date().getTime(); if (isNaN(requestObject.delayLoadingTime) || now >= requestObject.delayLoadingTime) { // no delay - just send - httpRequests.push(httpRequest); - return _loadRequest(loader, httpRequest, httpResponse); + httpRequests.push(commonMediaRequest); + return _loadRequest(loader, commonMediaRequest, commonMediaResponse); } else { // delay let delayedRequest = { - httpRequest, - httpResponse + httpRequest: commonMediaRequest, + httpResponse: commonMediaResponse }; delayedRequests.push(delayedRequest); delayedRequest.delayTimeout = setTimeout(function () { diff --git a/src/streaming/net/XHRLoader.js b/src/streaming/net/XHRLoader.js index fea478cec1..f724750242 100644 --- a/src/streaming/net/XHRLoader.js +++ b/src/streaming/net/XHRLoader.js @@ -43,45 +43,45 @@ function XHRLoader() { /** * Load request - * @param {CommonMediaRequest} httpRequest - * @param {CommonMediaResponse} httpResponse + * @param {CommonMediaRequest} commonMediaRequest + * @param {CommonMediaResponse} commonMediaResponse */ - function load(httpRequest, httpResponse) { + function load(commonMediaRequest, commonMediaResponse) { xhr = null; xhr = new XMLHttpRequest(); - xhr.open(httpRequest.method, httpRequest.url, true); + xhr.open(commonMediaRequest.method, commonMediaRequest.url, true); - if (httpRequest.responseType) { - xhr.responseType = httpRequest.responseType; + if (commonMediaRequest.responseType) { + xhr.responseType = commonMediaRequest.responseType; } - if (httpRequest.headers) { - for (let header in httpRequest.headers) { - let value = httpRequest.headers[header]; + if (commonMediaRequest.headers) { + for (let header in commonMediaRequest.headers) { + let value = commonMediaRequest.headers[header]; if (value) { xhr.setRequestHeader(header, value); } } } - xhr.withCredentials = httpRequest.credentials === 'include'; - xhr.timeout = httpRequest.timeout; + xhr.withCredentials = commonMediaRequest.credentials === 'include'; + xhr.timeout = commonMediaRequest.timeout; xhr.onload = function () { - httpResponse.url = this.responseURL; - httpResponse.status = this.status; - httpResponse.statusText = this.statusText; - httpResponse.headers = Utils.parseHttpHeaders(this.getAllResponseHeaders()); - httpResponse.data = this.response; + commonMediaResponse.url = this.responseURL; + commonMediaResponse.status = this.status; + commonMediaResponse.statusText = this.statusText; + commonMediaResponse.headers = Utils.parseHttpHeaders(this.getAllResponseHeaders()); + commonMediaResponse.data = this.response; } - xhr.onloadend = httpRequest.customData.onloadend; - xhr.onprogress = httpRequest.customData.onprogress; - xhr.onabort = httpRequest.customData.onabort; - xhr.ontimeout = httpRequest.customData.ontimeout; + xhr.onloadend = commonMediaRequest.customData.onloadend; + xhr.onprogress = commonMediaRequest.customData.onprogress; + xhr.onabort = commonMediaRequest.customData.onabort; + xhr.ontimeout = commonMediaRequest.customData.ontimeout; xhr.send(); - httpRequest.customData.abort = abort.bind(this); + commonMediaRequest.customData.abort = abort.bind(this); return true; } diff --git a/src/streaming/text/TextController.js b/src/streaming/text/TextController.js index ab479f3d38..6fe1882bc7 100644 --- a/src/streaming/text/TextController.js +++ b/src/streaming/text/TextController.js @@ -508,6 +508,23 @@ function TextController(config) { } } + function clearDataForStream(streamId) { + if (textSourceBuffers[streamId]) { + textSourceBuffers[streamId].resetEmbedded(); + textSourceBuffers[streamId].reset(); + delete textSourceBuffers[streamId]; + } + + if (textTracks[streamId]) { + textTracks[streamId].deleteAllTextTracks(); + delete textTracks[streamId]; + } + + if (streamData[streamId]) { + delete streamData[streamId]; + } + } + function resetInitialSettings() { textSourceBuffers = {}; textTracks = {}; @@ -518,6 +535,11 @@ function TextController(config) { } function reset() { + Object.keys(textSourceBuffers).forEach((key) => { + textSourceBuffers[key].resetEmbedded(); + textSourceBuffers[key].reset(); + }); + dvbFonts.reset(); resetInitialSettings(); eventBus.off(Events.TEXT_TRACKS_QUEUE_INITIALIZED, _onTextTracksAdded, instance); @@ -528,11 +550,6 @@ function TextController(config) { eventBus.off(Events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); eventBus.off(Events.PLAYBACK_SEEKING, _onPlaybackSeeking, instance) } - - Object.keys(textSourceBuffers).forEach((key) => { - textSourceBuffers[key].resetEmbedded(); - textSourceBuffers[key].reset(); - }); } instance = { @@ -550,6 +567,7 @@ function TextController(config) { isTextEnabled, reset, setTextTrack, + clearDataForStream, }; setup(); return instance; diff --git a/src/streaming/thumbnail/ThumbnailTracks.js b/src/streaming/thumbnail/ThumbnailTracks.js index ca41d51fef..daa26cc886 100644 --- a/src/streaming/thumbnail/ThumbnailTracks.js +++ b/src/streaming/thumbnail/ThumbnailTracks.js @@ -325,6 +325,10 @@ function ThumbnailTracks(config) { representations = []; currentTrackIndex = -1; mediaInfo = null; + if (dashHandler) { + dashHandler.reset(); + dashHandler = null; + } } instance = { diff --git a/src/streaming/utils/BoxParser.js b/src/streaming/utils/BoxParser.js index 73138aa091..e856dfce99 100644 --- a/src/streaming/utils/BoxParser.js +++ b/src/streaming/utils/BoxParser.js @@ -71,25 +71,27 @@ function BoxParser(/*config*/) { /** * From the list of type boxes to look for, returns the latest one that is fully completed (header + payload). This * method only looks into the list of top boxes and doesn't analyze nested boxes. - * @param {string[]} types + * @param {string[]} boxTypesToSearchFor * @param {ArrayBuffer|uint8Array} buffer * @param {number} offset * @returns {IsoBoxSearchInfo} * @memberof BoxParser# */ - function findLastTopIsoBoxCompleted(types, buffer, offset) { + function findLastTopIsoBoxCompleted(boxTypesToSearchFor, buffer, offset) { if (offset === undefined) { offset = 0; } // 8 = size (uint32) + type (4 characters) if (!buffer || offset + 8 >= buffer.byteLength) { - return new IsoBoxSearchInfo(0, false); + return new IsoBoxSearchInfo({ found: false }); } const data = (buffer instanceof ArrayBuffer) ? new Uint8Array(buffer) : buffer; let boxInfo; - let lastCompletedOffset = 0; + let startOffsetOfLastCompletedBox = 0; + let sizeOfLastCompletedBox = 0; + let typeOfLastCompletedBox = null; while (offset < data.byteLength) { const boxSize = parseUint32(data, offset); const boxType = parseIsoBoxType(data, offset + 4); @@ -99,10 +101,17 @@ function BoxParser(/*config*/) { } if (offset + boxSize <= data.byteLength) { - if (types.indexOf(boxType) >= 0) { - boxInfo = new IsoBoxSearchInfo(offset, true, boxSize); + if (boxTypesToSearchFor.indexOf(boxType) >= 0) { + boxInfo = new IsoBoxSearchInfo({ + found: true, + startOffsetOfLastFoundTargetBox: offset, + sizeOfLastFoundTargetBox: boxSize, + typeOfLastTargetBox: boxType + }); } else { - lastCompletedOffset = offset + boxSize; + startOffsetOfLastCompletedBox = offset; + sizeOfLastCompletedBox = boxSize; + typeOfLastCompletedBox = boxType; } } @@ -110,7 +119,12 @@ function BoxParser(/*config*/) { } if (!boxInfo) { - return new IsoBoxSearchInfo(lastCompletedOffset, false); + return new IsoBoxSearchInfo({ + found: false, + startOffsetOfLastCompletedBox, + sizeOfLastCompletedBox, + typeOfLastCompletedBox + }); } return boxInfo; @@ -259,58 +273,12 @@ function BoxParser(/*config*/) { return initRange; } - /** - * Real-time parsing (whenever data is loaded in the buffer payload) of the payload to capture the moof of a chunk - * @param {array} types - * @param {ArrayBuffer} buffer - * @param {number} offset - * @return {IsoBoxSearchInfo} - */ - function parsePayload(types, buffer, offset) { - if (offset === undefined) { - offset = 0; - } - - if (!buffer || offset + 8 >= buffer.byteLength) { - return new IsoBoxSearchInfo(0, false); - } - - const data = (buffer instanceof ArrayBuffer) ? new Uint8Array(buffer) : buffer; - let boxInfo; - let lastCompletedOffset = 0; - while (offset < data.byteLength) { - const boxSize = parseUint32(data, offset); - const boxType = parseIsoBoxType(data, offset + 4); - - if (boxSize === 0) { - break; - } - - if (offset + boxSize <= data.byteLength) { - if (types.indexOf(boxType) >= 0) { - boxInfo = new IsoBoxSearchInfo(offset, true, boxSize, boxType); - } else { - lastCompletedOffset = offset + boxSize; - } - } - - offset += boxSize; - } - - if (!boxInfo) { - return new IsoBoxSearchInfo(lastCompletedOffset, false); - } - - return boxInfo; - } - instance = { - parse, + findInitRange, findLastTopIsoBoxCompleted, getMediaTimescaleFromMoov, getSamplesInfo, - findInitRange, - parsePayload + parse, }; setup(); diff --git a/src/streaming/vo/CommonMediaRequest.js b/src/streaming/vo/CommonMediaRequest.js new file mode 100644 index 0000000000..819d0e8884 --- /dev/null +++ b/src/streaming/vo/CommonMediaRequest.js @@ -0,0 +1,27 @@ +class CommonMediaRequest { + /** + * @param {Object} params + * @param {string} params.url + * @param {string} params.method + * @param {string} [params.responseType] + * @param {Object} [params.headers] + * @param {RequestCredentials} [params.credentials] + * @param {RequestMode} [params.mode] + * @param {number} [params.timeout] + * @param {Cmcd} [params.cmcd] + * @param {any} [params.customData] + */ + constructor(params) { + this.url = params.url; + this.method = params.method; + this.responseType = params.responseType !== undefined ? params.responseType : null; + this.headers = params.headers !== undefined ? params.headers : {}; + this.credentials = params.credentials !== undefined ? params.credentials : null; + this.mode = params.mode !== undefined ? params.mode : null; + this.timeout = params.timeout !== undefined ? params.timeout : 0; + this.cmcd = params.cmcd !== undefined ? params.cmcd : null; + this.customData = params.customData !== undefined ? params.customData : null; + } +} + +export default CommonMediaRequest; diff --git a/src/streaming/vo/CommonMediaResponse.js b/src/streaming/vo/CommonMediaResponse.js new file mode 100644 index 0000000000..e4d7f2c654 --- /dev/null +++ b/src/streaming/vo/CommonMediaResponse.js @@ -0,0 +1,27 @@ +class CommonMediaResponse { + /** + * @param {Object} params + * @param {CommonMediaRequest} params.request + * @param {string} [params.url] + * @param {boolean} [params.redirected] + * @param {number} [params.status] + * @param {string} [params.statusText] + * @param {string} [params.type] + * @param {Object} [params.headers] + * @param {any} [params.data] + * @param {ResourceTiming} [params.resourceTiming] + */ + constructor(params) { + this.request = params.request; + this.url = params.url !== undefined ? params.url : null; + this.redirected = params.redirected !== undefined ? params.redirected : false; + this.status = params.status !== undefined ? params.status : null; + this.statusText = params.statusText !== undefined ? params.statusText : ''; + this.type = params.type !== undefined ? params.type : ''; + this.headers = params.headers !== undefined ? params.headers : {}; + this.data = params.data !== undefined ? params.data : null; + this.resourceTiming = params.resourceTiming !== undefined ? params.resourceTiming : null; + } +} + +export default CommonMediaResponse; diff --git a/src/streaming/vo/IsoBoxSearchInfo.js b/src/streaming/vo/IsoBoxSearchInfo.js index 39bd03d2eb..b60b2ccf21 100644 --- a/src/streaming/vo/IsoBoxSearchInfo.js +++ b/src/streaming/vo/IsoBoxSearchInfo.js @@ -33,10 +33,22 @@ * @ignore */ class IsoBoxSearchInfo { - constructor(lastCompletedOffset, found, size) { - this.lastCompletedOffset = lastCompletedOffset; - this.found = found; - this.size = size; + constructor({ + found, + sizeOfLastCompletedBox, + sizeOfLastFoundTargetBox, + startOffsetOfLastCompletedBox, + startOffsetOfLastFoundTargetBox, + typeOfLastCompletedBox, + typeOfLastTargetBox, + }) { + this.found = found !== undefined ? found : false; + this.sizeOfLastCompletedBox = sizeOfLastCompletedBox !== undefined ? sizeOfLastCompletedBox : 0; + this.sizeOfLastFoundTargetBox = sizeOfLastFoundTargetBox !== undefined ? sizeOfLastFoundTargetBox : 0; + this.startOffsetOfLastCompletedBox = startOffsetOfLastCompletedBox !== undefined ? startOffsetOfLastCompletedBox : 0; + this.startOffsetOfLastFoundTargetBox = startOffsetOfLastFoundTargetBox !== undefined ? startOffsetOfLastFoundTargetBox : 0; + this.typeOfLastCompletedBox = typeOfLastCompletedBox !== undefined ? typeOfLastCompletedBox : null; + this.typeOfLastTargetBox = typeOfLastTargetBox !== undefined ? typeOfLastTargetBox : null; } } diff --git a/test/unit/mocks/TextControllerMock.js b/test/unit/mocks/TextControllerMock.js index d70c287f8a..7c450bab70 100644 --- a/test/unit/mocks/TextControllerMock.js +++ b/test/unit/mocks/TextControllerMock.js @@ -32,6 +32,9 @@ class TextControllerMock { initializeForStream() { } + + clearDataForStream() { + } } export default TextControllerMock; diff --git a/test/unit/test/streaming/streaming.net.FetchLoader.js b/test/unit/test/streaming/streaming.net.FetchLoader.js index f52322ddc1..7f9f6e15cc 100644 --- a/test/unit/test/streaming/streaming.net.FetchLoader.js +++ b/test/unit/test/streaming/streaming.net.FetchLoader.js @@ -11,10 +11,10 @@ describe('FetchLoader', function () { fetchLoader = FetchLoader(context).create({}); // 1MB test, 2.4MB received - const Data1MB = '[{"ts":1529393808348,"bytes":16384},{"ts":1529393808617,"bytes":32768},{"ts":1529393808864,"bytes":32768},{"ts":1529393809121,"bytes":32768},{"ts":1529393809378,"bytes":32768},{"ts":1529393809637,"bytes":32768},{"ts":1529393809942,"bytes":32768},{"ts":1529393810258,"bytes":32768},{"ts":1529393810516,"bytes":32768},{"ts":1529393810762,"bytes":32759},{"ts":1529393811020,"bytes":32768},{"ts":1529393811326,"bytes":32768},{"ts":1529393811582,"bytes":32768},{"ts":1529393811840,"bytes":32768},{"ts":1529393812099,"bytes":32768},{"ts":1529393812380,"bytes":32768},{"ts":1529393812508,"bytes":16393},{"ts":1529393812766,"bytes":32768},{"ts":1529393813023,"bytes":32768},{"ts":1529393813327,"bytes":32759},{"ts":1529393813585,"bytes":32768},{"ts":1529393813843,"bytes":32768},{"ts":1529393814090,"bytes":32768},{"ts":1529393814384,"bytes":32768},{"ts":1529393814641,"bytes":32768},{"ts":1529393814899,"bytes":32768},{"ts":1529393815191,"bytes":32768},{"ts":1529393815449,"bytes":32768},{"ts":1529393815708,"bytes":32759},{"ts":1529393815954,"bytes":32768},{"ts":1529393816233,"bytes":32768},{"ts":1529393816492,"bytes":32768},{"ts":1529393816750,"bytes":32768},{"ts":1529393816751,"bytes":18},{"ts":1529393817007,"bytes":32768},{"ts":1529393817264,"bytes":32768},{"ts":1529393817510,"bytes":32768},{"ts":1529393817792,"bytes":32768},{"ts":1529393818075,"bytes":32759},{"ts":1529393818368,"bytes":32768},{"ts":1529393818626,"bytes":32768},{"ts":1529393818883,"bytes":32768},{"ts":1529393819174,"bytes":32768},{"ts":1529393819422,"bytes":32768},{"ts":1529393819679,"bytes":32768},{"ts":1529393819937,"bytes":32768},{"ts":1529393820241,"bytes":32768},{"ts":1529393820499,"bytes":32759},{"ts":1529393820758,"bytes":32768},{"ts":1529393821004,"bytes":32768},{"ts":1529393821006,"bytes":18},{"ts":1529393821332,"bytes":32768},{"ts":1529393821589,"bytes":32768},{"ts":1529393821848,"bytes":32768},{"ts":1529393822104,"bytes":32768},{"ts":1529393822398,"bytes":32768},{"ts":1529393822656,"bytes":32768},{"ts":1529393822903,"bytes":32768},{"ts":1529393823193,"bytes":32759},{"ts":1529393823453,"bytes":32768},{"ts":1529393823710,"bytes":32768},{"ts":1529393823968,"bytes":32768},{"ts":1529393824261,"bytes":32768},{"ts":1529393824506,"bytes":32768},{"ts":1529393824766,"bytes":32768},{"ts":1529393825022,"bytes":32768},{"ts":1529393825280,"bytes":32768},{"ts":1529393825282,"bytes":9},{"ts":1529393825538,"bytes":32759},{"ts":1529393825797,"bytes":32768},{"ts":1529393826090,"bytes":32768},{"ts":1529393826382,"bytes":32768},{"ts":1529393826639,"bytes":32768},{"ts":1529393826897,"bytes":32768},{"ts":1529393827191,"bytes":32768},{"ts":1529393827448,"bytes":32768},{"ts":1529393827706,"bytes":32768},{"ts":1529393827800,"bytes":13409}]'; + const Data1MB = '[{"timestamp":1529393808348,"bytes":16384},{"timestamp":1529393808617,"bytes":32768},{"timestamp":1529393808864,"bytes":32768},{"timestamp":1529393809121,"bytes":32768},{"timestamp":1529393809378,"bytes":32768},{"timestamp":1529393809637,"bytes":32768},{"timestamp":1529393809942,"bytes":32768},{"timestamp":1529393810258,"bytes":32768},{"timestamp":1529393810516,"bytes":32768},{"timestamp":1529393810762,"bytes":32759},{"timestamp":1529393811020,"bytes":32768},{"timestamp":1529393811326,"bytes":32768},{"timestamp":1529393811582,"bytes":32768},{"timestamp":1529393811840,"bytes":32768},{"timestamp":1529393812099,"bytes":32768},{"timestamp":1529393812380,"bytes":32768},{"timestamp":1529393812508,"bytes":16393},{"timestamp":1529393812766,"bytes":32768},{"timestamp":1529393813023,"bytes":32768},{"timestamp":1529393813327,"bytes":32759},{"timestamp":1529393813585,"bytes":32768},{"timestamp":1529393813843,"bytes":32768},{"timestamp":1529393814090,"bytes":32768},{"timestamp":1529393814384,"bytes":32768},{"timestamp":1529393814641,"bytes":32768},{"timestamp":1529393814899,"bytes":32768},{"timestamp":1529393815191,"bytes":32768},{"timestamp":1529393815449,"bytes":32768},{"timestamp":1529393815708,"bytes":32759},{"timestamp":1529393815954,"bytes":32768},{"timestamp":1529393816233,"bytes":32768},{"timestamp":1529393816492,"bytes":32768},{"timestamp":1529393816750,"bytes":32768},{"timestamp":1529393816751,"bytes":18},{"timestamp":1529393817007,"bytes":32768},{"timestamp":1529393817264,"bytes":32768},{"timestamp":1529393817510,"bytes":32768},{"timestamp":1529393817792,"bytes":32768},{"timestamp":1529393818075,"bytes":32759},{"timestamp":1529393818368,"bytes":32768},{"timestamp":1529393818626,"bytes":32768},{"timestamp":1529393818883,"bytes":32768},{"timestamp":1529393819174,"bytes":32768},{"timestamp":1529393819422,"bytes":32768},{"timestamp":1529393819679,"bytes":32768},{"timestamp":1529393819937,"bytes":32768},{"timestamp":1529393820241,"bytes":32768},{"timestamp":1529393820499,"bytes":32759},{"timestamp":1529393820758,"bytes":32768},{"timestamp":1529393821004,"bytes":32768},{"timestamp":1529393821006,"bytes":18},{"timestamp":1529393821332,"bytes":32768},{"timestamp":1529393821589,"bytes":32768},{"timestamp":1529393821848,"bytes":32768},{"timestamp":1529393822104,"bytes":32768},{"timestamp":1529393822398,"bytes":32768},{"timestamp":1529393822656,"bytes":32768},{"timestamp":1529393822903,"bytes":32768},{"timestamp":1529393823193,"bytes":32759},{"timestamp":1529393823453,"bytes":32768},{"timestamp":1529393823710,"bytes":32768},{"timestamp":1529393823968,"bytes":32768},{"timestamp":1529393824261,"bytes":32768},{"timestamp":1529393824506,"bytes":32768},{"timestamp":1529393824766,"bytes":32768},{"timestamp":1529393825022,"bytes":32768},{"timestamp":1529393825280,"bytes":32768},{"timestamp":1529393825282,"bytes":9},{"timestamp":1529393825538,"bytes":32759},{"timestamp":1529393825797,"bytes":32768},{"timestamp":1529393826090,"bytes":32768},{"timestamp":1529393826382,"bytes":32768},{"timestamp":1529393826639,"bytes":32768},{"timestamp":1529393826897,"bytes":32768},{"timestamp":1529393827191,"bytes":32768},{"timestamp":1529393827448,"bytes":32768},{"timestamp":1529393827706,"bytes":32768},{"timestamp":1529393827800,"bytes":13409}]'; let result1MB = fetchLoader.calculateDownloadedTime(JSON.parse(Data1MB), 2405464); - const Data300MB = '[{"ts":1529394745999,"bytes":32768},{"ts":1529394746180,"bytes":32768},{"ts":1529394746182,"bytes":32768},{"ts":1529394746359,"bytes":32768},{"ts":1529394746361,"bytes":65536},{"ts":1529394746363,"bytes":16384},{"ts":1529394746537,"bytes":16384},{"ts":1529394746538,"bytes":65536},{"ts":1529394746540,"bytes":38539},{"ts":1529394746718,"bytes":32768},{"ts":1529394746720,"bytes":65536},{"ts":1529394746720,"bytes":32768},{"ts":1529394746897,"bytes":32768},{"ts":1529394746899,"bytes":26997},{"ts":1529394746900,"bytes":5771},{"ts":1529394746903,"bytes":65536},{"ts":1529394747072,"bytes":5771},{"ts":1529394747098,"bytes":49152},{"ts":1529394747099,"bytes":16384},{"ts":1529394747101,"bytes":65536},{"ts":1529394747274,"bytes":32768},{"ts":1529394747275,"bytes":65536},{"ts":1529394747277,"bytes":49152},{"ts":1529394747453,"bytes":22155},{"ts":1529394748096,"bytes":32761},{"ts":1529394748098,"bytes":16391},{"ts":1529394748099,"bytes":16384},{"ts":1529394748273,"bytes":32768},{"ts":1529394748275,"bytes":16384},{"ts":1529394748277,"bytes":31839},{"ts":1529394748279,"bytes":33697},{"ts":1529394748452,"bytes":32768},{"ts":1529394748454,"bytes":16384},{"ts":1529394748456,"bytes":65536},{"ts":1529394748458,"bytes":5771},{"ts":1529394749095,"bytes":16384},{"ts":1529394749097,"bytes":32768},{"ts":1529394749279,"bytes":16384},{"ts":1529394749279,"bytes":32768},{"ts":1529394749281,"bytes":65536},{"ts":1529394749283,"bytes":16384},{"ts":1529394749458,"bytes":32768},{"ts":1529394749460,"bytes":65536},{"ts":1529394749461,"bytes":22155},{"ts":1529394750095,"bytes":32761},{"ts":1529394750096,"bytes":7},{"ts":1529394750097,"bytes":16384},{"ts":1529394750273,"bytes":20297},{"ts":1529394750275,"bytes":28855},{"ts":1529394750276,"bytes":65536},{"ts":1529394750452,"bytes":32768},{"ts":1529394750454,"bytes":65536},{"ts":1529394750455,"bytes":16384},{"ts":1529394750551,"bytes":16384},{"ts":1529394750628,"bytes":5771},{"ts":1529394751095,"bytes":32761},{"ts":1529394751097,"bytes":7},{"ts":1529394751099,"bytes":16384},{"ts":1529394751275,"bytes":65536},{"ts":1529394751277,"bytes":65536},{"ts":1529394751452,"bytes":32768},{"ts":1529394751454,"bytes":65536},{"ts":1529394751455,"bytes":14526},{"ts":1529394751456,"bytes":7629},{"ts":1529394752095,"bytes":16384},{"ts":1529394752098,"bytes":32768},{"ts":1529394752273,"bytes":32768},{"ts":1529394752276,"bytes":65536},{"ts":1529394752277,"bytes":32768},{"ts":1529394752451,"bytes":32768},{"ts":1529394752454,"bytes":65536},{"ts":1529394752466,"bytes":22155}]'; + const Data300MB = '[{"timestamp":1529394745999,"bytes":32768},{"timestamp":1529394746180,"bytes":32768},{"timestamp":1529394746182,"bytes":32768},{"timestamp":1529394746359,"bytes":32768},{"timestamp":1529394746361,"bytes":65536},{"timestamp":1529394746363,"bytes":16384},{"timestamp":1529394746537,"bytes":16384},{"timestamp":1529394746538,"bytes":65536},{"timestamp":1529394746540,"bytes":38539},{"timestamp":1529394746718,"bytes":32768},{"timestamp":1529394746720,"bytes":65536},{"timestamp":1529394746720,"bytes":32768},{"timestamp":1529394746897,"bytes":32768},{"timestamp":1529394746899,"bytes":26997},{"timestamp":1529394746900,"bytes":5771},{"timestamp":1529394746903,"bytes":65536},{"timestamp":1529394747072,"bytes":5771},{"timestamp":1529394747098,"bytes":49152},{"timestamp":1529394747099,"bytes":16384},{"timestamp":1529394747101,"bytes":65536},{"timestamp":1529394747274,"bytes":32768},{"timestamp":1529394747275,"bytes":65536},{"timestamp":1529394747277,"bytes":49152},{"timestamp":1529394747453,"bytes":22155},{"timestamp":1529394748096,"bytes":32761},{"timestamp":1529394748098,"bytes":16391},{"timestamp":1529394748099,"bytes":16384},{"timestamp":1529394748273,"bytes":32768},{"timestamp":1529394748275,"bytes":16384},{"timestamp":1529394748277,"bytes":31839},{"timestamp":1529394748279,"bytes":33697},{"timestamp":1529394748452,"bytes":32768},{"timestamp":1529394748454,"bytes":16384},{"timestamp":1529394748456,"bytes":65536},{"timestamp":1529394748458,"bytes":5771},{"timestamp":1529394749095,"bytes":16384},{"timestamp":1529394749097,"bytes":32768},{"timestamp":1529394749279,"bytes":16384},{"timestamp":1529394749279,"bytes":32768},{"timestamp":1529394749281,"bytes":65536},{"timestamp":1529394749283,"bytes":16384},{"timestamp":1529394749458,"bytes":32768},{"timestamp":1529394749460,"bytes":65536},{"timestamp":1529394749461,"bytes":22155},{"timestamp":1529394750095,"bytes":32761},{"timestamp":1529394750096,"bytes":7},{"timestamp":1529394750097,"bytes":16384},{"timestamp":1529394750273,"bytes":20297},{"timestamp":1529394750275,"bytes":28855},{"timestamp":1529394750276,"bytes":65536},{"timestamp":1529394750452,"bytes":32768},{"timestamp":1529394750454,"bytes":65536},{"timestamp":1529394750455,"bytes":16384},{"timestamp":1529394750551,"bytes":16384},{"timestamp":1529394750628,"bytes":5771},{"timestamp":1529394751095,"bytes":32761},{"timestamp":1529394751097,"bytes":7},{"timestamp":1529394751099,"bytes":16384},{"timestamp":1529394751275,"bytes":65536},{"timestamp":1529394751277,"bytes":65536},{"timestamp":1529394751452,"bytes":32768},{"timestamp":1529394751454,"bytes":65536},{"timestamp":1529394751455,"bytes":14526},{"timestamp":1529394751456,"bytes":7629},{"timestamp":1529394752095,"bytes":16384},{"timestamp":1529394752098,"bytes":32768},{"timestamp":1529394752273,"bytes":32768},{"timestamp":1529394752276,"bytes":65536},{"timestamp":1529394752277,"bytes":32768},{"timestamp":1529394752451,"bytes":32768},{"timestamp":1529394752454,"bytes":65536},{"timestamp":1529394752466,"bytes":22155}]'; let result300MB = fetchLoader.calculateDownloadedTime(JSON.parse(Data300MB), 2405464); expect(result1MB).to.be.above(10000); expect(result300MB).to.be.below(200); diff --git a/test/unit/test/streaming/streaming.utils.BoxParser.js b/test/unit/test/streaming/streaming.utils.BoxParser.js index d2ea057931..13a6ccc617 100644 --- a/test/unit/test/streaming/streaming.utils.BoxParser.js +++ b/test/unit/test/streaming/streaming.utils.BoxParser.js @@ -129,7 +129,8 @@ describe('BoxParser', function () { const res = boxParser.findLastTopIsoBoxCompleted(['moov'], data.buffer); expect(res.found).to.be.false; - expect(res.lastCompletedOffset).to.equal(36); + expect(res.startOffsetOfLastCompletedBox).to.equal(28); + expect(res.sizeOfLastCompletedBox).to.equal(8); }); it('should not return null when looking for init range in a completed init segment', () => {