diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index ea36bd25ed..5b8d25b681 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -229,7 +229,6 @@ function StreamProcessor(config) { 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 diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index ebd3bc4a26..874be66831 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -241,7 +241,7 @@ function BufferController(config) { function showBufferRanges(ranges) { if (ranges && ranges.length > 0) { for (let i = 0, len = ranges.length; i < len; i++) { - logger.debug('Buffered Range', ranges.start(i), ' - ', ranges.end(i), ' currentTime = ', playbackController.getTime()); + logger.debug('Buffered range: ' + ranges.start(i) + ' - ' + ranges.end(i) + ', currentTime = ', playbackController.getTime()); } } } @@ -281,19 +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(); - logger.debug('AppendToBuffer seek target should be ' + currentTime); - triggerEvent(Events.SEEK_TARGET, {time: currentTime}); - } + adjustSeekTarget(); + } else if (replacingBuffer) { + // When replacing buffer due to switch track, and once new initialization segment has been appended + // (and previous buffered data removed) then seek stream to current time + const currentTime = playbackController.getTime(); + logger.debug('AppendToBuffer seek target should be ' + currentTime); + triggerEvent(Events.SEEK_TARGET, {time: currentTime}); } if (appendedBytesInfo) { @@ -306,6 +300,38 @@ function BufferController(config) { } } + function adjustSeekTarget () { + // Check buffered data only for audio and video + if (type !== Constants.AUDIO && type !== Constants.VIDEO) return; + if (isNaN(seekTarget)) return; + + // Check if current buffered range already contains seek target (and current video element time) + const currentTime = playbackController.getTime(); + let range = getRangeAt(seekTarget, 0); + if (currentTime === seekTarget && range) return; + + // Get buffered range corresponding to the seek target + const segmentDuration = representationController.getCurrentRepresentation().segmentDuration; + range = getRangeAt(seekTarget, segmentDuration); + if (!range) return; + + if (Math.abs(currentTime - seekTarget) > segmentDuration) { + // If current video model time is decorrelated from seek target (and appended buffer) then seek video element + // (in case of live streams on some browsers/devices for which we can't set video element time at unavalaible range) + + // Check if appended segment is not anterior from seek target (segments timeline/template tolerance) + if (seekTarget <= range.end) { + // Seek video element to seek target or range start if appended buffer starts after seek target (segments timeline/template tolerance) + playbackController.seek(Math.max(seekTarget, range.start), false, true); + seekTarget = NaN; + } + } else if (currentTime < range.start) { + // If appended buffer starts after seek target (segments timeline/template tolerance) then seek to range start + playbackController.seek(range.start, false, true); + seekTarget = NaN; + } + } + function onQualityChanged(e) { if (e.streamInfo.id != streamInfo.id || e.mediaType !== type || requiredQuality === e.newQuality) return; @@ -441,7 +467,7 @@ function BufferController(config) { let len, i; - const toler = (tolerance || 0.15); + const toler = !isNaN(tolerance) ? tolerance : 0.15; if (ranges !== null && ranges !== undefined) { for (i = 0, len = ranges.length; i < len; i++) { @@ -653,9 +679,6 @@ function BufferController(config) { if (currentTime < range.end) { isBufferingCompleted = false; maxAppendedIndex = 0; - if (!replacingBuffer) { - triggerEvent(Events.SEEK_TARGET, {time: currentTime}); - } } buffer.remove(range.start, range.end, range.force); @@ -682,7 +705,6 @@ function BufferController(config) { clearNextRange(); } else { if (!replacingBuffer) { - logger.debug('onRemoved : call updateBufferLevel'); updateBufferLevel(); } else { replacingBuffer = false; diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index b7d70a39e5..c24f7e04ff 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -59,6 +59,7 @@ function PlaybackController() { playOnceInitialized, lastLivePlaybackTime, availabilityStartTime, + seekTarget, isLowLatencySeekingInProgress, playbackStalled, minPlaybackRateChange, @@ -178,20 +179,22 @@ function PlaybackController() { } function seek(time, stickToBuffered, internalSeek) { - if (streamInfo && videoModel) { - if (internalSeek === true) { - if (time !== videoModel.getTime()) { - // Internal seek = seek video model only (disable 'seeking' listener), - // buffer(s) are already appended at given time (see onBytesAppended()) - videoModel.removeEventListener('seeking', onPlaybackSeeking); - logger.info('Requesting internal seek to time: ' + time); - videoModel.setCurrentTime(time, stickToBuffered); - } - } else { - eventBus.trigger(Events.PLAYBACK_SEEK_ASKED); - logger.info('Requesting seek to time: ' + time); - videoModel.setCurrentTime(time, stickToBuffered); - } + if (!streamInfo || !videoModel) return; + + let currentTime = !isNaN(seekTarget) ? seekTarget : videoModel.getTime(); + if (time === currentTime) return; + + if (internalSeek === true) { + // Internal seek = seek video model only (disable 'seeking' listener) + // buffer(s) are already appended at requested time + videoModel.removeEventListener('seeking', onPlaybackSeeking); + logger.info('Requesting internal seek to time: ' + time); + videoModel.setCurrentTime(time, stickToBuffered); + } else { + seekTarget = time; + eventBus.trigger(Events.PLAYBACK_SEEK_ASKED); + logger.info('Requesting seek to time: ' + time); + videoModel.setCurrentTime(time, stickToBuffered); } } @@ -322,6 +325,7 @@ function PlaybackController() { streamSeekTime = NaN; liveDelay = 0; availabilityStartTime = 0; + seekTarget = NaN; if (videoModel) { eventBus.off(Events.STREAM_INITIALIZED, onStreamInitialized, this); eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); @@ -486,7 +490,15 @@ function PlaybackController() { } function onPlaybackSeeking() { - const seekTime = getTime(); + let seekTime = getTime(); + + // On some browsers/devices, in case of live streams, setting current time on video element fails when there is no buffered data at requested time + // Then re-set seek target time and video element will be seeked afterwhile once data is buffered (see BufferContoller) + if (!isNaN(seekTarget) && seekTarget !== seekTime) { + seekTime = seekTarget; + } + seekTarget = NaN; + logger.info('Seeking to: ' + seekTime); startUpdatingWallclockTime(); eventBus.trigger(Events.PLAYBACK_SEEKING, { diff --git a/src/streaming/controllers/ScheduleController.js b/src/streaming/controllers/ScheduleController.js index 5fd3eded20..5108e82000 100644 --- a/src/streaming/controllers/ScheduleController.js +++ b/src/streaming/controllers/ScheduleController.js @@ -458,11 +458,12 @@ function ScheduleController(config) { latency: latency }); - //if, during the seek command, the scheduleController is waiting : stop waiting, request chunk as soon as possible if (!isFragmentProcessingInProgress) { + // Restart scheduler if in pending state startScheduleTimer(0); } else { - logger.debug('onPlaybackSeeking, call fragmentModel.abortRequests in order to seek quicker'); + // Abort current requests + logger.debug('Abort requests'); fragmentModel.abortRequests(); } }