Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to ignore ready state check for synthetic stalls #91

Merged
merged 8 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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