From d090d015192452b000a87334275443be738cb3c1 Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Thu, 19 Aug 2021 14:36:37 +0200 Subject: [PATCH 1/6] Refactor PlaybackController.js --- .../controllers/PlaybackController.js | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js index d7124a6dfb..e08af758b5 100644 --- a/src/streaming/controllers/PlaybackController.js +++ b/src/streaming/controllers/PlaybackController.js @@ -102,9 +102,9 @@ function PlaybackController() { const isSafari = /safari/.test(ua) && !/chrome/.test(ua); minPlaybackRateChange = isSafari ? 0.25 : 0.02; - eventBus.on(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.on(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); - eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); + eventBus.on(Events.DATA_UPDATE_COMPLETED, _onDataUpdateCompleted, this); + eventBus.on(Events.LOADING_PROGRESS, _onFragmentLoadProgress, this); + eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, this); eventBus.on(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, this); eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackProgression, this); eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, this, { priority: EventBus.EVENT_PRIORITY_HIGH }); @@ -346,9 +346,9 @@ function PlaybackController() { availabilityStartTime = 0; seekTarget = NaN; if (videoModel) { - eventBus.off(Events.DATA_UPDATE_COMPLETED, onDataUpdateCompleted, this); - eventBus.off(Events.LOADING_PROGRESS, onFragmentLoadProgress, this); - eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, onBufferLevelStateChanged, this); + eventBus.off(Events.DATA_UPDATE_COMPLETED, _onDataUpdateCompleted, this); + eventBus.off(Events.LOADING_PROGRESS, _onFragmentLoadProgress, this); + eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, this); eventBus.off(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, this); eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackProgression, this); eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, this); @@ -424,7 +424,7 @@ function PlaybackController() { if (wallclockTimeIntervalId !== null) return; const tick = function () { - onWallclockTime(); + _onWallclockTime(); }; wallclockTimeIntervalId = setInterval(tick, settings.get().streaming.wallclockTimeUpdateInterval); @@ -457,7 +457,7 @@ function PlaybackController() { } } - function onDataUpdateCompleted(e) { + function _onDataUpdateCompleted(e) { const representationInfo = adapter.convertRepresentationToRepresentationInfo(e.currentRepresentation); const info = representationInfo ? representationInfo.mediaInfo.streamInfo : null; @@ -465,37 +465,37 @@ function PlaybackController() { streamInfo = info; } - function onCanPlay() { + function _onCanPlay() { eventBus.trigger(Events.CAN_PLAY); } - function onCanPlayThrough() { + function _onCanPlayThrough() { eventBus.trigger(Events.CAN_PLAY_THROUGH); } - function onPlaybackStart() { + function _onPlaybackStart() { logger.info('Native video element event: play'); updateCurrentTime(); startUpdatingWallclockTime(); eventBus.trigger(Events.PLAYBACK_STARTED, { startTime: getTime() }); } - function onPlaybackWaiting() { + function _onPlaybackWaiting() { logger.info('Native video element event: waiting'); eventBus.trigger(Events.PLAYBACK_WAITING, { playingTime: getTime() }); } - function onPlaybackPlaying() { + function _onPlaybackPlaying() { logger.info('Native video element event: playing'); eventBus.trigger(Events.PLAYBACK_PLAYING, { playingTime: getTime() }); } - function onPlaybackPaused() { + function _onPlaybackPaused() { logger.info('Native video element event: pause'); eventBus.trigger(Events.PLAYBACK_PAUSED, { ended: getEnded() }); } - function onPlaybackSeeking() { + function _onPlaybackSeeking() { // Check if internal seeking to be ignored if (internalSeek) { internalSeek = false; @@ -518,12 +518,12 @@ function PlaybackController() { }); } - function onPlaybackSeeked() { + function _onPlaybackSeeked() { logger.info('Native video element event: seeked'); eventBus.trigger(Events.PLAYBACK_SEEKED); } - function onPlaybackTimeUpdated() { + function _onPlaybackTimeUpdated() { if (streamInfo) { eventBus.trigger(Events.PLAYBACK_TIME_UPDATED, { timeToEnd: getTimeToStreamEnd(), @@ -533,37 +533,29 @@ function PlaybackController() { } } - function updateLivePlaybackTime() { - const now = Date.now(); - if (!lastLivePlaybackTime || now > lastLivePlaybackTime + LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS) { - lastLivePlaybackTime = now; - onPlaybackTimeUpdated(); - } - } - - function onPlaybackProgress() { + function _onPlaybackProgress() { eventBus.trigger(Events.PLAYBACK_PROGRESS, { streamId: streamInfo.id }); } - function onPlaybackRateChanged() { + function _onPlaybackRateChanged() { const rate = getPlaybackRate(); logger.info('Native video element event: ratechange: ', rate); eventBus.trigger(Events.PLAYBACK_RATE_CHANGED, { playbackRate: rate }); } - function onPlaybackMetaDataLoaded() { + function _onPlaybackMetaDataLoaded() { logger.info('Native video element event: loadedmetadata'); eventBus.trigger(Events.PLAYBACK_METADATA_LOADED); startUpdatingWallclockTime(); } - function onPlaybackLoadedData() { + function _onPlaybackLoadedData() { logger.info('Native video element event: loadeddata'); eventBus.trigger(Events.PLAYBACK_LOADED_DATA); } // Event to handle the native video element ended event - function onNativePlaybackEnded() { + function _onNativePlaybackEnded() { logger.info('Native video element event: ended'); pause(); stopUpdatingWallclockTime(); @@ -584,12 +576,12 @@ function PlaybackController() { } } - function onPlaybackError(event) { + function _onPlaybackError(event) { const target = event.target || event.srcElement; eventBus.trigger(Events.PLAYBACK_ERROR, { error: target.error }); } - function onWallclockTime() { + function _onWallclockTime() { eventBus.trigger(Events.WALLCLOCK_TIME_UPDATED, { isDynamic: isDynamic, time: new Date() @@ -599,14 +591,22 @@ function PlaybackController() { // (video element doesn't call timeupdate when the playback is paused) if (getIsDynamic()) { if (isPaused()) { - updateLivePlaybackTime(); + _updateLivePlaybackTime(); } else { updateCurrentTime(); } } + } + function _updateLivePlaybackTime() { + const now = Date.now(); + if (!lastLivePlaybackTime || now > lastLivePlaybackTime + LIVE_UPDATE_PLAYBACK_TIME_INTERVAL_MS) { + lastLivePlaybackTime = now; + _onPlaybackTimeUpdated(); + } } + function _onPlaybackProgression() { if ( isDynamic && @@ -873,7 +873,7 @@ function PlaybackController() { } } - function onFragmentLoadProgress(e) { + 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)) { const minDelay = 1.2 * e.request.duration; @@ -890,7 +890,7 @@ function PlaybackController() { } } - function onBufferLevelStateChanged(e) { + function _onBufferLevelStateChanged(e) { // do not stall playback when get an event from Stream that is not active if (e.streamId !== streamInfo.id) return; @@ -929,41 +929,41 @@ function PlaybackController() { } function addAllListeners() { - videoModel.addEventListener('canplay', onCanPlay); - videoModel.addEventListener('canplaythrough', onCanPlayThrough); - videoModel.addEventListener('play', onPlaybackStart); - videoModel.addEventListener('waiting', onPlaybackWaiting); - videoModel.addEventListener('playing', onPlaybackPlaying); - videoModel.addEventListener('pause', onPlaybackPaused); - videoModel.addEventListener('error', onPlaybackError); - videoModel.addEventListener('seeking', onPlaybackSeeking); - videoModel.addEventListener('seeked', onPlaybackSeeked); - videoModel.addEventListener('timeupdate', onPlaybackTimeUpdated); - videoModel.addEventListener('progress', onPlaybackProgress); - videoModel.addEventListener('ratechange', onPlaybackRateChanged); - videoModel.addEventListener('loadedmetadata', onPlaybackMetaDataLoaded); - videoModel.addEventListener('loadeddata', onPlaybackLoadedData); + videoModel.addEventListener('canplay', _onCanPlay); + videoModel.addEventListener('canplaythrough', _onCanPlayThrough); + videoModel.addEventListener('play', _onPlaybackStart); + videoModel.addEventListener('waiting', _onPlaybackWaiting); + videoModel.addEventListener('playing', _onPlaybackPlaying); + videoModel.addEventListener('pause', _onPlaybackPaused); + videoModel.addEventListener('error', _onPlaybackError); + videoModel.addEventListener('seeking', _onPlaybackSeeking); + videoModel.addEventListener('seeked', _onPlaybackSeeked); + videoModel.addEventListener('timeupdate', _onPlaybackTimeUpdated); + videoModel.addEventListener('progress', _onPlaybackProgress); + videoModel.addEventListener('ratechange', _onPlaybackRateChanged); + videoModel.addEventListener('loadedmetadata', _onPlaybackMetaDataLoaded); + videoModel.addEventListener('loadeddata', _onPlaybackLoadedData); videoModel.addEventListener('stalled', onPlaybackStalled); - videoModel.addEventListener('ended', onNativePlaybackEnded); + videoModel.addEventListener('ended', _onNativePlaybackEnded); } function removeAllListeners() { - videoModel.removeEventListener('canplay', onCanPlay); - videoModel.removeEventListener('canplaythrough', onCanPlayThrough); - videoModel.removeEventListener('play', onPlaybackStart); - videoModel.removeEventListener('waiting', onPlaybackWaiting); - videoModel.removeEventListener('playing', onPlaybackPlaying); - videoModel.removeEventListener('pause', onPlaybackPaused); - videoModel.removeEventListener('error', onPlaybackError); - videoModel.removeEventListener('seeking', onPlaybackSeeking); - videoModel.removeEventListener('seeked', onPlaybackSeeked); - videoModel.removeEventListener('timeupdate', onPlaybackTimeUpdated); - videoModel.removeEventListener('progress', onPlaybackProgress); - videoModel.removeEventListener('ratechange', onPlaybackRateChanged); - videoModel.removeEventListener('loadedmetadata', onPlaybackMetaDataLoaded); - videoModel.removeEventListener('loadeddata', onPlaybackLoadedData); + videoModel.removeEventListener('canplay', _onCanPlay); + videoModel.removeEventListener('canplaythrough', _onCanPlayThrough); + videoModel.removeEventListener('play', _onPlaybackStart); + videoModel.removeEventListener('waiting', _onPlaybackWaiting); + videoModel.removeEventListener('playing', _onPlaybackPlaying); + videoModel.removeEventListener('pause', _onPlaybackPaused); + videoModel.removeEventListener('error', _onPlaybackError); + videoModel.removeEventListener('seeking', _onPlaybackSeeking); + videoModel.removeEventListener('seeked', _onPlaybackSeeked); + videoModel.removeEventListener('timeupdate', _onPlaybackTimeUpdated); + videoModel.removeEventListener('progress', _onPlaybackProgress); + videoModel.removeEventListener('ratechange', _onPlaybackRateChanged); + videoModel.removeEventListener('loadedmetadata', _onPlaybackMetaDataLoaded); + videoModel.removeEventListener('loadeddata', _onPlaybackLoadedData); videoModel.removeEventListener('stalled', onPlaybackStalled); - videoModel.removeEventListener('ended', onNativePlaybackEnded); + videoModel.removeEventListener('ended', _onNativePlaybackEnded); } instance = { From 8cc2fde3661e50bcf518e2f53040ac143ad8cbd1 Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Thu, 26 Aug 2021 11:11:21 +0200 Subject: [PATCH 2/6] Refactor BufferController.js --- src/streaming/controllers/BufferController.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index 6996bfac7e..0076a833ef 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -165,7 +165,10 @@ function BufferController(config) { } const requiredQuality = abrController.getQualityFor(type, streamInfo.id); - sourceBufferSink = SourceBufferSink(context).create({ mediaSource, textController }); + sourceBufferSink = SourceBufferSink(context).create({ + mediaSource, + textController + }); _initializeSink(mediaInfo, oldBufferSinks, requiredQuality) .then(() => { return updateBufferTimestampOffset(_getRepresentationInfo(requiredQuality)); @@ -250,7 +253,7 @@ function BufferController(config) { }); if (chunk.mediaInfo.type === Constants.VIDEO) { - triggerEvent(Events.VIDEO_CHUNK_RECEIVED, { chunk: chunk }); + _triggerEvent(Events.VIDEO_CHUNK_RECEIVED, { chunk: chunk }); } } @@ -271,7 +274,7 @@ function BufferController(config) { if (e.error.code === QUOTA_EXCEEDED_ERROR_CODE || !hasEnoughSpaceToAppend()) { logger.warn('Clearing playback buffer to overcome quota exceed situation'); // Notify ScheduleController to stop scheduling until buffer has been pruned - triggerEvent(Events.QUOTA_EXCEEDED, { + _triggerEvent(Events.QUOTA_EXCEEDED, { criticalBufferLevel: criticalBufferLevel, quotaExceededTime: e.chunk.start }); @@ -302,7 +305,7 @@ function BufferController(config) { } if (appendedBytesInfo) { - triggerEvent(Events.BYTES_APPENDED_END_FRAGMENT, { + _triggerEvent(Events.BYTES_APPENDED_END_FRAGMENT, { quality: appendedBytesInfo.quality, startTime: appendedBytesInfo.start, index: appendedBytesInfo.index, @@ -642,7 +645,7 @@ function BufferController(config) { if (playbackController) { const tolerance = settings.get().streaming.gaps.jumpGaps && !isNaN(settings.get().streaming.gaps.smallGapLimit) ? settings.get().streaming.gaps.smallGapLimit : NaN; bufferLevel = Math.max(getBufferLength(playbackController.getTime() || 0, tolerance), 0); - triggerEvent(Events.BUFFER_LEVEL_UPDATED, { mediaType: type, bufferLevel: bufferLevel }); + _triggerEvent(Events.BUFFER_LEVEL_UPDATED, { mediaType: type, bufferLevel: bufferLevel }); checkIfSufficientBuffer(); } } @@ -682,8 +685,8 @@ function BufferController(config) { bufferState = state; - triggerEvent(Events.BUFFER_LEVEL_STATE_CHANGED, { state: state }); - triggerEvent(state === MetricsConstants.BUFFER_LOADED ? Events.BUFFER_LOADED : Events.BUFFER_EMPTY); + _triggerEvent(Events.BUFFER_LEVEL_STATE_CHANGED, { state: state }); + _triggerEvent(state === MetricsConstants.BUFFER_LOADED ? Events.BUFFER_LOADED : Events.BUFFER_EMPTY); logger.debug(state === MetricsConstants.BUFFER_LOADED ? 'Got enough buffer to start' : 'Waiting for more buffer before starting playback'); } @@ -827,7 +830,7 @@ function BufferController(config) { if (e.unintended) { logger.warn('Detected unintended removal from:', e.from, 'to', e.to, 'setting streamprocessor time to', e.from); - triggerEvent(Events.SEEK_TARGET, { time: e.from }); + _triggerEvent(Events.SEEK_TARGET, { time: e.from }); } if (isPruningInProgress) { @@ -838,7 +841,7 @@ function BufferController(config) { } else { replacingBuffer = false; } - triggerEvent(Events.BUFFER_CLEARED, { + _triggerEvent(Events.BUFFER_CLEARED, { from: e.from, to: e.to, unintended: e.unintended, @@ -918,7 +921,7 @@ function BufferController(config) { isBufferingCompleted = value; if (isBufferingCompleted) { - triggerEvent(Events.BUFFERING_COMPLETED); + _triggerEvent(Events.BUFFERING_COMPLETED); } else { maximumIndex = Number.POSITIVE_INFINITY; } @@ -990,7 +993,7 @@ function BufferController(config) { seekTarget = value; } - function triggerEvent(eventType, data) { + function _triggerEvent(eventType, data) { let payload = data || {}; eventBus.trigger(eventType, payload, { streamId: streamInfo.id, mediaType: type }); } From d8ef15a48eeda133380c997e1e5dc809b966b29b Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Thu, 26 Aug 2021 11:12:01 +0200 Subject: [PATCH 3/6] Refactor SourceBufferSink.js --- src/streaming/SourceBufferSink.js | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/streaming/SourceBufferSink.js b/src/streaming/SourceBufferSink.js index 284cbc6433..2aa528a63b 100644 --- a/src/streaming/SourceBufferSink.js +++ b/src/streaming/SourceBufferSink.js @@ -149,17 +149,17 @@ function SourceBufferSink(config) { // use updateend event if possible if (typeof buffer.addEventListener === 'function') { try { - buffer.addEventListener('updateend', updateEndHandler, false); - buffer.addEventListener('error', errHandler, false); - buffer.addEventListener('abort', errHandler, false); + buffer.addEventListener('updateend', _updateEndHandler, false); + buffer.addEventListener('error', _errHandler, false); + buffer.addEventListener('abort', _errHandler, false); } catch (err) { // use setInterval to periodically check if updating has been completed - intervalId = setInterval(updateEndHandler, CHECK_INTERVAL); + intervalId = setInterval(_updateEndHandler, CHECK_INTERVAL); } } else { // use setInterval to periodically check if updating has been completed - intervalId = setInterval(updateEndHandler, CHECK_INTERVAL); + intervalId = setInterval(_updateEndHandler, CHECK_INTERVAL); } } @@ -170,9 +170,9 @@ function SourceBufferSink(config) { function _removeEventListeners() { try { if (typeof buffer.removeEventListener === 'function') { - buffer.removeEventListener('updateend', updateEndHandler, false); - buffer.removeEventListener('error', errHandler, false); - buffer.removeEventListener('abort', errHandler, false); + buffer.removeEventListener('updateend', _updateEndHandler, false); + buffer.removeEventListener('error', _errHandler, false); + buffer.removeEventListener('abort', _errHandler, false); } clearInterval(intervalId); } catch (e) { @@ -283,7 +283,7 @@ function SourceBufferSink(config) { return; } appendQueue.push({ data: chunk, promise: { resolve, reject } }); - waitForUpdateEnd(appendNextInQueue.bind(this)); + waitForUpdateEnd(_appendNextInQueue.bind(this)); }); } @@ -342,7 +342,7 @@ function SourceBufferSink(config) { }); } - function appendNextInQueue() { + function _appendNextInQueue() { if (isAppendingInProgress) { return; } @@ -355,7 +355,7 @@ function SourceBufferSink(config) { const afterSuccess = function () { isAppendingInProgress = false; if (appendQueue.length > 0) { - appendNextInQueue.call(this); + _appendNextInQueue.call(this); } // Init segments are cached. In any other case we dont need the chunk bytes anymore and can free the memory if (nextChunk && nextChunk.data && nextChunk.data.segmentType && nextChunk.data.segmentType !== HTTPRequest.INIT_SEGMENT_TYPE) { @@ -379,7 +379,7 @@ function SourceBufferSink(config) { } catch (err) { logger.fatal('SourceBuffer append failed "' + err + '"'); if (appendQueue.length > 0) { - appendNextInQueue(); + _appendNextInQueue(); } else { isAppendingInProgress = false; } @@ -412,28 +412,28 @@ function SourceBufferSink(config) { } - function executeCallback() { + function _executeCallback() { if (callbacks.length > 0) { if (!buffer.updating) { const cb = callbacks.shift(); cb(); // Try to execute next callback if still not updating - executeCallback(); + _executeCallback(); } } } - function updateEndHandler() { + function _updateEndHandler() { // if updating is still in progress do nothing and wait for the next check again. if (buffer.updating) { return; } // updating is completed, now we can stop checking and resolve the promise - executeCallback(); + _executeCallback(); } - function errHandler() { + function _errHandler() { logger.error('SourceBufferSink error'); } @@ -441,7 +441,7 @@ function SourceBufferSink(config) { callbacks.push(callback); if (!buffer.updating) { - executeCallback(); + _executeCallback(); } } From 6b54a21312b1fe88c71ae00a0145bdd3e40cd053 Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Thu, 26 Aug 2021 14:12:39 +0200 Subject: [PATCH 4/6] Reset MSE after MEDIA_ERROR_DECODE and blacklist the segment that caused the error --- src/core/Settings.js | 5 +++ src/core/events/CoreEvents.js | 3 ++ src/streaming/SourceBufferSink.js | 40 ++++++++++------- src/streaming/Stream.js | 43 ++++++++++++------- src/streaming/StreamProcessor.js | 35 ++++++++++++++- src/streaming/controllers/BufferController.js | 9 ++-- src/streaming/controllers/StreamController.js | 36 ++++++++++++++-- .../text/NotFragmentedTextBufferController.js | 2 +- 8 files changed, 132 insertions(+), 41 deletions(-) diff --git a/src/core/Settings.js b/src/core/Settings.js index 7c14892cb9..06fcf42a7d 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -884,6 +884,11 @@ function Settings() { rtpSafetyFactor: 5, mode: Constants.CMCD_MODE_QUERY } + }, + errors: { + recoverAttempts: { + mediaErrorDecode: 3 + } } }; diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js index 765b2589ee..6035671794 100644 --- a/src/core/events/CoreEvents.js +++ b/src/core/events/CoreEvents.js @@ -62,10 +62,13 @@ class CoreEvents extends EventsBase { this.MEDIA_FRAGMENT_LOADED = 'mediaFragmentLoaded'; this.MEDIA_FRAGMENT_NEEDED = 'mediaFragmentNeeded'; this.QUOTA_EXCEEDED = 'quotaExceeded'; + this.SEGMENT_LOCATION_BLACKLIST_ADD = 'segmentLocationBlacklistAdd'; + this.SEGMENT_LOCATION_BLACKLIST_CHANGED = 'segmentLocationBlacklistChanged'; this.SERVICE_LOCATION_BLACKLIST_ADD = 'serviceLocationBlacklistAdd'; this.SERVICE_LOCATION_BLACKLIST_CHANGED = 'serviceLocationBlacklistChanged'; this.SET_FRAGMENTED_TEXT_AFTER_DISABLED = 'setFragmentedTextAfterDisabled'; this.SET_NON_FRAGMENTED_TEXT = 'setNonFragmentedText'; + this.SOURCE_BUFFER_ERROR = 'sourceBufferError'; this.STREAMS_COMPOSED = 'streamsComposed'; this.STREAM_BUFFERING_COMPLETED = 'streamBufferingCompleted'; this.STREAM_REQUESTING_COMPLETED = 'streamRequestingCompleted'; diff --git a/src/streaming/SourceBufferSink.js b/src/streaming/SourceBufferSink.js index 2aa528a63b..fb5c42416d 100644 --- a/src/streaming/SourceBufferSink.js +++ b/src/streaming/SourceBufferSink.js @@ -35,6 +35,7 @@ import Errors from '../core/errors/Errors'; import Settings from '../core/Settings'; import constants from './constants/Constants'; import {HTTPRequest} from './vo/metrics/HTTPRequest'; +import Events from '../core/events/Events'; const APPEND_WINDOW_START_OFFSET = 0.1; const APPEND_WINDOW_END_OFFSET = 0.01; @@ -51,6 +52,7 @@ function SourceBufferSink(config) { const context = this.context; const settings = Settings(context).getInstance(); const textController = config.textController; + const eventBus = config.eventBus; let instance, type, @@ -63,6 +65,7 @@ function SourceBufferSink(config) { let appendQueue = []; let isAppendingInProgress = false; let mediaSource = config.mediaSource; + let lastRequestAppended = null; function setup() { logger = Debug(context).getInstance().getLogger(instance); @@ -91,7 +94,7 @@ function SourceBufferSink(config) { function changeType(codec) { return new Promise((resolve) => { - waitForUpdateEnd(() => { + _waitForUpdateEnd(() => { if (buffer.changeType) { buffer.changeType(codec); } @@ -188,7 +191,7 @@ function SourceBufferSink(config) { return; } - waitForUpdateEnd(() => { + _waitForUpdateEnd(() => { try { if (!buffer) { resolve(); @@ -227,7 +230,7 @@ function SourceBufferSink(config) { return; } - waitForUpdateEnd(() => { + _waitForUpdateEnd(() => { try { if (buffer.timestampOffset !== MSETimeOffset && !isNaN(MSETimeOffset)) { buffer.timestampOffset = MSETimeOffset; @@ -258,6 +261,7 @@ function SourceBufferSink(config) { } buffer = null; } + lastRequestAppended = null; } function getBuffer() { @@ -273,7 +277,7 @@ function SourceBufferSink(config) { } } - function append(chunk) { + function append(chunk, request = null) { return new Promise((resolve, reject) => { if (!chunk) { reject({ @@ -282,14 +286,14 @@ function SourceBufferSink(config) { }); return; } - appendQueue.push({ data: chunk, promise: { resolve, reject } }); - waitForUpdateEnd(_appendNextInQueue.bind(this)); + appendQueue.push({ data: chunk, promise: { resolve, reject }, request }); + _waitForUpdateEnd(_appendNextInQueue.bind(this)); }); } function _abortBeforeAppend() { return new Promise((resolve) => { - waitForUpdateEnd(() => { + _waitForUpdateEnd(() => { // Save the append window, which is reset on abort(). const appendWindowStart = buffer.appendWindowStart; const appendWindowEnd = buffer.appendWindowEnd; @@ -313,11 +317,11 @@ function SourceBufferSink(config) { return; } - waitForUpdateEnd(function () { + _waitForUpdateEnd(function () { try { buffer.remove(start, end); // updating is in progress, we should wait for it to complete before signaling that this operation is done - waitForUpdateEnd(function () { + _waitForUpdateEnd(function () { resolve({ from: start, to: end, @@ -365,6 +369,7 @@ function SourceBufferSink(config) { }; try { + lastRequestAppended = nextChunk.request; if (nextChunk.data.bytes.byteLength === 0) { afterSuccess.call(this); } else { @@ -374,7 +379,7 @@ function SourceBufferSink(config) { buffer.append(nextChunk.data.bytes, nextChunk.data); } // updating is in progress, we should wait for it to complete before signaling that this operation is done - waitForUpdateEnd(afterSuccess.bind(this)); + _waitForUpdateEnd(afterSuccess.bind(this)); } } catch (err) { logger.fatal('SourceBuffer append failed "' + err + '"'); @@ -395,7 +400,7 @@ function SourceBufferSink(config) { try { appendQueue = []; if (mediaSource.readyState === 'open') { - waitForUpdateEnd(() => { + _waitForUpdateEnd(() => { buffer.abort(); resolve(); }); @@ -433,11 +438,17 @@ function SourceBufferSink(config) { _executeCallback(); } - function _errHandler() { - logger.error('SourceBufferSink error'); + function _errHandler(e) { + const error = e.target || {}; + _triggerEvent(Events.SOURCE_BUFFER_ERROR, { error, lastRequestAppended }) } - function waitForUpdateEnd(callback) { + function _triggerEvent(eventType, data) { + let payload = data || {}; + eventBus.trigger(eventType, payload, { streamId: mediaInfo.streamInfo.id, mediaType: type }); + } + + function _waitForUpdateEnd(callback) { callbacks.push(callback); if (!buffer.updating) { @@ -454,7 +465,6 @@ function SourceBufferSink(config) { abort, reset, updateTimestampOffset, - waitForUpdateEnd, initializeForStreamSwitch, initializeForFirstUse, updateAppendWindow, diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js index 4ba12c999f..bbc854de28 100644 --- a/src/streaming/Stream.js +++ b/src/streaming/Stream.js @@ -41,6 +41,7 @@ import FactoryMaker from '../core/FactoryMaker'; import DashJSError from './vo/DashJSError'; import BoxParser from './utils/BoxParser'; import URLUtils from './utils/URLUtils'; +import BlacklistController from './controllers/BlacklistController'; const MEDIA_TYPES = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT, Constants.MUXED, Constants.IMAGE]; @@ -84,6 +85,7 @@ function Stream(config) { isUpdating, fragmentController, thumbnailController, + segmentBlacklistController, preloaded, boxParser, debug, @@ -101,6 +103,11 @@ function Stream(config) { boxParser = BoxParser(context).getInstance(); + segmentBlacklistController = BlacklistController(context).create({ + updateEventName: Events.SEGMENT_LOCATION_BLACKLIST_CHANGED, + addBlacklistEventName: Events.SEGMENT_LOCATION_BLACKLIST_ADD + }); + fragmentController = FragmentController(context).create({ streamInfo: streamInfo, mediaPlayerModel: mediaPlayerModel, @@ -430,24 +437,25 @@ function Stream(config) { const isFragmented = mediaInfo ? mediaInfo.isFragmented : null; let streamProcessor = StreamProcessor(context).create({ - streamInfo: streamInfo, - type: type, - mimeType: mimeType, - timelineConverter: timelineConverter, - adapter: adapter, - manifestModel: manifestModel, - mediaPlayerModel: mediaPlayerModel, - fragmentModel: fragmentModel, + streamInfo, + type, + mimeType, + timelineConverter, + adapter, + manifestModel, + mediaPlayerModel, + fragmentModel, dashMetrics: config.dashMetrics, baseURLController: config.baseURLController, segmentBaseController: config.segmentBaseController, - abrController: abrController, - playbackController: playbackController, - mediaController: mediaController, - textController: textController, - errHandler: errHandler, - settings: settings, - boxParser: boxParser + abrController, + playbackController, + mediaController, + textController, + errHandler, + settings, + boxParser, + segmentBlacklistController }); streamProcessor.initialize(mediaSource, hasVideoTrack, isFragmented); @@ -560,6 +568,11 @@ function Stream(config) { abrController.clearDataForStream(streamInfo.id); } + if (segmentBlacklistController) { + segmentBlacklistController.reset(); + segmentBlacklistController = null; + } + resetInitialSettings(keepBuffers); streamInfo = null; diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index e207ebd560..6934ef70be 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -73,6 +73,7 @@ function StreamProcessor(config) { let dashMetrics = config.dashMetrics; let settings = config.settings; let boxParser = config.boxParser; + let segmentBlacklistController = config.segmentBlacklistController; let instance, logger, @@ -107,6 +108,7 @@ function StreamProcessor(config) { eventBus.on(Events.SET_NON_FRAGMENTED_TEXT, _onSetNonFragmentedText, instance); eventBus.on(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); eventBus.on(Events.STREAMS_COMPOSED, _onStreamsComposed, instance); + eventBus.on(Events.SOURCE_BUFFER_ERROR, _onSourceBufferError, instance); } function initialize(mediaSource, hasVideoTrack, isFragmented) { @@ -253,6 +255,7 @@ function StreamProcessor(config) { eventBus.off(Events.QUOTA_EXCEEDED, _onQuotaExceeded, instance); eventBus.off(Events.MANIFEST_UPDATED, _onManifestUpdated, instance); eventBus.off(Events.STREAMS_COMPOSED, _onStreamsComposed, instance); + eventBus.off(Events.SOURCE_BUFFER_ERROR, _onSourceBufferError, instance); resetInitialSettings(); type = null; @@ -432,14 +435,27 @@ function StreamProcessor(config) { } if (request) { - logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`); - fragmentModel.executeRequest(request); + if (!_shouldIgnoreRequest(request.url)) { + logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`); + fragmentModel.executeRequest(request); + } else { + logger.warn(`Fragment request url ${request.url} for stream id ${streamInfo.id} and media type ${type} is on the ignore list and will be skipped`); + _noValidRequest(); + } } else if (rescheduleIfNoRequest) { // Use case - Playing at the bleeding live edge and frag is not available yet. Cycle back around. _noValidRequest(); } } + /** + * In certain situations we need to ignore a request. For instance, if a segment is blacklisted because it caused an MSE error. + * @private + */ + function _shouldIgnoreRequest(url) { + return segmentBlacklistController.contains(url) + } + /** * Get the init or media segment request using the DashHandler. * @return {null|FragmentRequest|null} @@ -523,7 +539,22 @@ function StreamProcessor(config) { if (e.hasEnoughSpaceToAppend && e.quotaExceeded) { scheduleController.startScheduleTimer(); } + } + + /** + * This function is called when the corresponding SourceBuffer encounterd an error. + * We blacklist the last segment assuming it caused the error + * @param {object} e + * @private + */ + function _onSourceBufferError(e) { + if (!e || !e.lastRequestAppended) { + return; + } + const blacklistUrl = e.lastRequestAppended.url; + logger.warn(`Blacklisting segment with url ${blacklistUrl}`); + segmentBlacklistController.add(blacklistUrl); } /** diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index 0076a833ef..6a7e4c6e96 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -167,7 +167,8 @@ function BufferController(config) { const requiredQuality = abrController.getQualityFor(type, streamInfo.id); sourceBufferSink = SourceBufferSink(context).create({ mediaSource, - textController + textController, + eventBus }); _initializeSink(mediaInfo, oldBufferSinks, requiredQuality) .then(() => { @@ -235,7 +236,7 @@ function BufferController(config) { * @param {object} e */ function _onMediaFragmentLoaded(e) { - _appendToBuffer(e.chunk); + _appendToBuffer(e.chunk, e.request); } /** @@ -243,8 +244,8 @@ function BufferController(config) { * @param {object} chunk * @private */ - function _appendToBuffer(chunk) { - sourceBufferSink.append(chunk) + function _appendToBuffer(chunk, request = null) { + sourceBufferSink.append(chunk, request) .then((e) => { _onAppended(e); }) diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 05163cb73f..6f52d48b2d 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -100,7 +100,8 @@ function StreamController() { supportsChangeType, settings, firstLicenseIsFetched, - waitForPlaybackStartTimeout; + waitForPlaybackStartTimeout, + errorInformation; function setup() { logger = Debug(context).getInstance().getLogger(instance); @@ -162,7 +163,7 @@ function StreamController() { function registerEvents() { eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance); - eventBus.on(MediaPlayerEvents.PLAYBACK_ERROR, onPlaybackError, instance); + eventBus.on(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance); eventBus.on(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); eventBus.on(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance); eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); @@ -185,7 +186,7 @@ function StreamController() { function unRegisterEvents() { eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance); eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance); - eventBus.off(MediaPlayerEvents.PLAYBACK_ERROR, onPlaybackError, instance); + eventBus.off(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance); eventBus.off(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance); eventBus.off(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance); eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance); @@ -1216,7 +1217,7 @@ function StreamController() { dashMetrics.createPlaylistMetrics(playbackController.getTime() * 1000, startReason); } - function onPlaybackError(e) { + function _onPlaybackError(e) { if (!e.error) return; let msg = ''; @@ -1242,6 +1243,13 @@ function StreamController() { break; } + + if (msg === 'MEDIA_ERR_DECODE' && settings.get().errors.recoverAttempts.mediaErrorDecode > errorInformation.counts.mediaErrorDecode) { + errorInformation.counts.mediaErrorDecode += 1; + _handleMediaErrorDecode(); + return; + } + hasMediaError = true; if (e.error.message) { @@ -1260,6 +1268,21 @@ function StreamController() { reset(); } + /** + * Handles mediaError + * @private + */ + function _handleMediaErrorDecode() { + logger.warn('A MEDIA_ERR_DECODE occured: Resetting the MediaSource'); + const time = playbackController.getTime(); + // Deactivate the current stream. + activeStream.deactivate(false); + + // Reset MSE + logger.warn(`MediaSource has been resetted. Resuming playback from time ${time}`); + _openMediaSource(time, false); + } + function getActiveStreamInfo() { return activeStream ? activeStream.getStreamInfo() : null; } @@ -1395,6 +1418,11 @@ function StreamController() { supportsChangeType = false; preloadingStreams = []; waitForPlaybackStartTimeout = null; + errorInformation = { + counts: { + mediaErrorDecode: 0 + } + } } function reset() { diff --git a/src/streaming/text/NotFragmentedTextBufferController.js b/src/streaming/text/NotFragmentedTextBufferController.js index 0e8cc6dcd3..0ea6e6d54e 100644 --- a/src/streaming/text/NotFragmentedTextBufferController.js +++ b/src/streaming/text/NotFragmentedTextBufferController.js @@ -77,7 +77,7 @@ function NotFragmentedTextBufferController(config) { function createBufferSink(mediaInfo) { return new Promise((resolve, reject) => { try { - sourceBufferSink = SourceBufferSink(context).create({ mediaSource, textController }); + sourceBufferSink = SourceBufferSink(context).create({ mediaSource, textController, eventBus }); sourceBufferSink.initializeForFirstUse(streamInfo, mediaInfo); if (!initialized) { if (sourceBufferSink.getBuffer() && typeof sourceBufferSink.getBuffer().initialize === 'function') { From 1fc74f387b01ec75af1c54d02974612eea9bb008 Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Thu, 26 Aug 2021 14:19:30 +0200 Subject: [PATCH 5/6] Fix unit test --- test/unit/streaming.controllers.StreamController.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/streaming.controllers.StreamController.js b/test/unit/streaming.controllers.StreamController.js index ace8c9d7e9..a94dafaf13 100644 --- a/test/unit/streaming.controllers.StreamController.js +++ b/test/unit/streaming.controllers.StreamController.js @@ -122,6 +122,7 @@ describe('StreamController', function () { describe('Well initialized', () => { beforeEach(function () { + settings.reset(); streamController.setConfig({ adapter: adapterMock, manifestLoader: manifestLoaderMock, @@ -176,6 +177,7 @@ describe('StreamController', function () { }); it('should return the correct error when a playback error occurs : MEDIA_ERR_DECODE', function () { + settings.update({ errors: { recoverAttempts: { mediaErrorDecode: 0 } } }) eventBus.trigger(Events.PLAYBACK_ERROR, { error: { code: 3 } }); expect(errHandlerMock.errorValue).to.include('MEDIA_ERR_DECODE'); From bd9b5246b6a3321cd12a29e171f1da02c5329b33 Mon Sep 17 00:00:00 2001 From: dsilhavy Date: Fri, 27 Aug 2021 08:38:08 +0200 Subject: [PATCH 6/6] Account for SegmentBase content when blacklisting segments --- src/core/Settings.js | 2 +- src/streaming/StreamProcessor.js | 22 ++++++++++++++----- src/streaming/controllers/StreamController.js | 4 ++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/core/Settings.js b/src/core/Settings.js index 06fcf42a7d..9fc88ca24e 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -887,7 +887,7 @@ function Settings() { }, errors: { recoverAttempts: { - mediaErrorDecode: 3 + mediaErrorDecode: 5 } } }; diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index 6934ef70be..1d4063eba1 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -435,7 +435,7 @@ function StreamProcessor(config) { } if (request) { - if (!_shouldIgnoreRequest(request.url)) { + if (!_shouldIgnoreRequest(request)) { logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`); fragmentModel.executeRequest(request); } else { @@ -452,8 +452,14 @@ function StreamProcessor(config) { * In certain situations we need to ignore a request. For instance, if a segment is blacklisted because it caused an MSE error. * @private */ - function _shouldIgnoreRequest(url) { - return segmentBlacklistController.contains(url) + function _shouldIgnoreRequest(request) { + let blacklistUrl = request.url; + + if (request.range) { + blacklistUrl = blacklistUrl.concat('_', request.range); + } + + return segmentBlacklistController.contains(blacklistUrl) } /** @@ -542,17 +548,21 @@ function StreamProcessor(config) { } /** - * This function is called when the corresponding SourceBuffer encounterd an error. + * This function is called when the corresponding SourceBuffer encountered an error. * We blacklist the last segment assuming it caused the error * @param {object} e * @private */ function _onSourceBufferError(e) { - if (!e || !e.lastRequestAppended) { + if (!e || !e.lastRequestAppended || !e.lastRequestAppended.url) { return; } - const blacklistUrl = e.lastRequestAppended.url; + let blacklistUrl = e.lastRequestAppended.url; + + if (e.lastRequestAppended.range) { + blacklistUrl = blacklistUrl.concat('_', e.lastRequestAppended.range); + } logger.warn(`Blacklisting segment with url ${blacklistUrl}`); segmentBlacklistController.add(blacklistUrl); } diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js index 6f52d48b2d..6716ea6d4a 100644 --- a/src/streaming/controllers/StreamController.js +++ b/src/streaming/controllers/StreamController.js @@ -1231,6 +1231,7 @@ function StreamController() { break; case 3: msg = 'MEDIA_ERR_DECODE'; + errorInformation.counts.mediaErrorDecode += 1; break; case 4: msg = 'MEDIA_ERR_SRC_NOT_SUPPORTED'; @@ -1244,8 +1245,7 @@ function StreamController() { } - if (msg === 'MEDIA_ERR_DECODE' && settings.get().errors.recoverAttempts.mediaErrorDecode > errorInformation.counts.mediaErrorDecode) { - errorInformation.counts.mediaErrorDecode += 1; + if (msg === 'MEDIA_ERR_DECODE' && settings.get().errors.recoverAttempts.mediaErrorDecode >= errorInformation.counts.mediaErrorDecode) { _handleMediaErrorDecode(); return; }