Skip to content

Commit d8be47c

Browse files
authored
Add synthetic stalls (Dash-Industry-Forum#4600)
* Add synthetic stalling (set playback rate to 0) when stream stalls * Remove onPlaybackCanPlay from VideoModel interface * Add tests for setPlaybackRate & setStallState in VideoModel * Add tests for synthetic stall playing events * Add syntheticStallEvents config to config docs * also fixes typo (specified -> specifies) in docs
1 parent 8425e6a commit d8be47c

File tree

5 files changed

+210
-15
lines changed

5 files changed

+210
-15
lines changed

index.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,10 @@ declare namespace dashjs {
11081108
useChangeType?: boolean
11091109
mediaSourceDurationInfinity?: boolean
11101110
resetSourceBuffersForTrackSwitch?: boolean
1111+
syntheticStallEvents?: {
1112+
enabled?: boolean
1113+
ignoreReadyState?: boolean
1114+
}
11111115
},
11121116
gaps?: {
11131117
jumpGaps?: boolean,
@@ -2873,8 +2877,6 @@ declare namespace dashjs {
28732877

28742878
reset(): void;
28752879

2876-
onPlaybackCanPlay(): void;
2877-
28782880
setPlaybackRate(value: number, ignoreReadyState?: boolean): void;
28792881

28802882
setcurrentTime(currentTime: number, stickToBuffered: boolean): void;

src/core/Settings.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@ import Events from './events/Events.js';
121121
* avoidCurrentTimeRangePruning: false,
122122
* useChangeType: true,
123123
* mediaSourceDurationInfinity: true,
124-
* resetSourceBuffersForTrackSwitch: false
124+
* resetSourceBuffersForTrackSwitch: false,
125+
* syntheticStallEvents: {
126+
* enabled: false,
127+
* ignoreReadyState: false
128+
* }
125129
* },
126130
* gaps: {
127131
* jumpGaps: true,
@@ -412,7 +416,9 @@ import Events from './events/Events.js';
412416
* @property {boolean} [useAppendWindow=true]
413417
* Specifies if the appendWindow attributes of the MSE SourceBuffers should be set according to content duration from manifest.
414418
* @property {boolean} [setStallState=true]
415-
* Specifies if we fire manual waiting events once the stall threshold is reached
419+
* Specifies if we fire manual waiting events once the stall threshold is reached.
420+
* @property {module:Settings~SyntheticStallSettings} [syntheticStallEvents]
421+
* Specifies if manual stall events are to be fired once the stall threshold is reached.
416422
* @property {boolean} [avoidCurrentTimeRangePruning=false]
417423
* Avoids pruning of the buffered range that contains the current playback time.
418424
*
@@ -436,6 +442,17 @@ import Events from './events/Events.js';
436442
* Configuration for video media type of tracks.
437443
*/
438444

445+
/**
446+
* @typedef {Object} module:Settings~SyntheticStallSettings
447+
* @property {boolean} [enabled]
448+
* Enables manual stall events and sets the playback rate to 0 once the stall threshold is reached.
449+
* @property {boolean} [ignoreReadyState]
450+
* Ignore the media element's ready state when entering or exiting a stall.
451+
* Enable this when either of these scenarios still occur with synthetic stalls enabled:
452+
* - If the buffer is empty, but playback is not stalled.
453+
* - If playback resumes, but a playing event isn't reported.
454+
*/
455+
439456
/**
440457
* @typedef {Object} DebugSettings
441458
* @property {number} [logLevel=dashjs.Debug.LOG_LEVEL_WARNING]
@@ -1106,7 +1123,11 @@ function Settings() {
11061123
avoidCurrentTimeRangePruning: false,
11071124
useChangeType: true,
11081125
mediaSourceDurationInfinity: true,
1109-
resetSourceBuffersForTrackSwitch: false
1126+
resetSourceBuffersForTrackSwitch: false,
1127+
syntheticStallEvents: {
1128+
enabled: false,
1129+
ignoreReadyState: false
1130+
}
11101131
},
11111132
gaps: {
11121133
jumpGaps: true,

src/streaming/models/VideoModel.js

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ function VideoModel() {
4848

4949
let instance,
5050
logger,
51+
settings,
5152
element,
5253
_currentTime,
5354
setCurrentTimeReadyStateFunction,
55+
resumeReadyStateFunction,
5456
TTMLRenderingDiv,
5557
vttRenderingDiv,
5658
previousPlaybackRate,
@@ -60,11 +62,11 @@ function VideoModel() {
6062

6163
const context = this.context;
6264
const eventBus = EventBus(context).getInstance();
63-
const settings = Settings(context).getInstance();
6465
const stalledStreams = [];
6566

6667
function setup() {
6768
logger = Debug(context).getInstance().getLogger(instance);
69+
settings = Settings(context).getInstance();
6870
_currentTime = NaN;
6971
}
7072

@@ -75,25 +77,33 @@ function VideoModel() {
7577
function reset() {
7678
clearTimeout(timeout);
7779
eventBus.off(Events.PLAYBACK_PLAYING, onPlaying, this);
80+
stalledStreams.length = 0;
7881
}
7982

80-
function onPlaybackCanPlay() {
81-
if (element) {
82-
element.playbackRate = previousPlaybackRate || 1;
83-
element.removeEventListener('canplay', onPlaybackCanPlay);
83+
function setConfig(config) {
84+
if (!config) {
85+
return;
86+
}
87+
88+
if (config.settings) {
89+
settings = config.settings;
8490
}
8591
}
8692

8793
function setPlaybackRate(value, ignoreReadyState = false) {
8894
if (!element) {
8995
return;
9096
}
91-
if (!ignoreReadyState && element.readyState <= 2 && value > 0) {
92-
// If media element hasn't loaded enough data to play yet, wait until it has
93-
element.addEventListener('canplay', onPlaybackCanPlay);
94-
} else {
97+
98+
if (ignoreReadyState) {
9599
element.playbackRate = value;
100+
return;
96101
}
102+
103+
// If media element hasn't loaded enough data to play yet, wait until it has
104+
waitForReadyState(Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA, () => {
105+
element.playbackRate = value;
106+
});
97107
}
98108

99109
//TODO Move the DVR window calculations from MediaPlayer to Here.
@@ -237,12 +247,21 @@ function VideoModel() {
237247
}
238248

239249
function addStalledStream(type) {
240-
241250
if (type === null || !element || element.seeking || stalledStreams.indexOf(type) !== -1) {
242251
return;
243252
}
244253

245254
stalledStreams.push(type);
255+
256+
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)) {
257+
// Halt playback until nothing is stalled
258+
previousPlaybackRate = element.playbackRate;
259+
setPlaybackRate(0, true);
260+
261+
const event = document.createEvent('Event');
262+
event.initEvent('waiting', true, false);
263+
element.dispatchEvent(event);
264+
}
246265
}
247266

248267
function removeStalledStream(type) {
@@ -255,6 +274,26 @@ function VideoModel() {
255274
stalledStreams.splice(index, 1);
256275
}
257276

277+
if (settings.get().streaming.buffer.syntheticStallEvents.enabled && element && !isStalled()) {
278+
const resume = () => {
279+
setPlaybackRate(previousPlaybackRate || 1, settings.get().streaming.buffer.syntheticStallEvents.ignoreReadyState);
280+
281+
if (!element.paused) {
282+
const event = document.createEvent('Event');
283+
event.initEvent('playing', true, false);
284+
element.dispatchEvent(event);
285+
}
286+
}
287+
288+
if (settings.get().streaming.buffer.syntheticStallEvents.ignoreReadyState) {
289+
resume();
290+
} else {
291+
if (resumeReadyStateFunction && resumeReadyStateFunction.func && resumeReadyStateFunction.event) {
292+
removeEventListener(resumeReadyStateFunction.event, resumeReadyStateFunction.func);
293+
}
294+
resumeReadyStateFunction = waitForReadyState(Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA, resume);
295+
}
296+
}
258297
}
259298

260299
function stallStream(type, isStalled) {
@@ -513,6 +552,7 @@ function VideoModel() {
513552
removeChild,
514553
removeEventListener,
515554
reset,
555+
setConfig,
516556
setCurrentTime,
517557
setDisableRemotePlayback,
518558
setElement,

test/unit/mocks/VideoElementMock.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class VideoElementMock {
2626
this.nodeName = 'VIDEO';
2727
this.videoWidth = 800;
2828
this.videoHeight = 600;
29+
this.readyState = 0;
30+
this.events = {};
2931
}
3032

3133
constructor() {
@@ -46,6 +48,39 @@ class VideoElementMock {
4648
return textTrack.getCurrentCue();
4749
}
4850

51+
addEventListener(type, handler) {
52+
if (this.events.hasOwnProperty(type)) {
53+
this.events[type].push(handler);
54+
} else {
55+
this.events[type] = [handler];
56+
}
57+
}
58+
59+
removeEventListener(type, handler) {
60+
if (!this.events.hasOwnProperty(type)) {
61+
return;
62+
}
63+
64+
let index = this.events[type].indexOf(handler);
65+
if (index != -1) {
66+
this.events[type].splice(index, 1);
67+
}
68+
}
69+
70+
dispatchEvent(event) {
71+
const { type } = event;
72+
73+
if (!this.events.hasOwnProperty(type)) {
74+
return;
75+
}
76+
77+
let evs = this.events[type];
78+
let l = evs.length;
79+
for (let i = 0; i < l; i++) {
80+
evs[i]();
81+
}
82+
}
83+
4984
reset() {
5085
this.setup();
5186
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import VideoModel from '../../../../src/streaming/models/VideoModel.js';
2+
import VideoElementMock from '../../mocks/VideoElementMock.js';
3+
import Settings from '../../../../src/core/Settings.js';
4+
import Constants from '../../../../src/streaming/constants/Constants.js';
5+
6+
import {expect} from 'chai';
7+
8+
describe('VideoModel', () => {
9+
const context = {};
10+
const videoModel = VideoModel(context).getInstance();
11+
const videoElementMock = new VideoElementMock();
12+
const settings = Settings(context).getInstance();
13+
14+
beforeEach(() => {
15+
videoModel.setElement(videoElementMock);
16+
});
17+
18+
afterEach(() => {
19+
videoModel.reset();
20+
videoElementMock.reset();
21+
settings.reset();
22+
});
23+
24+
describe('setPlaybackRate()', () => {
25+
it('Should always set playback rate even when not in ready state if ignoring ready state', () => {
26+
videoElementMock.playbackRate = 1;
27+
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_NOTHING;
28+
29+
videoModel.setPlaybackRate(0, true);
30+
expect(videoElementMock.playbackRate).to.equal(0);
31+
});
32+
33+
it('Should set playback rate if the video element is in ready state', () => {
34+
videoElementMock.playbackRate = 1;
35+
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;
36+
37+
videoModel.setPlaybackRate(0.5, false);
38+
expect(videoElementMock.playbackRate).to.equal(0.5);
39+
});
40+
});
41+
42+
describe('setStallState()', () => {
43+
describe('syntheticStallEvents enabled', () => {
44+
beforeEach(() => {
45+
settings.update({ streaming: { buffer: { syntheticStallEvents: { enabled: true, ignoreReadyState: false } }}});
46+
videoModel.setConfig({ settings });
47+
})
48+
49+
it('Should set playback rate to 0 on stall if video element is in ready state', () => {
50+
videoElementMock.playbackRate = 1;
51+
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;
52+
53+
videoModel.setStallState('video', true);
54+
55+
expect(videoElementMock.playbackRate).to.equal(0);
56+
});
57+
58+
it('Should emit a waiting event on stall if video element is in ready state', (done) => {
59+
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;
60+
61+
const onWaiting = () => {
62+
videoElementMock.removeEventListener('waiting', onWaiting);
63+
done();
64+
};
65+
videoElementMock.addEventListener('waiting', onWaiting);
66+
67+
videoModel.setStallState('video', true);
68+
});
69+
70+
it('Should emit a playing event on stall end even if not in ready state if ignoring ready state', (done) => {
71+
settings.update({ streaming: { buffer: { syntheticStallEvents: { enabled: true, ignoreReadyState: true } }}});
72+
73+
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_NOTHING;
74+
75+
const onPlaying = () => {
76+
videoElementMock.removeEventListener('playing', onPlaying);
77+
done();
78+
}
79+
videoElementMock.addEventListener('playing', onPlaying);
80+
81+
videoModel.setStallState('video', false);
82+
});
83+
84+
it('Should emit a playing event on stall end if video element is in ready state', (done) => {
85+
videoElementMock.readyState = Constants.VIDEO_ELEMENT_READY_STATES.HAVE_FUTURE_DATA;
86+
87+
const onPlaying = () => {
88+
videoElementMock.removeEventListener('playing', onPlaying);
89+
done();
90+
}
91+
videoElementMock.addEventListener('playing', onPlaying);
92+
93+
videoModel.setStallState('video', false);
94+
});
95+
});
96+
});
97+
});

0 commit comments

Comments
 (0)