Skip to content

Commit

Permalink
fix: Monitor playback for stalls due to gaps in the beginning of stre…
Browse files Browse the repository at this point in the history
…am when a new source is loaded (#1087)
  • Loading branch information
evanfarina authored Mar 11, 2021
1 parent 01ca182 commit 64a1f35
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 19 deletions.
19 changes: 13 additions & 6 deletions src/playback-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,19 @@ export default class PlaybackWatcher {
this.tech_.on('waiting', waitingHandler);
this.tech_.on(timerCancelEvents, cancelTimerHandler);
this.tech_.on('canplay', canPlayHandler);
// Catch an edge case that occurs when there is a gap at the start of a stream and no content has buffered by the time the first `waiting` event is emitted.
// In this case, a `waiting` event is followed by a `play` event. On first play we need to check that playback has not stalled due to a gap, and skip the gap
// if it has
if (this.tech_.paused()) {
this.tech_.one('play', playHandler);
}

/*
An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case
is surfaced in one of two ways:
1) The `waiting` event is fired before the player has buffered content, making it impossible
to find or skip the gap. The `waiting` event is followed by a `play` event. On first play
we can check if playback is stalled due to a gap, and skip the gap if necessary.
2) A source with a gap at the beginning of the stream is loaded programatically while the player
is in a playing state. To catch this case, it's important that our one-time play listener is setup
even if the player is in a playing state
*/
this.tech_.one('play', playHandler);

// Define the dispose function to clean up our events
this.dispose = () => {
Expand Down
119 changes: 106 additions & 13 deletions test/playback-watcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ QUnit.module('PlaybackWatcher', {
this.old = {};

// setup a player
this.player = createPlayer({html5: {
vhs: {
overrideNative: true
this.player = createPlayer({
html5: {
vhs: {
overrideNative: true
}
}
}});
});
this.player.muted(true);
this.player.autoplay(true);
},
Expand Down Expand Up @@ -70,7 +72,7 @@ QUnit.test('skips over gap at beginning of stream if played before content is bu
this.player.tech_.trigger('waiting');
// create a buffer with a gap of 2 seconds at beginning of stream
this.player.tech_.buffered = () => videojs.createTimeRanges([[2, 10]]);
// Playback watcher loop runs on a 250ms clock and needs run 6 consecutive stall checks before skipping the gap
// Playback watcher loop runs on a 250ms clock and needs 6 consecutive stall checks before skipping the gap
this.clock.tick(250 * 6);
// Need to wait for the duration of the gap
this.clock.tick(2000);
Expand Down Expand Up @@ -105,22 +107,17 @@ QUnit.test('multiple play events do not cause the gap-skipping logic to be calle
type: 'application/vnd.apple.mpegurl'
});

// start playback normally
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
this.player.tech_.trigger('play');
this.player.tech_.trigger('waiting');
// create a buffer with a gap of 2 seconds at beginning of stream
this.player.tech_.buffered = () => videojs.createTimeRanges([[2, 10]]);
// Playback watcher loop runs on a 250ms clock and needs run 6 consecutive stall checks before skipping the gap
// Start with three consecutive playback checks
this.clock.tick(250 * 3);
// Playback watcher loop runs on a 250ms clock and needs 6 consecutive stall checks before skipping the gap
// Start with 5 consecutive playback checks
this.clock.tick(250 * 5);
// and then simulate the playback monitor being called 'manually' by a new play event
this.player.tech_.trigger('play');
// Simulate remaining time
this.clock.tick(250 * 2);
// Need to wait for the duration of the gap
this.clock.tick(2000);

Expand All @@ -133,6 +130,102 @@ QUnit.test('multiple play events do not cause the gap-skipping logic to be calle
0,
'Player did not seek over gap'
);

// Simulate remaining time
this.clock.tick(250);
// Need to wait for the duration of the gap
this.clock.tick(2000);

assert.equal(vhsGapSkipEvents, 1, 'there is one skipped gap');
assert.equal(hlsGapSkipEvents, 1, 'there is one skipped gap');

// check that player did skip the gap after another 250ms has gone by
assert.equal(
Math.round(this.player.currentTime()),
2,
'Player did skip the gap'
);
});

QUnit.test('changing sources does not break ability to skip gap at beginning of stream on first play', function(assert) {
let vhsGapSkipEvents = 0;
let hlsGapSkipEvents = 0;

this.player = createPlayer({
html5: {
vhs: {
overrideNative: true
}
},
enableSourceset: true
});

this.player.autoplay(true);

this.player.tech_.on('usage', (event) => {
if (event.name === 'vhs-gap-skip') {
vhsGapSkipEvents++;
}
if (event.name === 'hls-gap-skip') {
hlsGapSkipEvents++;
}
});

// set an arbitrary source
this.player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
// start playback normally
this.player.tech_.triggerReady();
this.clock.tick(1);
standardXHRResponse(this.requests.shift());
openMediaSource(this.player, this.clock);
this.player.play();
this.player.tech_.trigger('waiting');
// create a buffer with a gap of 2 seconds at beginning of stream
this.player.tech_.buffered = () => videojs.createTimeRanges([[2, 10]]);
// Playback watcher loop runs on a 250ms clock and needs 6 consecutive stall checks before skipping the gap
this.clock.tick(250 * 6);
// Need to wait for the duration of the gap
this.clock.tick(2000);

assert.equal(vhsGapSkipEvents, 1, 'there is one skipped gap');
assert.equal(hlsGapSkipEvents, 1, 'there is one skipped gap');

// check that player jumped the gap
assert.equal(
Math.round(this.player.currentTime()),
2,
'Player seeked over gap after timer'
);

// Simulate the source changing while the player is in a `playing` state
vhsGapSkipEvents = 0;
hlsGapSkipEvents = 0;
this.player.currentTime(0);

this.player.src({
src: 'new-master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.clock.tick(1);

// Playback watcher loop runs on a 250ms clock and needs 6 consecutive stall checks before skipping the gap
this.clock.tick(250 * 6);
// Need to wait for the duration of the gap
this.clock.tick(2000);

assert.equal(vhsGapSkipEvents, 1, 'there is one skipped gap');
assert.equal(hlsGapSkipEvents, 1, 'there is one skipped gap');

// check that player jumped the gap
assert.equal(
Math.round(this.player.currentTime()),
2,
'Player seeked over gap after source changed'
);
});

QUnit.test('skips over gap in firefox with waiting event', function(assert) {
Expand Down

0 comments on commit 64a1f35

Please sign in to comment.