diff --git a/src/core/Settings.js b/src/core/Settings.js index e1af49b79b..7888485308 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -107,11 +107,15 @@ import Events from './events/Events'; * stallThreshold: 0.3, * useAppendWindow: true, * setStallState: true, - * emitSyntheticStallEvents: true, * avoidCurrentTimeRangePruning: false, * useChangeTypeForTrackSwitch: true, * mediaSourceDurationInfinity: true, - * resetSourceBuffersForTrackSwitch: false + * resetSourceBuffersForTrackSwitch: false, + * syntheticStallEvents: { + * enabled: false, + * ignoreReadyState: false + * } + * * }, * gaps: { * jumpGaps: true, @@ -337,7 +341,7 @@ import Events from './events/Events'; * Specifies if the appendWindow attributes of the MSE SourceBuffers should be set according to content duration from manifest. * @property {boolean} [setStallState=true] * Specifies if we record stalled streams once the stall threshold is reached - * @property {boolean} [emitSyntheticStallEvents=true] + * @property {module:Settings~SyntheticStallSettings} [syntheticStallEvents] * Specified if we fire manual stall events once the stall threshold is reached * @property {boolean} [avoidCurrentTimeRangePruning=false] * Avoids pruning of the buffered range that contains the current playback time. @@ -362,6 +366,17 @@ import Events from './events/Events'; * Configuration for video media type of tracks. */ +/** + * @typedef {Object} module:Settings~SyntheticStallSettings + * @property {boolean} [enabled] + * Fire manual stall events once the stall threshold is reached + * @property {boolean} [ignoreReadyState] + * Ignore the media element's ready state when entering and exiting a stall + * Enable this when either of these scenarios still occur with synthetic stalls enabled: + * - If the buffer is empty, but playback is not stalled. + * - If playback resumes, but a playing event isn't reported. + */ + /** * @typedef {Object} DebugSettings * @property {number} [logLevel=dashjs.Debug.LOG_LEVEL_WARNING] @@ -920,11 +935,14 @@ function Settings() { stallThreshold: 0.3, useAppendWindow: true, setStallState: true, - emitSyntheticStallEvents: true, avoidCurrentTimeRangePruning: false, useChangeTypeForTrackSwitch: true, mediaSourceDurationInfinity: true, - resetSourceBuffersForTrackSwitch: false + resetSourceBuffersForTrackSwitch: false, + syntheticStallEvents: { + enabled: false, + ignoreReadyState: false + } }, gaps: { jumpGaps: true, diff --git a/src/streaming/models/VideoModel.js b/src/streaming/models/VideoModel.js index 32f05d553d..8e51acce93 100644 --- a/src/streaming/models/VideoModel.js +++ b/src/streaming/models/VideoModel.js @@ -54,6 +54,7 @@ function VideoModel() { element, _currentTime, setCurrentTimeReadyStateFunction, + resumeReadyStateFunction, TTMLRenderingDiv, vttRenderingDiv, previousPlaybackRate, @@ -80,21 +81,20 @@ function VideoModel() { eventBus.off(Events.PLAYBACK_PLAYING, onPlaying, this); } - function onPlaybackCanPlay() { - if (element) { - element.playbackRate = previousPlaybackRate || 1; - element.removeEventListener('canplay', onPlaybackCanPlay); + function setPlaybackRate(value, ignoreReadyState = false) { + if (!element) { + return; } - } - function setPlaybackRate(value, ignoreReadyState = false) { - if (!element) return; - if (!ignoreReadyState && element.readyState <= 2 && value > 0) { - // If media element hasn't loaded enough data to play yet, wait until it has - element.addEventListener('canplay', onPlaybackCanPlay); - } else { + if (ignoreReadyState) { element.playbackRate = value; + return; } + + // If media element hasn't loaded enough data to play yet, wait until it has + waitForReadyState(Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA, () => { + element.playbackRate = value; + }); } //TODO Move the DVR window calculations from MediaPlayer to Here. @@ -238,19 +238,27 @@ function VideoModel() { } function addStalledStream(type) { - if (type === null || !element || element.seeking || stalledStreams.indexOf(type) !== -1) { return; } stalledStreams.push(type); - if (settings.get().streaming.buffer.emitSyntheticStallEvents && element && stalledStreams.length === 1 && element.readyState >= Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA) { + + if ( + settings.get().streaming.buffer.syntheticStallEvents.enabled && + element && + stalledStreams.length === 1 && + (settings.get().streaming.buffer.syntheticStallEvents.ignoreReadyState || getReadyState() >= Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA) + ) { logger.debug(`emitting synthetic waiting event and halting playback with playback rate 0`); + + previousPlaybackRate = element.playbackRate; + + setPlaybackRate(0, true); + // Halt playback until nothing is stalled. const event = document.createEvent('Event'); event.initEvent('waiting', true, false); - previousPlaybackRate = element.playbackRate; - setPlaybackRate(0); element.dispatchEvent(event); } } @@ -265,14 +273,27 @@ function VideoModel() { stalledStreams.splice(index, 1); } - // If nothing is stalled resume playback. - if (settings.get().streaming.buffer.emitSyntheticStallEvents && element && isStalled() === false && element.playbackRate === 0 && element.readyState >= Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA) { - logger.debug(`emitting synthetic playing event (if not paused) and resuming playback with playback rate: ${previousPlaybackRate || 1}`); - setPlaybackRate(previousPlaybackRate || 1); - if (!element.paused) { - const event = document.createEvent('Event'); - event.initEvent('playing', true, false); - element.dispatchEvent(event); + + if (settings.get().streaming.buffer.syntheticStallEvents.enabled && element && !isStalled() && element.playbackRate === 0) { + const resume = () => { + logger.debug(`emitting synthetic playing event (if not paused) and resuming playback with playback rate: ${previousPlaybackRate || 1}`); + + setPlaybackRate(previousPlaybackRate || 1, settings.get().streaming.buffer.syntheticStallEvents.ignoreReadyState); + + if (!element.paused) { + const event = document.createEvent('Event'); + event.initEvent('playing', true, false); + element.dispatchEvent(event); + } + } + + if (settings.get().streaming.buffer.syntheticStallEvents.ignoreReadyState) { + resume() + } else { + if (resumeReadyStateFunction && resumeReadyStateFunction.func && resumeReadyStateFunction.event) { + removeEventListener(resumeReadyStateFunction.event, resumeReadyStateFunction.func); + } + resumeReadyStateFunction = waitForReadyState(Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA, resume) } } } @@ -465,15 +486,18 @@ function VideoModel() { } function waitForReadyState(targetReadyState, callback) { - if (targetReadyState === Constants.VIDEO_ELEMENT_READY_STATES.HAVE_NOTHING || - getReadyState() >= targetReadyState) { + if ( + targetReadyState === Constants.VIDEO_ELEMENT_READY_STATES.HAVE_NOTHING || + getReadyState() >= targetReadyState + ) { callback(); return null; - } else { - // wait for the appropriate callback before checking again - const event = READY_STATES_TO_EVENT_NAMES[targetReadyState]; - _listenOnce(event, callback); } + + // wait for the appropriate callback before checking again + const event = READY_STATES_TO_EVENT_NAMES[targetReadyState]; + + return _listenOnce(event, callback); } function _listenOnce(event, callback) { @@ -483,6 +507,7 @@ function VideoModel() { // Call the original listener. callback(event); }; + addEventListener(event, func); return { func, event }