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..ea36bd25ed 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,31 +226,17 @@ 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) { + // Update representation if no 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, update nevertheless DVR info addDVRMetric(); } } @@ -624,9 +608,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,30 +622,13 @@ 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(); - - // 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() - }); + return liveStartTime; } function findRequestForLiveEdge(liveEdge, currentRepresentationInfo) { @@ -763,6 +733,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..ebd3bc4a26 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,13 @@ 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) && + (playbackController.getTime() === 0 || playbackController.getTime() < ranges.start(0))) { + playbackController.seek(ranges.start(0), true, true); + seekTarget = NaN; + } } else { if (replacingBuffer) { const currentTime = playbackController.getTime(); @@ -309,7 +316,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 +333,7 @@ function BufferController(config) { } function onPlaybackSeeked() { - setSeekStartTime(undefined); + seekTarget = NaN; } // Prune full buffer but what is around current time position @@ -405,17 +413,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 +427,7 @@ function BufferController(config) { } function onPlaybackPlaying() { - if (seekStartTime !== undefined) { - setSeekStartTime(undefined); - } + seekTarget = NaN; checkIfSufficientBuffer(); } @@ -488,11 +484,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) { @@ -762,10 +753,6 @@ function BufferController(config) { return type; } - function setSeekStartTime(value) { - seekStartTime = value; - } - function getBuffer() { return buffer; } @@ -846,6 +833,7 @@ function BufferController(config) { bufferLevel = 0; wallclockTicked = 0; pendingPruningRanges = []; + seekTarget = NaN; if (buffer) { if (!errored) { @@ -885,7 +873,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..97e59bdc3d 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,10 +101,52 @@ 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) 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)) { + 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 (capped by DVR window range) + const dvrInfo = dashMetrics.getCurrentDVRInfo(); + const dvrWindow = dvrInfo ? dvrInfo.range : null; + if (dvrWindow) { + // #t shall be relative to period start + const startTimeFromUri = getStartTimeFromUriParameters(streamInfo.start, true); + if (!isNaN(startTimeFromUri)) { + logger.info('Start time from URI parameters: ' + startTimeFromUri); + startTime = Math.max(Math.min(startTime, startTimeFromUri), dvrWindow.start); + } + } + } 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, false); + 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; + } + + if (!isNaN(startTime)) { + // Trigger PLAYBACK_SEEKING event for controllers + eventBus.trigger(Events.PLAYBACK_SEEKING, { + seekTime: startTime + }); + // Seek video model + seek(startTime, false, true); } } @@ -116,9 +155,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 +192,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 +242,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 +320,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); @@ -354,65 +372,23 @@ function PlaybackController() { } } - function getStartTimeFromUriParameters() { + function getStartTimeFromUriParameters(rangeStart, isDynamic) { 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); - } - return uriParameters; - } - - /** - * @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 (!fragData || !fragData.t) { + return NaN; } - if (isDynamic) { - if (!isNaN(startTimeOffset) && streamInfo) { - presentationStartTime = startTimeOffset - (streamInfo.manifestInfo.availableFrom.getTime() / 1000); + let startTime = NaN; - if (presentationStartTime > liveStartTime || - presentationStartTime < (!isNaN(liveEdge) ? (liveEdge - streamInfo.manifestInfo.DVRWindowSize) : NaN)) { - presentationStartTime = null; - } - } - presentationStartTime = presentationStartTime || liveStartTime; + // 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]; - } 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; - } - } - } + // "t=