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

Skip over playback gaps that occur in the beginning of streams #1085

Merged
merged 4 commits into from
Mar 3, 2021
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
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);
gesinger marked this conversation as resolved.
Show resolved Hide resolved
}

// 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