diff --git a/index.d.ts b/index.d.ts index daf5be6ecf..611591973e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -193,7 +193,8 @@ declare namespace dashjs { longFormContentDurationThreshold?: number, stallThreshold?: number, useAppendWindow?: boolean, - setStallState?: boolean + setStallState?: boolean, + enableLiveSeekableRangeFix?: boolean }, gaps?: { jumpGaps?: boolean, diff --git a/package-lock.json b/package-lock.json index 09d7a26b93..d39b9fd71d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dashjs", - "version": "4.3.4", + "version": "4.3.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a8c51f8d38..e456d45866 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashjs", - "version": "4.3.4", + "version": "4.3.5", "description": "A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.", "author": "Dash Industry Forum", "license": "BSD-3-Clause", diff --git a/src/core/Settings.js b/src/core/Settings.js index 4953252f99..9fb20c6106 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -99,7 +99,8 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * longFormContentDurationThreshold: 600, * stallThreshold: 0.5, * useAppendWindow: true, - * setStallState: false + * setStallState: false, + * enableLiveSeekableRangeFix: true * }, * gaps: { * jumpGaps: true, @@ -301,6 +302,8 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest'; * Specifies if the appendWindow attributes of the MSE SourceBuffers should be set according to content duration from manifest. * @property {boolean} [setStallState=false] * Specifies if we fire manual waiting events once the stall threshold is reached + * @property {boolean} [enableLiveSeekableRangeFix=true] + * Sets `mediaSource.duration` when live seekable range changes if `mediaSource.setLiveSeekableRange` is unavailable. */ /** @@ -801,7 +804,8 @@ function Settings() { longFormContentDurationThreshold: 600, stallThreshold: 0.3, useAppendWindow: true, - setStallState: true + setStallState: true, + enableLiveSeekableRangeFix: true }, gaps: { jumpGaps: true, diff --git a/src/streaming/controllers/MediaSourceController.js b/src/streaming/controllers/MediaSourceController.js index 074d7fa3ce..6cce7efa99 100644 --- a/src/streaming/controllers/MediaSourceController.js +++ b/src/streaming/controllers/MediaSourceController.js @@ -70,27 +70,55 @@ function MediaSourceController() { videoModel.setSource(null); } - function setDuration(value) { + function setDuration(value, log = true) { if (!mediaSource || mediaSource.readyState !== 'open') return; if (value === null && isNaN(value)) return; if (mediaSource.duration === value) return; if (!isBufferUpdating(mediaSource)) { - logger.info('Set MediaSource duration:' + value); + if (log) { + logger.info('Set MediaSource duration:' + value); + } mediaSource.duration = value; } else { setTimeout(setDuration.bind(null, value), 50); } } - function setSeekable(start, end) { - if (mediaSource && typeof mediaSource.setLiveSeekableRange === 'function' && typeof mediaSource.clearLiveSeekableRange === 'function' && - mediaSource.readyState === 'open' && start >= 0 && start < end) { + function setSeekable(start, end, enableLiveSeekableRangeFix) { + if (!mediaSource || mediaSource.readyState !== 'open') return; + if (start < 0 || end <= start) return; + + if (typeof mediaSource.setLiveSeekableRange === 'function' && typeof mediaSource.clearLiveSeekableRange === 'function') { mediaSource.clearLiveSeekableRange(); mediaSource.setLiveSeekableRange(start, end); + } else if (enableLiveSeekableRangeFix) { + try { + const bufferedRangeEnd = getBufferedRangeEnd(mediaSource); + const targetMediaSourceDuration = Math.max(end, bufferedRangeEnd); + if (!isFinite(mediaSource.duration) || mediaSource.duration < targetMediaSourceDuration) { + setDuration(targetMediaSourceDuration, false); + } + } catch (e) { + logger.error(`Failed to set MediaSource duration! ` + e.toString()); + } } } + function getBufferedRangeEnd(source) { + let max = 0; + const buffers = source.sourceBuffers; + + for (let i = 0; i < buffers.length; i++) { + if (buffers[i].buffered.length > 0) { + const end = buffers[i].buffered.end(buffers[i].buffered.length - 1); + max = Math.max(end, max); + } + } + + return max; + } + function signalEndOfStream(source) { if (!source || source.readyState !== 'open') { return; diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 1ec6cc364c..2ccf114e74 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -423,7 +423,7 @@ function StreamController() { _setMediaDuration(); const dvrInfo = dashMetrics.getCurrentDVRInfo(); - mediaSourceController.setSeekable(dvrInfo.range.start, dvrInfo.range.end); + mediaSourceController.setSeekable(dvrInfo.range.start, dvrInfo.range.end, _enableLiveSeekableRangeFix()); _activateStream(seekTime, keepBuffers); } @@ -611,6 +611,10 @@ function StreamController() { } } + function _enableLiveSeekableRangeFix() { + return settings.get().streaming.buffer.enableLiveSeekableRangeFix; + } + /** * Initiate the preloading of the next stream * @param {object} nextStream @@ -1487,7 +1491,7 @@ function StreamController() { //Should we normalize and union the two? const targetMediaType = hasAudioTrack() ? Constants.AUDIO : Constants.VIDEO; if (e.mediaType === targetMediaType) { - mediaSourceController.setSeekable(e.value.range.start, e.value.range.end); + mediaSourceController.setSeekable(e.value.range.start, e.value.range.end, _enableLiveSeekableRangeFix()); } } }