Skip to content

Commit

Permalink
fix(playback-watcher): Skip over playback gaps that occur in the begi…
Browse files Browse the repository at this point in the history
…nning of streams (#1085)

Co-authored-by: Evan farina <[email protected]>
  • Loading branch information
evanfarina and evanfarina authored Mar 3, 2021
1 parent 0760d45 commit ccd9352
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/playback-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default class PlaybackWatcher {

this.logger_('initialize');

const playHandler = () => this.monitorCurrentTime_();
const canPlayHandler = () => this.monitorCurrentTime_();
const waitingHandler = () => this.techWaiting_();
const cancelTimerHandler = () => this.cancelTimer_();
Expand Down Expand Up @@ -118,6 +119,12 @@ 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);
}

// Define the dispose function to clean up our events
this.dispose = () => {
Expand All @@ -126,6 +133,7 @@ export default class PlaybackWatcher {
this.tech_.off('waiting', waitingHandler);
this.tech_.off(timerCancelEvents, cancelTimerHandler);
this.tech_.off('canplay', canPlayHandler);
this.tech_.off('play', playHandler);

loaderTypes.forEach((type) => {
mpc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend);
Expand Down
95 changes: 95 additions & 0 deletions test/playback-watcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,99 @@ QUnit.module('PlaybackWatcher', {
}
});

QUnit.test('skips over gap at beginning of stream if played before content is buffered', function(assert) {
let vhsGapSkipEvents = 0;
let hlsGapSkipEvents = 0;

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.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
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'
);
});

QUnit.test('multiple play events do not cause the gap-skipping logic to be called sooner than expected', function(assert) {
let vhsGapSkipEvents = 0;
let hlsGapSkipEvents = 0;

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.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);
// 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);

assert.equal(vhsGapSkipEvents, 0, 'there is no skipped gap');
assert.equal(hlsGapSkipEvents, 0, 'there is no skipped gap');

// check that player did not skip the gap
assert.equal(
Math.round(this.player.currentTime()),
0,
'Player did not seek over gap'
);
});

QUnit.test('skips over gap in firefox with waiting event', function(assert) {
let vhsGapSkipEvents = 0;
let hlsGapSkipEvents = 0;
Expand Down Expand Up @@ -1111,6 +1204,8 @@ QUnit.module('PlaybackWatcher isolated functions', {
tech: {
on: () => {},
off: () => {},
one: () => {},
paused: () => false,
// needed to construct a playback watcher
options_: {
playerId: 'mock-player-id'
Expand Down

0 comments on commit ccd9352

Please sign in to comment.