From 5bf61a02e4f448b1e5039844344575b1a3b8ee43 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Wed, 24 Jun 2020 14:58:34 +0200 Subject: [PATCH 01/14] Refactor stream playback starting process: - always use seeking process from PlaybackController to seek controllers - adjust seek time in BufferController according to effective appended ranges - ScheduleController: do not stop scheduler when updating data (at manifest updates) --- src/streaming/Stream.js | 27 +- src/streaming/StreamProcessor.js | 89 ++++--- src/streaming/controllers/BufferController.js | 34 +-- .../controllers/PlaybackController.js | 242 +++++++----------- .../controllers/ScheduleController.js | 2 +- src/streaming/controllers/StreamController.js | 70 ++--- .../text/NotFragmentedTextBufferController.js | 4 - src/streaming/text/TextBufferController.js | 5 - ...reaming.controllers.PlaybackControllers.js | 3 - 9 files changed, 206 insertions(+), 270 deletions(-) diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js index 3769421309..8d2e1f70e7 100644 --- a/src/streaming/Stream.js +++ b/src/streaming/Stream.js @@ -67,10 +67,11 @@ function Stream(config) { let instance, logger, + streamInfo, streamProcessors, + isStreamInitialized, isStreamActivated, isMediaInitialized, - streamInfo, hasVideoTrack, hasAudioTrack, updateError, @@ -227,6 +228,7 @@ function Stream(config) { function resetInitialSettings() { deactivate(); streamInfo = null; + isStreamInitialized = false; hasVideoTrack = false; hasAudioTrack = false; updateError = {}; @@ -261,6 +263,19 @@ function Stream(config) { return streamInfo ? streamInfo.start : NaN; } + function getLiveStartTime() { + if (!streamInfo.manifestInfo.isDynamic) return NaN; + // Get live start time of the video stream (1st in array of streams) + // or audio if no video stream + for (let i = 0; i < streamProcessors.length; i++) { + if (streamProcessors[i].getType() === Constants.AUDIO || + streamProcessors[i].getType() === Constants.VIDEO) { + return streamProcessors[i].getLiveStartTime(); + } + } + return NaN; + } + function getId() { return streamInfo ? streamInfo.id : null; } @@ -610,8 +625,14 @@ function Stream(config) { if (error) { errHandler.error(error); - } else { - eventBus.trigger(Events.STREAM_INITIALIZED, { streamInfo: streamInfo }); + } else if (!isStreamInitialized) { + isStreamInitialized = true; + timelineConverter.setTimeSyncCompleted(true); + + eventBus.trigger(Events.STREAM_INITIALIZED, { + streamInfo: streamInfo, + liveStartTime: getLiveStartTime() + }); } } diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index 5957356a34..0f8fd1844f 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -90,8 +90,7 @@ function StreamProcessor(config) { logger = Debug(context).getInstance().getLogger(instance); resetInitialSettings(); - eventBus.on(Events.STREAM_INITIALIZED, onStreamInitialized, instance); - eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance); + eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance, EventBus.EVENT_PRIORITY_HIGH); // High priority to be notified before Stream eventBus.on(Events.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance); eventBus.on(Events.INIT_FRAGMENT_NEEDED, onInitFragmentNeeded, instance); eventBus.on(Events.MEDIA_FRAGMENT_NEEDED, onMediaFragmentNeeded, instance); @@ -208,7 +207,6 @@ function StreamProcessor(config) { abrController.unRegisterStreamType(type); } - eventBus.off(Events.STREAM_INITIALIZED, onStreamInitialized, instance); eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, instance); eventBus.off(Events.QUALITY_CHANGE_REQUESTED, onQualityChanged, instance); eventBus.off(Events.INIT_FRAGMENT_NEEDED, onInitFragmentNeeded, instance); @@ -228,27 +226,10 @@ function StreamProcessor(config) { return representationController ? representationController.isUpdating() : false; } - function onStreamInitialized(e) { - if (!e.streamInfo || streamInfo.id !== e.streamInfo.id) return; - - if (!streamInitialized) { - streamInitialized = true; - if (isDynamic) { - timelineConverter.setTimeSyncCompleted(true); - setLiveEdgeSeekTarget(); - } else { - const seekTarget = playbackController.getStreamStartTime(false); - bufferController.setSeekStartTime(seekTarget); - scheduleController.setCurrentRepresentation(getRepresentationInfo()); - scheduleController.setSeekTarget(seekTarget); - } - } - - scheduleController.start(); - } function onDataUpdateCompleted(e) { if (e.sender.getType() !== getType() || e.sender.getStreamId() !== streamInfo.id) return; + logger.info('DataUpdateCompleted'); if (!e.error) { scheduleController.setCurrentRepresentation(adapter.convertDataToRepresentationInfo(e.currentRepresentation)); @@ -624,9 +605,12 @@ function StreamProcessor(config) { return controller; } - function setLiveEdgeSeekTarget() { - if (!liveEdgeFinder) return; + function getLiveStartTime() { + if (!isDynamic) return NaN; + if (!liveEdgeFinder) return NaN; + + let liveStartTime = NaN; const currentRepresentationInfo = getRepresentationInfo(); const liveEdge = liveEdgeFinder.getLiveEdge(currentRepresentationInfo); const request = findRequestForLiveEdge(liveEdge, currentRepresentationInfo); @@ -635,31 +619,51 @@ function StreamProcessor(config) { // When low latency mode is selected but browser doesn't support fetch // start at the beginning of the segment to avoid consuming the whole buffer if (settings.get().streaming.lowLatencyEnabled) { - const liveStartTime = request.duration < mediaPlayerModel.getLiveDelay() ? request.startTime : request.startTime + request.duration - mediaPlayerModel.getLiveDelay(); - playbackController.setLiveStartTime(liveStartTime); + liveStartTime = request.duration < mediaPlayerModel.getLiveDelay() ? request.startTime : request.startTime + request.duration - mediaPlayerModel.getLiveDelay(); } else { - playbackController.setLiveStartTime(request.startTime); + liveStartTime = request.startTime; } } - const seekTarget = playbackController.getStreamStartTime(false, liveEdge); - bufferController.setSeekStartTime(seekTarget); - scheduleController.setCurrentRepresentation(currentRepresentationInfo); - scheduleController.setSeekTarget(seekTarget); - scheduleController.start(); + return liveStartTime; + } - // For multi periods stream, if the startTime is beyond current period then seek to corresponding period (see StreamController::onPlaybackSeeking) - if (seekTarget > (currentRepresentationInfo.mediaInfo.streamInfo.start + currentRepresentationInfo.mediaInfo.streamInfo.duration)) { - playbackController.seek(seekTarget); - } + // function setLiveEdgeSeekTarget() { + // if (!liveEdgeFinder) return; - dashMetrics.updateManifestUpdateInfo({ - currentTime: seekTarget, - presentationStartTime: liveEdge, - latency: liveEdge - seekTarget, - clientTimeOffset: timelineConverter.getClientTimeOffset() - }); - } + // const currentRepresentationInfo = getRepresentationInfo(); + // const liveEdge = liveEdgeFinder.getLiveEdge(currentRepresentationInfo); + // const request = findRequestForLiveEdge(liveEdge, currentRepresentationInfo); + + // if (request) { + // // When low latency mode is selected but browser doesn't support fetch + // // start at the beginning of the segment to avoid consuming the whole buffer + // if (settings.get().streaming.lowLatencyEnabled) { + // const liveStartTime = request.duration < mediaPlayerModel.getLiveDelay() ? request.startTime : request.startTime + request.duration - mediaPlayerModel.getLiveDelay(); + // playbackController.setLiveStartTime(liveStartTime); + // } else { + // playbackController.setLiveStartTime(request.startTime); + // } + // } + + // const seekTarget = playbackController.getStreamStartTime(false, liveEdge); + // bufferController.setSeekStartTime(seekTarget); + // scheduleController.setCurrentRepresentation(currentRepresentationInfo); + // scheduleController.setSeekTarget(seekTarget); + // scheduleController.start(); + + // // For multi periods stream, if the startTime is beyond current period then seek to corresponding period (see StreamController::onPlaybackSeeking) + // if (seekTarget > (currentRepresentationInfo.mediaInfo.streamInfo.start + currentRepresentationInfo.mediaInfo.streamInfo.duration)) { + // playbackController.seek(seekTarget); + // } + + // dashMetrics.updateManifestUpdateInfo({ + // currentTime: seekTarget, + // presentationStartTime: liveEdge, + // latency: liveEdge - seekTarget, + // clientTimeOffset: timelineConverter.getClientTimeOffset() + // }); + // } function findRequestForLiveEdge(liveEdge, currentRepresentationInfo) { try { @@ -763,6 +767,7 @@ function StreamProcessor(config) { getStreamInfo: getStreamInfo, selectMediaInfo: selectMediaInfo, addMediaInfo: addMediaInfo, + getLiveStartTime: getLiveStartTime, switchTrackAsked: switchTrackAsked, getMediaInfoArr: getMediaInfoArr, getMediaInfo: getMediaInfo, diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index 907ae382d4..bbc2f1512c 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -85,7 +85,7 @@ function BufferController(config) { isPruningInProgress, isQuotaExceeded, initCache, - seekStartTime, + seekTarget, seekClearedBufferingCompleted, pendingPruningRanges, replacingBuffer, @@ -281,6 +281,11 @@ function BufferController(config) { if (appendedBytesInfo.segmentType === HTTPRequest.MEDIA_SEGMENT_TYPE) { showBufferRanges(ranges); onPlaybackProgression(); + + // If seeking, seek video model to range start in case appended segment starts beyond seek target + if (!isNaN(seekTarget) && (seekTarget < ranges.start(0) || playbackController.getTime() === 0)) { + playbackController.seek(ranges.start(0), true, true); + } } else { if (replacingBuffer) { const currentTime = playbackController.getTime(); @@ -309,7 +314,8 @@ function BufferController(config) { //********************************************************************** // START Buffer Level, State & Sufficiency Handling. //********************************************************************** - function onPlaybackSeeking(/*e*/) { + function onPlaybackSeeking(e) { + seekTarget = e.seekTime; if (isBufferingCompleted) { seekClearedBufferingCompleted = true; isBufferingCompleted = false; @@ -325,7 +331,7 @@ function BufferController(config) { } function onPlaybackSeeked() { - setSeekStartTime(undefined); + seekTarget = NaN; } // Prune full buffer but what is around current time position @@ -405,17 +411,7 @@ function BufferController(config) { } function getWorkingTime() { - // This function returns current working time for buffer (either start time or current time if playback has started) - let ret = playbackController.getTime(); - - if (seekStartTime) { - // if there is a seek start time, the first buffer data will be available on maximum value between first buffer range value and seek start time. - let ranges = buffer.getAllBufferRanges(); - if (ranges && ranges.length) { - ret = Math.max(ranges.start(0), seekStartTime); - } - } - return ret; + return isNaN(seekTarget) ? playbackController.getTime() : seekTarget; } function onPlaybackProgression() { @@ -429,9 +425,7 @@ function BufferController(config) { } function onPlaybackPlaying() { - if (seekStartTime !== undefined) { - setSeekStartTime(undefined); - } + seekTarget = NaN; checkIfSufficientBuffer(); } @@ -762,10 +756,6 @@ function BufferController(config) { return type; } - function setSeekStartTime(value) { - seekStartTime = value; - } - function getBuffer() { return buffer; } @@ -846,6 +836,7 @@ function BufferController(config) { bufferLevel = 0; wallclockTicked = 0; pendingPruningRanges = []; + seekTarget = NaN; if (buffer) { if (!errored) { @@ -885,7 +876,6 @@ function BufferController(config) { createBuffer: createBuffer, dischargePreBuffer: dischargePreBuffer, getType: getType, - setSeekStartTime: setSeekStartTime, getBuffer: getBuffer, setBuffer: setBuffer, getBufferLevel: getBufferLevel, diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index dd4510a51c..66c379ee64 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -49,18 +49,16 @@ function PlaybackController() { adapter, videoModel, timelineConverter, - liveStartTime, + streamSwitch, + streamSeekTime, wallclockTimeIntervalId, - earliestTime, liveDelay, - bufferedRange, streamInfo, isDynamic, mediaPlayerModel, playOnceInitialized, lastLivePlaybackTime, availabilityStartTime, - compatibleWithPreviousStream, isLowLatencySeekingInProgress, playbackStalled, minPlaybackRateChange, @@ -73,14 +71,14 @@ function PlaybackController() { reset(); } - function initialize(StreamInfo, compatible) { + function initialize(StreamInfo, periodSwitch, seekTime) { streamInfo = StreamInfo; addAllListeners(); isDynamic = streamInfo.manifestInfo.isDynamic; isLowLatencySeekingInProgress = false; playbackStalled = false; - liveStartTime = streamInfo.start; - compatibleWithPreviousStream = compatible; + streamSwitch = periodSwitch === true; + streamSeekTime = seekTime; const ua = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''; @@ -88,11 +86,10 @@ function PlaybackController() { const isSafari = /safari/.test(ua) && !/chrome/.test(ua); minPlaybackRateChange = isSafari ? 0.25 : 0.02; + eventBus.on(Events.STREAM_INITIALIZED, onStreamInitialized, this); eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.on(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this); eventBus.on(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); eventBus.on(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); - eventBus.on(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this); eventBus.on(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); eventBus.on(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); eventBus.on(Events.PLAYBACK_ENDED, onPlaybackEnded, this); @@ -104,11 +101,27 @@ function PlaybackController() { } } - function onPeriodSwitchStarted(e) { - if (!isDynamic && e.fromStreamInfo && earliestTime[e.fromStreamInfo.id] !== undefined) { - delete bufferedRange[e.fromStreamInfo.id]; - delete earliestTime[e.fromStreamInfo.id]; + function onStreamInitialized(e) { + // Seamless period switch + if (streamSwitch && isNaN(streamSeekTime)) return; + + // Seek new stream in priority order: + // - at seek time (streamSeekTime) that required period switch + // - at start time provided in URI parameters + // - at stream/period start time (for static streams) or live start time (for dynamic streams) + let startTime = streamSeekTime; + if (isNaN(startTime)) { + startTime = isDynamic ? e.liveStartTime : e.streamInfo.start; + startTime = Math.max(startTime, getStartTimeFromUri()); + if (startTime === videoModel.getTime()) return; } + + // Trigger PLAYBACK_SEEKING event for controllers + eventBus.trigger(Events.PLAYBACK_SEEKING, { + seekTime: startTime + }); + // Seek video model + seek(startTime, false, true); } function getTimeToStreamEnd() { @@ -116,9 +129,7 @@ function PlaybackController() { } function getStreamEndTime() { - const startTime = getStreamStartTime(true); - const offset = isDynamic && streamInfo ? startTime - streamInfo.start : 0; - return startTime + (streamInfo ? streamInfo.duration - offset : offset); + return streamInfo.start + streamInfo.duration; } function play() { @@ -155,10 +166,6 @@ function PlaybackController() { } } else { eventBus.trigger(Events.PLAYBACK_SEEK_ASKED); - if (streamInfo) { - delete bufferedRange[streamInfo.id]; - delete earliestTime[streamInfo.id]; - } logger.info('Requesting seek to time: ' + time); videoModel.setCurrentTime(time, stickToBuffered); } @@ -209,19 +216,6 @@ function PlaybackController() { return streamController; } - function setLiveStartTime(value) { - if (liveStartTime !== streamInfo.start) { - // Consider only 1st live start time (set by video stream or audio if audio only) - return; - } - logger.info('Set live start time: ' + value); - liveStartTime = value; - } - - function getLiveStartTime() { - return liveStartTime; - } - /** * Computes the desirable delay for the live edge to avoid a risk of getting 404 when playing at the bleeding edge * @param {number} fragmentDuration - seconds? @@ -300,18 +294,16 @@ function PlaybackController() { } function reset() { - liveStartTime = NaN; playOnceInitialized = false; - earliestTime = {}; + streamSwitch = false; + streamSeekTime = NaN; liveDelay = 0; availabilityStartTime = 0; - bufferedRange = {}; if (videoModel) { + eventBus.off(Events.STREAM_INITIALIZED, onStreamInitialized, this); eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); eventBus.off(Events.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); - eventBus.off(Events.BYTES_APPENDED_END_FRAGMENT, onBytesAppended, this); eventBus.off(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); - eventBus.off(Events.PERIOD_SWITCH_STARTED, onPeriodSwitchStarted, this); eventBus.off(Events.PLAYBACK_PROGRESS, onPlaybackProgression, this); eventBus.off(Events.PLAYBACK_TIME_UPDATED, onPlaybackProgression, this); eventBus.off(Events.PLAYBACK_ENDED, onPlaybackEnded, this); @@ -369,51 +361,57 @@ function PlaybackController() { return uriParameters; } + function getStartTimeFromUri() { + const uriParameters = getStartTimeFromUriParameters(); + let start = !isNaN(uriParameters.fragS) ? uriParameters.fragS : (!isNaN(uriParameters.fragT) ? uriParameters.fragT : -1); + return start; + } + /** * @param {boolean} ignoreStartOffset - ignore URL fragment start offset if true * @param {number} liveEdge - liveEdge value * @returns {number} object * @memberof PlaybackController# */ - function getStreamStartTime(ignoreStartOffset, liveEdge) { - let presentationStartTime; - let startTimeOffset = NaN; - - if (!ignoreStartOffset) { - const uriParameters = getStartTimeFromUriParameters(); - if (uriParameters) { - startTimeOffset = !isNaN(uriParameters.fragS) ? uriParameters.fragS : uriParameters.fragT; - } else { - startTimeOffset = 0; - } - } else { - startTimeOffset = streamInfo ? streamInfo.start : startTimeOffset; - } - - if (isDynamic) { - if (!isNaN(startTimeOffset) && streamInfo) { - presentationStartTime = startTimeOffset - (streamInfo.manifestInfo.availableFrom.getTime() / 1000); - - if (presentationStartTime > liveStartTime || - presentationStartTime < (!isNaN(liveEdge) ? (liveEdge - streamInfo.manifestInfo.DVRWindowSize) : NaN)) { - presentationStartTime = null; - } - } - presentationStartTime = presentationStartTime || liveStartTime; - - } else { - if (streamInfo) { - if (!isNaN(startTimeOffset) && startTimeOffset < Math.max(streamInfo.manifestInfo.duration, streamInfo.duration) && startTimeOffset >= 0) { - presentationStartTime = startTimeOffset; - } else { - let currentEarliestTime = earliestTime[streamInfo.id]; //set by ready bufferStart after first onBytesAppended - presentationStartTime = currentEarliestTime !== undefined ? Math.max(currentEarliestTime.audio !== undefined ? currentEarliestTime.audio : 0, currentEarliestTime.video !== undefined ? currentEarliestTime.video : 0, streamInfo.start) : streamInfo.start; - } - } - } - - return presentationStartTime; - } + // function getStreamStartTime(ignoreStartOffset, liveEdge) { + // let presentationStartTime; + // let startTimeOffset = NaN; + + // if (!ignoreStartOffset) { + // const uriParameters = getStartTimeFromUriParameters(); + // if (uriParameters) { + // startTimeOffset = !isNaN(uriParameters.fragS) ? uriParameters.fragS : uriParameters.fragT; + // } else { + // startTimeOffset = 0; + // } + // } else { + // startTimeOffset = streamInfo ? streamInfo.start : startTimeOffset; + // } + + // if (isDynamic) { + // if (!isNaN(startTimeOffset) && streamInfo) { + // presentationStartTime = startTimeOffset - (streamInfo.manifestInfo.availableFrom.getTime() / 1000); + + // if (presentationStartTime > liveStartTime || + // presentationStartTime < (!isNaN(liveEdge) ? (liveEdge - streamInfo.manifestInfo.DVRWindowSize) : NaN)) { + // presentationStartTime = null; + // } + // } + // presentationStartTime = presentationStartTime || liveStartTime; + + // } else { + // if (streamInfo) { + // if (!isNaN(startTimeOffset) && startTimeOffset < Math.max(streamInfo.manifestInfo.duration, streamInfo.duration) && startTimeOffset >= 0) { + // presentationStartTime = startTimeOffset; + // } else { + // let currentEarliestTime = earliestTime[streamInfo.id]; //set by ready bufferStart after first onBytesAppended + // presentationStartTime = currentEarliestTime !== undefined ? Math.max(currentEarliestTime.audio !== undefined ? currentEarliestTime.audio : 0, currentEarliestTime.video !== undefined ? currentEarliestTime.video : 0, streamInfo.start) : streamInfo.start; + // } + // } + // } + + // return presentationStartTime; + // } function getActualPresentationTime(currentTime) { const DVRMetrics = dashMetrics.getCurrentDVRInfo(); @@ -602,16 +600,16 @@ function PlaybackController() { } } - function checkTimeInRanges(time, ranges) { - if (ranges && ranges.length > 0) { - for (let i = 0, len = ranges.length; i < len; i++) { - if (time >= ranges.start(i) && time < ranges.end(i)) { - return true; - } - } - } - return false; - } + // function checkTimeInRanges(time, ranges) { + // if (ranges && ranges.length > 0) { + // for (let i = 0, len = ranges.length; i < len; i++) { + // if (time >= ranges.start(i) && time < ranges.end(i)) { + // return true; + // } + // } + // } + // return false; + // } function onPlaybackProgression() { if ( @@ -692,72 +690,6 @@ function PlaybackController() { } } - function onBytesAppended(e) { - let commonEarliestTime, - initialStartTime; - let ranges = e.bufferedRanges; - if (!ranges || !ranges.length) return; - if (earliestTime[streamInfo.id] && earliestTime[streamInfo.id].started === true) { - //stream has already been started. - return; - } - - const type = e.mediaType; - - if (bufferedRange[streamInfo.id] === undefined) { - bufferedRange[streamInfo.id] = []; - } - - bufferedRange[streamInfo.id][type] = ranges; - - if (earliestTime[streamInfo.id] === undefined) { - earliestTime[streamInfo.id] = []; - earliestTime[streamInfo.id].started = false; - } - - if (earliestTime[streamInfo.id][type] === undefined) { - earliestTime[streamInfo.id][type] = Math.max(ranges.start(0), streamInfo.start); - } - - initialStartTime = getStreamStartTime(false); - if (streamController.hasVideoTrack() && streamController.hasAudioTrack()) { - //current stream has audio and video contents - if (!isNaN(earliestTime[streamInfo.id].audio) && !isNaN(earliestTime[streamInfo.id].video)) { - - if (earliestTime[streamInfo.id].audio < earliestTime[streamInfo.id].video) { - // common earliest is video time - // check buffered audio range has video time, if ok, we seek, otherwise, we wait some other data - commonEarliestTime = earliestTime[streamInfo.id].video > initialStartTime ? earliestTime[streamInfo.id].video : initialStartTime; - ranges = bufferedRange[streamInfo.id].audio; - } else { - // common earliest is audio time - // check buffered video range has audio time, if ok, we seek, otherwise, we wait some other data - commonEarliestTime = earliestTime[streamInfo.id].audio > initialStartTime ? earliestTime[streamInfo.id].audio : initialStartTime; - ranges = bufferedRange[streamInfo.id].video; - } - if (checkTimeInRanges(commonEarliestTime, ranges)) { - if (!(checkTimeInRanges(getNormalizedTime(), bufferedRange[streamInfo.id].audio) && checkTimeInRanges(getNormalizedTime(), bufferedRange[streamInfo.id].video))) { - if (!compatibleWithPreviousStream && commonEarliestTime !== 0) { - seek(commonEarliestTime, true, true); - } - } - earliestTime[streamInfo.id].started = true; - } - } - } else { - //current stream has only audio or only video content - if (earliestTime[streamInfo.id][type]) { - commonEarliestTime = earliestTime[streamInfo.id][type] > initialStartTime ? earliestTime[streamInfo.id][type] : initialStartTime; - if (!checkTimeInRanges(getNormalizedTime(), bufferedRange[streamInfo.id][type])) { - if (!compatibleWithPreviousStream) { - seek(commonEarliestTime, false, true); - } - } - earliestTime[streamInfo.id].started = true; - } - } - } - function onFragmentLoadProgress(e) { // If using fetch and stream mode is not available, readjust live latency so it is 20% higher than segment duration if (e.stream === false && settings.get().streaming.lowLatencyEnabled && !isNaN(e.request.duration)) { @@ -874,7 +806,7 @@ function PlaybackController() { initialize: initialize, setConfig: setConfig, getStartTimeFromUriParameters: getStartTimeFromUriParameters, - getStreamStartTime: getStreamStartTime, + // getStreamStartTime: getStreamStartTime, getTimeToStreamEnd: getTimeToStreamEnd, getTime: getTime, getNormalizedTime: getNormalizedTime, @@ -883,8 +815,6 @@ function PlaybackController() { getEnded: getEnded, getIsDynamic: getIsDynamic, getStreamController: getStreamController, - setLiveStartTime: setLiveStartTime, - getLiveStartTime: getLiveStartTime, computeAndSetLiveDelay: computeAndSetLiveDelay, getLiveDelay: getLiveDelay, setLiveDelay: setLiveDelay, diff --git a/src/streaming/controllers/ScheduleController.js b/src/streaming/controllers/ScheduleController.js index 21d8619c61..5fd3eded20 100644 --- a/src/streaming/controllers/ScheduleController.js +++ b/src/streaming/controllers/ScheduleController.js @@ -411,7 +411,7 @@ function ScheduleController(config) { function onDataUpdateStarted(e) { if (e.sender.getType() !== type || e.sender.getStreamId() !== streamId) return; - stop(); + // stop(); } function onBufferingCompleted(e) { diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index bb02303df9..26c9c7e23c 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -95,7 +95,6 @@ function StreamController() { prefetchTimerId, wallclockTicked, buffers, - useSmoothPeriodTransition, preloading, lastPlaybackTime, supportsChangeType, @@ -271,7 +270,7 @@ function StreamController() { if (seekingStream && (seekingStream !== activeStream || (preloading && !activeStream.isActive()))) { // If we're preloading other stream, the active one was deactivated and we need to switch back flushPlaylistMetrics(PlayListTrace.END_OF_PERIOD_STOP_REASON); - switchStream(activeStream, seekingStream, e.seekTime); + switchStream(seekingStream, activeStream, e.seekTime); } else { flushPlaylistMetrics(PlayListTrace.USER_REQUEST_STOP_REASON); } @@ -366,10 +365,10 @@ function StreamController() { // Smooth period transition allowed only if both of these conditions are true: // 1.- None of the periods uses contentProtection. // 2.- changeType method implemented by browser or periods use the same codec. - useSmoothPeriodTransition = activeStream.isProtectionCompatible(newStream) && + let smoothPeriodSwitch = activeStream.isProtectionCompatible(newStream) && (supportsChangeType || activeStream.isMediaCodecCompatible(newStream)); - if (useSmoothPeriodTransition) { + if (smoothPeriodSwitch) { logger.info('[onStreamCanLoadNext] Preloading next stream'); activeStream.deactivate(true); newStream.preload(mediaSource, buffers); @@ -446,7 +445,7 @@ function StreamController() { function onEnded() { const nextStream = getNextStream(); if (nextStream) { - switchStream(activeStream, nextStream, NaN); + switchStream(nextStream, activeStream, NaN); } else { logger.debug('StreamController no next stream found'); } @@ -466,46 +465,49 @@ function StreamController() { } } - function switchStream(oldStream, newStream, seekTime) { - if (isStreamSwitchingInProgress || !newStream || (oldStream === newStream && newStream.isActive())) return; + function switchStream(stream, previousStream, seekTime) { + logger.info('Switch stream to ' + stream.getId() + ' at t=' + seekTime); + + if (isStreamSwitchingInProgress || !stream || (previousStream === stream && stream.isActive())) return; isStreamSwitchingInProgress = true; eventBus.trigger(Events.PERIOD_SWITCH_STARTED, { - fromStreamInfo: oldStream ? oldStream.getStreamInfo() : null, - toStreamInfo: newStream.getStreamInfo() + fromStreamInfo: previousStream ? previousStream.getStreamInfo() : null, + toStreamInfo: stream.getStreamInfo() }); - useSmoothPeriodTransition = false; - if (oldStream) { - useSmoothPeriodTransition = (activeStream.isProtectionCompatible(newStream) && - (supportsChangeType || activeStream.isMediaCodecCompatible(newStream))) && - !seekTime || newStream.getPreloaded(); - oldStream.deactivate(useSmoothPeriodTransition); + let seamlessPeriodSwitch = false; + if (previousStream) { + seamlessPeriodSwitch = (activeStream.isProtectionCompatible(stream) && + (supportsChangeType || activeStream.isMediaCodecCompatible(stream))) && + !seekTime || stream.getPreloaded(); + previousStream.deactivate(seamlessPeriodSwitch); } - activeStream = newStream; + // Determine seek time when switching to new period + // - seek at given seek time + // - or seek at period start if seamless period switching + seekTime = !isNaN(seekTime) ? seekTime : (!seamlessPeriodSwitch && previousStream ? stream.getStreamInfo().start : NaN); + + activeStream = stream; preloading = false; - playbackController.initialize(getActiveStreamInfo(), useSmoothPeriodTransition); + playbackController.initialize(getActiveStreamInfo(), !!previousStream, seekTime); if (videoModel.getElement()) { //TODO detect if we should close jump to activateStream. - openMediaSource(seekTime, oldStream, false, useSmoothPeriodTransition); + openMediaSource(seekTime, (previousStream === null), false, seamlessPeriodSwitch); } else { - preloadStream(seekTime); + activateStream(seekTime, seamlessPeriodSwitch); } } - function preloadStream(seekTime) { - activateStream(seekTime, useSmoothPeriodTransition); - } - function switchToVideoElement(seekTime) { if (activeStream) { playbackController.initialize(getActiveStreamInfo()); - openMediaSource(seekTime, null, true, false); + openMediaSource(seekTime, false, true, false); } } - function openMediaSource(seekTime, oldStream, streamActivated, keepBuffers) { + function openMediaSource(seekTime, sourceInitialized, streamActivated, keepBuffers) { let sourceUrl; function onMediaSourceOpen() { @@ -518,7 +520,7 @@ function StreamController() { mediaSource.removeEventListener('webkitsourceopen', onMediaSourceOpen); setMediaDuration(); - if (!oldStream) { + if (!sourceInitialized) { eventBus.trigger(Events.SOURCE_INITIALIZED); } @@ -538,7 +540,7 @@ function StreamController() { } else { if (keepBuffers) { activateStream(seekTime, keepBuffers); - if (!oldStream) { + if (!sourceInitialized) { eventBus.trigger(Events.SOURCE_INITIALIZED); } } else { @@ -567,12 +569,12 @@ function StreamController() { if (!isNaN(seekTime)) { playbackController.seek(seekTime); //we only need to call seek here, IndexHandlerTime was set from seeking event } else { - let startTime = playbackController.getStreamStartTime(true); - if (!keepBuffers) { - getActiveStreamProcessors().forEach(p => { - p.setIndexHandlerTime(startTime); - }); - } + // let startTime = playbackController.getStreamStartTime(true); + // if (!keepBuffers) { + // getActiveStreamProcessors().forEach(p => { + // p.setIndexHandlerTime(startTime); + // }); + // } } } @@ -669,7 +671,7 @@ function StreamController() { logger.debug('Dynamic stream: Trying to find the correct starting period'); initialStream = getInitialStream(); } - switchStream(null, initialStream !== null ? initialStream : streams[0], NaN); + switchStream(initialStream !== null ? initialStream : streams[0], null, NaN); } eventBus.trigger(Events.STREAMS_COMPOSED); diff --git a/src/streaming/text/NotFragmentedTextBufferController.js b/src/streaming/text/NotFragmentedTextBufferController.js index f7079002ff..a7cbeeb858 100644 --- a/src/streaming/text/NotFragmentedTextBufferController.js +++ b/src/streaming/text/NotFragmentedTextBufferController.js @@ -125,9 +125,6 @@ function NotFragmentedTextBufferController(config) { function dischargePreBuffer() { } - function setSeekStartTime() { //Unused - TODO Remove need for stub function - } - function getBufferLevel() { return 0; } @@ -199,7 +196,6 @@ function NotFragmentedTextBufferController(config) { initialize: initialize, createBuffer: createBuffer, getType: getType, - setSeekStartTime: setSeekStartTime, getBuffer: getBuffer, getBufferLevel: getBufferLevel, setMediaSource: setMediaSource, diff --git a/src/streaming/text/TextBufferController.js b/src/streaming/text/TextBufferController.js index 084f1a9165..427354458a 100644 --- a/src/streaming/text/TextBufferController.js +++ b/src/streaming/text/TextBufferController.js @@ -107,10 +107,6 @@ function TextBufferController(config) { _BufferControllerImpl.setMediaSource(value); } - function setSeekStartTime(value) { - _BufferControllerImpl.setSeekStartTime(value); - } - function getBufferLevel() { return _BufferControllerImpl.getBufferLevel(); } @@ -151,7 +147,6 @@ function TextBufferController(config) { initialize: initialize, createBuffer: createBuffer, getType: getType, - setSeekStartTime: setSeekStartTime, getBuffer: getBuffer, setBuffer: setBuffer, getBufferLevel: getBufferLevel, diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index 0048f3769a..b3696c84fd 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -56,7 +56,6 @@ describe('PlaybackController', function () { it('should initialize', function () { expect(playbackController.getIsDynamic()).to.not.exist; // jshint ignore:line - expect(playbackController.getLiveStartTime()).to.be.NaN; // jshint ignore:line expect(playbackController.isPaused()).to.be.null; // jshint ignore:line expect(playbackController.isSeeking()).to.be.null; // jshint ignore:line expect(playbackController.getTime()).to.be.null; // jshint ignore:line @@ -74,9 +73,7 @@ describe('PlaybackController', function () { playbackController.initialize(streamInfo); - expect(playbackController.getIsDynamic()).to.equal(true); - expect(playbackController.getLiveStartTime()).to.equal(10); }); }); From 32f8f71f17682ab3ca2f8bf51b6d9a90c6b622c4 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Wed, 24 Jun 2020 15:17:16 +0200 Subject: [PATCH 02/14] revert gap/discontinuity limit for buffer range tolerance (to do in another PR) --- src/streaming/controllers/BufferController.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index bbc2f1512c..871fecbb32 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -482,11 +482,6 @@ function BufferController(config) { let range, length; - // Consider gap/discontinuity limit as tolerance - if (settings.get().streaming.jumpGaps) { - tolerance = settings.get().streaming.smallGapLimit; - } - range = getRangeAt(time, tolerance); if (range === null) { From 95703aaff06136b58535d989f90bffaa6a13cb93 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Wed, 24 Jun 2020 15:20:25 +0200 Subject: [PATCH 03/14] update code comments --- src/streaming/controllers/StreamController.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 26c9c7e23c..252038e8fa 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -362,13 +362,13 @@ function StreamController() { if (mediaSource && !isLast) { const newStream = getNextStream(); - // Smooth period transition allowed only if both of these conditions are true: - // 1.- None of the periods uses contentProtection. - // 2.- changeType method implemented by browser or periods use the same codec. - let smoothPeriodSwitch = activeStream.isProtectionCompatible(newStream) && + // Seamless period switch allowed only if: + // - none of the periods uses contentProtection. + // - AND changeType method implemented by browser or periods use the same codec. + let seamlessPeriodSwitch = activeStream.isProtectionCompatible(newStream) && (supportsChangeType || activeStream.isMediaCodecCompatible(newStream)); - if (smoothPeriodSwitch) { + if (seamlessPeriodSwitch) { logger.info('[onStreamCanLoadNext] Preloading next stream'); activeStream.deactivate(true); newStream.preload(mediaSource, buffers); From 195a7fff5c86be3885f840990cfa2d9149bfaed4 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Wed, 24 Jun 2020 15:47:26 +0200 Subject: [PATCH 04/14] BufferController: optimise effective range seeking if seek target --- src/streaming/controllers/BufferController.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index 871fecbb32..ebd3bc4a26 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -283,8 +283,10 @@ function BufferController(config) { onPlaybackProgression(); // If seeking, seek video model to range start in case appended segment starts beyond seek target - if (!isNaN(seekTarget) && (seekTarget < ranges.start(0) || playbackController.getTime() === 0)) { + if (!isNaN(seekTarget) && + (playbackController.getTime() === 0 || playbackController.getTime() < ranges.start(0))) { playbackController.seek(ranges.start(0), true, true); + seekTarget = NaN; } } else { if (replacingBuffer) { From 5099ceb949742610299616c9508ebfa0b907d800 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Thu, 25 Jun 2020 09:44:14 +0200 Subject: [PATCH 05/14] avoid seeking at NaN --- src/streaming/controllers/PlaybackController.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 66c379ee64..31b1f2aa5a 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -116,12 +116,14 @@ function PlaybackController() { if (startTime === videoModel.getTime()) return; } - // Trigger PLAYBACK_SEEKING event for controllers - eventBus.trigger(Events.PLAYBACK_SEEKING, { - seekTime: startTime - }); - // Seek video model - seek(startTime, false, true); + if (!isNaN(startTime)) { + // Trigger PLAYBACK_SEEKING event for controllers + eventBus.trigger(Events.PLAYBACK_SEEKING, { + seekTime: startTime + }); + // Seek video model + seek(startTime, false, true); + } } function getTimeToStreamEnd() { From 2a52e09950e324198523538234d5911f0553d382 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Thu, 25 Jun 2020 10:44:55 +0200 Subject: [PATCH 06/14] remove commented code --- src/streaming/StreamProcessor.js | 37 --------------- .../controllers/PlaybackController.js | 47 ------------------- 2 files changed, 84 deletions(-) diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index 0f8fd1844f..75317c1a57 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -628,43 +628,6 @@ function StreamProcessor(config) { return liveStartTime; } - // function setLiveEdgeSeekTarget() { - // if (!liveEdgeFinder) return; - - // const currentRepresentationInfo = getRepresentationInfo(); - // const liveEdge = liveEdgeFinder.getLiveEdge(currentRepresentationInfo); - // const request = findRequestForLiveEdge(liveEdge, currentRepresentationInfo); - - // if (request) { - // // When low latency mode is selected but browser doesn't support fetch - // // start at the beginning of the segment to avoid consuming the whole buffer - // if (settings.get().streaming.lowLatencyEnabled) { - // const liveStartTime = request.duration < mediaPlayerModel.getLiveDelay() ? request.startTime : request.startTime + request.duration - mediaPlayerModel.getLiveDelay(); - // playbackController.setLiveStartTime(liveStartTime); - // } else { - // playbackController.setLiveStartTime(request.startTime); - // } - // } - - // const seekTarget = playbackController.getStreamStartTime(false, liveEdge); - // bufferController.setSeekStartTime(seekTarget); - // scheduleController.setCurrentRepresentation(currentRepresentationInfo); - // scheduleController.setSeekTarget(seekTarget); - // scheduleController.start(); - - // // For multi periods stream, if the startTime is beyond current period then seek to corresponding period (see StreamController::onPlaybackSeeking) - // if (seekTarget > (currentRepresentationInfo.mediaInfo.streamInfo.start + currentRepresentationInfo.mediaInfo.streamInfo.duration)) { - // playbackController.seek(seekTarget); - // } - - // dashMetrics.updateManifestUpdateInfo({ - // currentTime: seekTarget, - // presentationStartTime: liveEdge, - // latency: liveEdge - seekTarget, - // clientTimeOffset: timelineConverter.getClientTimeOffset() - // }); - // } - function findRequestForLiveEdge(liveEdge, currentRepresentationInfo) { try { let request = null; diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 31b1f2aa5a..42736b742f 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -369,52 +369,6 @@ function PlaybackController() { return start; } - /** - * @param {boolean} ignoreStartOffset - ignore URL fragment start offset if true - * @param {number} liveEdge - liveEdge value - * @returns {number} object - * @memberof PlaybackController# - */ - // function getStreamStartTime(ignoreStartOffset, liveEdge) { - // let presentationStartTime; - // let startTimeOffset = NaN; - - // if (!ignoreStartOffset) { - // const uriParameters = getStartTimeFromUriParameters(); - // if (uriParameters) { - // startTimeOffset = !isNaN(uriParameters.fragS) ? uriParameters.fragS : uriParameters.fragT; - // } else { - // startTimeOffset = 0; - // } - // } else { - // startTimeOffset = streamInfo ? streamInfo.start : startTimeOffset; - // } - - // if (isDynamic) { - // if (!isNaN(startTimeOffset) && streamInfo) { - // presentationStartTime = startTimeOffset - (streamInfo.manifestInfo.availableFrom.getTime() / 1000); - - // if (presentationStartTime > liveStartTime || - // presentationStartTime < (!isNaN(liveEdge) ? (liveEdge - streamInfo.manifestInfo.DVRWindowSize) : NaN)) { - // presentationStartTime = null; - // } - // } - // presentationStartTime = presentationStartTime || liveStartTime; - - // } else { - // if (streamInfo) { - // if (!isNaN(startTimeOffset) && startTimeOffset < Math.max(streamInfo.manifestInfo.duration, streamInfo.duration) && startTimeOffset >= 0) { - // presentationStartTime = startTimeOffset; - // } else { - // let currentEarliestTime = earliestTime[streamInfo.id]; //set by ready bufferStart after first onBytesAppended - // presentationStartTime = currentEarliestTime !== undefined ? Math.max(currentEarliestTime.audio !== undefined ? currentEarliestTime.audio : 0, currentEarliestTime.video !== undefined ? currentEarliestTime.video : 0, streamInfo.start) : streamInfo.start; - // } - // } - // } - - // return presentationStartTime; - // } - function getActualPresentationTime(currentTime) { const DVRMetrics = dashMetrics.getCurrentDVRInfo(); const DVRWindow = DVRMetrics ? DVRMetrics.range : null; @@ -808,7 +762,6 @@ function PlaybackController() { initialize: initialize, setConfig: setConfig, getStartTimeFromUriParameters: getStartTimeFromUriParameters, - // getStreamStartTime: getStreamStartTime, getTimeToStreamEnd: getTimeToStreamEnd, getTime: getTime, getNormalizedTime: getNormalizedTime, From 35d0dda75027eb596d0f660354bc84a7ffad940c Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Thu, 25 Jun 2020 10:46:33 +0200 Subject: [PATCH 07/14] remove commented code --- src/streaming/controllers/PlaybackController.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index 42736b742f..fa2a8b3b35 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -556,17 +556,6 @@ function PlaybackController() { } } - // function checkTimeInRanges(time, ranges) { - // if (ranges && ranges.length > 0) { - // for (let i = 0, len = ranges.length; i < len; i++) { - // if (time >= ranges.start(i) && time < ranges.end(i)) { - // return true; - // } - // } - // } - // return false; - // } - function onPlaybackProgression() { if ( isDynamic && From 6b772c4229e3ae4210ef80a4410791fdfce32ac3 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Mon, 29 Jun 2020 15:26:13 +0200 Subject: [PATCH 08/14] Add processing of MPD time anchor (#t) according to Annex C of MPEG DASH spec --- .../controllers/PlaybackController.js | 59 +++++--- src/streaming/controllers/StreamController.js | 7 +- ...reaming.controllers.PlaybackControllers.js | 129 ++++++++++++++++-- 3 files changed, 164 insertions(+), 31 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index fa2a8b3b35..cba61e391b 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -106,13 +106,36 @@ function PlaybackController() { if (streamSwitch && isNaN(streamSeekTime)) return; // Seek new stream in priority order: - // - at seek time (streamSeekTime) that required period switch + // - at seek time (streamSeekTime) when switching period // - at start time provided in URI parameters // - at stream/period start time (for static streams) or live start time (for dynamic streams) let startTime = streamSeekTime; if (isNaN(startTime)) { - startTime = isDynamic ? e.liveStartTime : e.streamInfo.start; - startTime = Math.max(startTime, getStartTimeFromUri()); + if (isDynamic) { + // For dynamic stream, start by default at (live edge - live delay) + startTime = e.liveStartTime; + // If start time in URI, take min value between live edge time and time from URI (if in DVR window range) + const dvrInfo = dashMetrics.getCurrentDVRInfo(); + const dvrWindow = dvrInfo ? dvrInfo.range : null; + if (dvrWindow) { + const startTimeFromUri = getStartTimeFromUriParameters(dvrWindow.start); + if (!isNaN(startTimeFromUri) && startTimeFromUri >= dvrWindow.start) { + logger.info('Start time from URI parameters: ' + startTimeFromUri); + startTime = Math.min(startTime, startTimeFromUri); + } + } + } else { + // For static stream, start by default at period start + startTime = streamInfo.start; + // If start time in URI, take max value between period start and time from URI (if in period range) + const startTimeFromUri = getStartTimeFromUriParameters(streamInfo.start); + if (!isNaN(startTimeFromUri) && startTimeFromUri < (startTime + streamInfo.duration)) { + logger.info('Start time from URI parameters: ' + startTimeFromUri); + startTime = Math.max(startTime, startTimeFromUri); + } + } + + // Check if not seeking at current time if (startTime === videoModel.getTime()) return; } @@ -348,25 +371,23 @@ function PlaybackController() { } } - function getStartTimeFromUriParameters() { + function getStartTimeFromUriParameters(periodStart) { const fragData = uriFragmentModel.getURIFragmentData(); - let uriParameters; - if (fragData) { - uriParameters = {}; - const r = parseInt(fragData.r, 10); - if (r >= 0 && streamInfo && r < streamInfo.manifestInfo.DVRWindowSize && fragData.t === null) { - fragData.t = Math.max(Math.floor(Date.now() / 1000) - streamInfo.manifestInfo.DVRWindowSize, (streamInfo.manifestInfo.availableFrom.getTime() / 1000) + streamInfo.start) + r; - } - uriParameters.fragS = parseFloat(fragData.s); - uriParameters.fragT = parseFloat(fragData.t); + if (!fragData || !fragData.t) { + return NaN; } - return uriParameters; - } - function getStartTimeFromUri() { - const uriParameters = getStartTimeFromUriParameters(); - let start = !isNaN(uriParameters.fragS) ? uriParameters.fragS : (!isNaN(uriParameters.fragT) ? uriParameters.fragT : -1); - return start; + let startTime = NaN; + + // Consider only start time of MediaRange + // TODO: consider end time of MediaRange to stop playback at provided end time + fragData.t = fragData.t.split(',')[0]; + + // t is relative to period start + // posix notation (t=posix:xxx) = absolute time range as number of seconds since 01-01-1970 + startTime = (fragData.t.indexOf('posix:') !== -1) ? parseInt(fragData.t.substring(6)) : (periodStart + parseInt(fragData.t)); + + return startTime; } function getActualPresentationTime(currentTime) { diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 252038e8fa..610cd8448c 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -660,11 +660,10 @@ function StreamController() { } // we need to figure out what the correct starting period is - const startTimeFormUriParameters = playbackController.getStartTimeFromUriParameters(); let initialStream = null; - if (startTimeFormUriParameters) { - const initialTime = !isNaN(startTimeFormUriParameters.fragS) ? startTimeFormUriParameters.fragS : startTimeFormUriParameters.fragT; - initialStream = getStreamForTime(initialTime); + const startTimeFormUri = playbackController.getStartTimeFromUriParameters(streamsInfo[0].start); + if (!isNaN(startTimeFormUri)) { + initialStream = getStreamForTime(startTimeFormUri); } // For multiperiod streams we should avoid a switch of streams after the seek to the live edge. So we do a calculation of the expected seek time to find the right stream object. if (!initialStream && adapter.getIsDynamic() && streams.length) { diff --git a/test/unit/streaming.controllers.PlaybackControllers.js b/test/unit/streaming.controllers.PlaybackControllers.js index b3696c84fd..e143bf84be 100644 --- a/test/unit/streaming.controllers.PlaybackControllers.js +++ b/test/unit/streaming.controllers.PlaybackControllers.js @@ -85,7 +85,8 @@ describe('PlaybackController', function () { isDynamic: true, availableFrom: new Date() }, - start: 10 + start: 10, + duration: 600 }; playbackController.initialize(streamInfo); @@ -160,13 +161,6 @@ describe('PlaybackController', function () { videoModelMock.ended = true; expect(playbackController.getEnded()).to.equal(videoModelMock.ended); }); - - it('getStartTimeFromUriParameters should return the expected value', function () { - uriFragmentModelMock.setURIFragmentData({t: 18.2}); - const uriParameters = playbackController.getStartTimeFromUriParameters(); - expect(uriParameters.fragT).to.exist; // jshint ignore:line - expect(uriParameters.fragT).to.equal(18.2); - }); }); describe('video event handler', function () { @@ -325,6 +319,125 @@ describe('PlaybackController', function () { videoModelMock.fireEvent('waiting'); }); }); + }); + + describe('start time', function () { + let expectedSeekTime; + let doneFn; + + let staticStreamInfo = { manifestInfo: { isDynamic: false }, start: 10, duration: 600 }; + let dynamicStreamInfo = { manifestInfo: { isDynamic: true }, start: 0, duration: Infinity }; + let dvrWindowRange = { start: 10, end: 90 }; + + let onPlaybackSeeking = function (e) { + eventBus.off(Events.PLAYBACK_SEEKING, onPlaybackSeeking); + expect(e.seekTime).to.equal(expectedSeekTime); + doneFn(); + }; + + beforeEach(function () { + videoModelMock.time = -1; + eventBus.on(Events.PLAYBACK_SEEKING, onPlaybackSeeking, this); + }); + + it('should start static stream at period start', function (done) { + doneFn = done; + + expectedSeekTime = staticStreamInfo.start; + + playbackController.initialize(staticStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {}); + }); + + it('should start static stream at #t', function (done) { + doneFn = done; + + let uriStartTime = 10; + uriFragmentModelMock.setURIFragmentData({t: uriStartTime.toString()}); + + expectedSeekTime = staticStreamInfo.start + uriStartTime; + + playbackController.initialize(staticStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {}); + }); + + it('should start static stream at period start if #t is beyond period end', function (done) { + doneFn = done; + + let uriStartTime = 700; + uriFragmentModelMock.setURIFragmentData({t: uriStartTime.toString()}); + + expectedSeekTime = staticStreamInfo.start; + + playbackController.initialize(staticStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {}); + }); + it('should start dynamic stream at live start time', function (done) { + doneFn = done; + + let liveStartTime = 90; + + expectedSeekTime = liveStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at #t', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 50; + uriFragmentModelMock.setURIFragmentData({t: uriStartTime.toString()}); + + expectedSeekTime = dvrWindowRange.start + uriStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at #t=posix:', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 50; + uriFragmentModelMock.setURIFragmentData({t: 'posix:' + uriStartTime.toString()}); + + expectedSeekTime = uriStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at live start time if #t=posix is before DVR window range', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 0; + uriFragmentModelMock.setURIFragmentData({t: 'posix:' + uriStartTime.toString()}); + + expectedSeekTime = liveStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); + + it('should start dynamic stream at live start time if #t=posix is beyond live start time', function (done) { + doneFn = done; + + let liveStartTime = 70; + dashMetricsMock.addDVRInfo('video', Date.now(), null, dvrWindowRange); + let uriStartTime = 80; + uriFragmentModelMock.setURIFragmentData({t: 'posix:' + uriStartTime.toString()}); + + expectedSeekTime = liveStartTime; + + playbackController.initialize(dynamicStreamInfo); + eventBus.trigger(Events.STREAM_INITIALIZED, {liveStartTime: liveStartTime}); + }); }); }); From 6689278f59391eaf44433ee90f6ed99939cf32e6 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Mon, 29 Jun 2020 16:19:44 +0200 Subject: [PATCH 09/14] StreamProcessor: update DVR info metrics also if SEGMENTS_UPDATE_FAILED_ERROR_CODE --- src/streaming/StreamProcessor.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index 75317c1a57..2561e87db8 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -233,7 +233,9 @@ function StreamProcessor(config) { if (!e.error) { scheduleController.setCurrentRepresentation(adapter.convertDataToRepresentationInfo(e.currentRepresentation)); - } else if (e.error.code !== Errors.SEGMENTS_UPDATE_FAILED_ERROR_CODE) { + } + if (!e.error || e.error.code === Errors.SEGMENTS_UPDATE_FAILED_ERROR_CODE) { + // Update has been postponed, we nevertheless update DVR info addDVRMetric(); } } From 4ea359fe47c19e6bace4040c47faf5c49d769fb7 Mon Sep 17 00:00:00 2001 From: Bertrand Berthelot Date: Mon, 29 Jun 2020 17:45:55 +0200 Subject: [PATCH 10/14] rename getStartTimeFromUriParameters() parameter to 'rangeStart' --- src/streaming/controllers/PlaybackController.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index cba61e391b..ebcdb388d9 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -371,7 +371,7 @@ function PlaybackController() { } } - function getStartTimeFromUriParameters(periodStart) { + function getStartTimeFromUriParameters(rangeStart) { const fragData = uriFragmentModel.getURIFragmentData(); if (!fragData || !fragData.t) { return NaN; @@ -380,12 +380,12 @@ function PlaybackController() { let startTime = NaN; // Consider only start time of MediaRange - // TODO: consider end time of MediaRange to stop playback at provided end time + // TODO: consider end time of MediaRange to stop playback at provided end time fragData.t = fragData.t.split(',')[0]; - // t is relative to period start - // posix notation (t=posix:xxx) = absolute time range as number of seconds since 01-01-1970 - startTime = (fragData.t.indexOf('posix:') !== -1) ? parseInt(fragData.t.substring(6)) : (periodStart + parseInt(fragData.t)); + // "t=