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 synthetic stalls #4600

Open
wants to merge 8 commits into
base: development
Choose a base branch
from
2 changes: 0 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2873,8 +2873,6 @@ declare namespace dashjs {

reset(): void;

onPlaybackCanPlay(): void;

setPlaybackRate(value: number, ignoreReadyState?: boolean): void;

setcurrentTime(currentTime: number, stickToBuffered: boolean): void;
Expand Down
21 changes: 19 additions & 2 deletions src/core/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,9 @@ import Events from './events/Events.js';
* @property {boolean} [useAppendWindow=true]
* 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 fire manual waiting events once the stall threshold is reached
* Specifies if we fire manual waiting events once the stall threshold is reached.
* @property {module:Settings~SyntheticStallSettings} [syntheticStallEvents]
* Specified if manual stall events are to be fired once the stall threshold is reached.
* @property {boolean} [avoidCurrentTimeRangePruning=false]
* Avoids pruning of the buffered range that contains the current playback time.
*
Expand All @@ -434,6 +436,17 @@ import Events from './events/Events.js';
* Configuration for video media type of tracks.
*/

/**
* @typedef {Object} module:Settings~SyntheticStallSettings
* @property {boolean} [enabled]
* Enables manual stall events and sets the playback rate to 0 once the stall threshold is reached.
* @property {boolean} [ignoreReadyState]
* Ignore the media element's ready state when entering or 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 @@ -1102,7 +1115,11 @@ function Settings() {
avoidCurrentTimeRangePruning: false,
useChangeType: true,
mediaSourceDurationInfinity: true,
resetSourceBuffersForTrackSwitch: false
resetSourceBuffersForTrackSwitch: false,
syntheticStallEvents: {
enabled: false,
ignoreReadyState: false
}
},
gaps: {
jumpGaps: true,
Expand Down
60 changes: 50 additions & 10 deletions src/streaming/models/VideoModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ function VideoModel() {

let instance,
logger,
settings,
element,
_currentTime,
setCurrentTimeReadyStateFunction,
resumeReadyStateFunction,
TTMLRenderingDiv,
vttRenderingDiv,
previousPlaybackRate,
Expand All @@ -60,11 +62,11 @@ function VideoModel() {

const context = this.context;
const eventBus = EventBus(context).getInstance();
const settings = Settings(context).getInstance();
const stalledStreams = [];

function setup() {
logger = Debug(context).getInstance().getLogger(instance);
settings = Settings(context).getInstance();
_currentTime = NaN;
}

Expand All @@ -75,25 +77,33 @@ function VideoModel() {
function reset() {
clearTimeout(timeout);
eventBus.off(Events.PLAYBACK_PLAYING, onPlaying, this);
stalledStreams.length = 0;
}

function onPlaybackCanPlay() {
if (element) {
element.playbackRate = previousPlaybackRate || 1;
element.removeEventListener('canplay', onPlaybackCanPlay);
function setConfig(config) {
if (!config) {
return;
}

if (config.settings) {
settings = config.settings;
}
}

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 @@ -237,12 +247,21 @@ function VideoModel() {
}

function addStalledStream(type) {

if (type === null || !element || element.seeking || stalledStreams.indexOf(type) !== -1) {
return;
}

stalledStreams.push(type);

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)) {
// Halt playback until nothing is stalled
previousPlaybackRate = element.playbackRate;
setPlaybackRate(0, true);

const event = document.createEvent('Event');
event.initEvent('waiting', true, false);
element.dispatchEvent(event);
}
}

function removeStalledStream(type) {
Expand All @@ -255,6 +274,26 @@ function VideoModel() {
stalledStreams.splice(index, 1);
}

if (settings.get().streaming.buffer.syntheticStallEvents.enabled && element && !isStalled()) {
const resume = () => {
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);
}
}
}

function stallStream(type, isStalled) {
Expand Down Expand Up @@ -513,6 +552,7 @@ function VideoModel() {
removeChild,
removeEventListener,
reset,
setConfig,
setCurrentTime,
setDisableRemotePlayback,
setElement,
Expand Down
35 changes: 35 additions & 0 deletions test/unit/mocks/VideoElementMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class VideoElementMock {
this.nodeName = 'VIDEO';
this.videoWidth = 800;
this.videoHeight = 600;
this.readyState = 0;
this.events = {};
}

constructor() {
Expand All @@ -46,6 +48,39 @@ class VideoElementMock {
return textTrack.getCurrentCue();
}

addEventListener(type, handler) {
if (this.events.hasOwnProperty(type)) {
this.events[type].push(handler);
} else {
this.events[type] = [handler];
}
}

removeEventListener(type, handler) {
if (!this.events.hasOwnProperty(type)) {
return;
}

let index = this.events[type].indexOf(handler);
if (index != -1) {
this.events[type].splice(index, 1);
}
}

dispatchEvent(event) {
const { type } = event;

if (!this.events.hasOwnProperty(type)) {
return;
}

let evs = this.events[type];
let l = evs.length;
for (let i = 0; i < l; i++) {
evs[i]();
}
}

reset() {
this.setup();
}
Expand Down
97 changes: 97 additions & 0 deletions test/unit/test/streaming/streaming.models.VideoModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import VideoModel from '../../../../src/streaming/models/VideoModel.js';
import VideoElementMock from '../../mocks/VideoElementMock.js';
import Settings from '../../../../src/core/Settings.js';
import Constants from '../../../../src/streaming/constants/Constants.js';

import {expect} from 'chai';

describe('VideoModel', () => {
const context = {};
const videoModel = VideoModel(context).getInstance();
const videoElementMock = new VideoElementMock();
const settings = Settings(context).getInstance();

beforeEach(() => {
videoModel.setElement(videoElementMock);
});

afterEach(() => {
videoModel.reset();
videoElementMock.reset();
settings.reset();
});

describe('setPlaybackRate()', () => {
it('Should always set playback rate even when not in ready state if ignoring ready state', () => {
videoElementMock.playbackRate = 1;
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_NOTHING;

videoModel.setPlaybackRate(0, true);
expect(videoElementMock.playbackRate).to.equal(0);
});

it('Should set playback rate if the video element is in ready state', () => {
videoElementMock.playbackRate = 1;
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;

videoModel.setPlaybackRate(0.5, false);
expect(videoElementMock.playbackRate).to.equal(0.5);
});
});

describe('setStallState()', () => {
describe('syntheticStallEvents enabled', () => {
beforeEach(() => {
settings.update({ streaming: { buffer: { syntheticStallEvents: { enabled: true, ignoreReadyState: false } }}});
videoModel.setConfig({ settings });
})

it('Should set playback rate to 0 on stall if video element is in ready state', () => {
videoElementMock.playbackRate = 1;
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;

videoModel.setStallState('video', true);

expect(videoElementMock.playbackRate).to.equal(0);
});

it('Should emit a waiting event on stall if video element is in ready state', (done) => {
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;

const onWaiting = () => {
videoElementMock.removeEventListener('waiting', onWaiting);
done();
};
videoElementMock.addEventListener('waiting', onWaiting);

videoModel.setStallState('video', true);
});

it('Should emit a playing event on stall end even if not in ready state if ignoring ready state', (done) => {
settings.update({ streaming: { buffer: { syntheticStallEvents: { enabled: true, ignoreReadyState: true } }}});

videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_NOTHING;

const onPlaying = () => {
videoElementMock.removeEventListener('playing', onPlaying);
done();
}
videoElementMock.addEventListener('playing', onPlaying);

videoModel.setStallState('video', false);
});

it('Should emit a playing event on stall end if video element is in ready state', (done) => {
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;

const onPlaying = () => {
videoElementMock.removeEventListener('playing', onPlaying);
done();
}
videoElementMock.addEventListener('playing', onPlaying);

videoModel.setStallState('video', false);
});
});
});
});