Skip to content

Commit

Permalink
feat: Add option to ignore ready state check for synthetic stalls (#91)
Browse files Browse the repository at this point in the history
* feat: remove ready state check

* feat: default emitSyntheticStallEvents to false

* wip: synthetic stall changes

* add option to check or ignore ready state

* fix: waitForReadyState not returning func

* style: remove redundant check

* fix: pass ignore ready state to playback rate setter

* fix: name

---------

Co-authored-by: Jo Monaghan <joanne.monaghan@bbc.co.uk>
  • Loading branch information
eirikbjornr and jmonaghan85 authored Apr 22, 2024
1 parent d2b8eea commit aaf36bf
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 34 deletions.
28 changes: 23 additions & 5 deletions src/core/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand Down
83 changes: 54 additions & 29 deletions src/streaming/models/VideoModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function VideoModel() {
element,
_currentTime,
setCurrentTimeReadyStateFunction,
resumeReadyStateFunction,
TTMLRenderingDiv,
vttRenderingDiv,
previousPlaybackRate,
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -483,6 +507,7 @@ function VideoModel() {
// Call the original listener.
callback(event);
};

addEventListener(event, func);

return { func, event }
Expand Down

0 comments on commit aaf36bf

Please sign in to comment.