diff --git a/src/playback-watcher.js b/src/playback-watcher.js index 33cac2552..6cb84c846 100644 --- a/src/playback-watcher.js +++ b/src/playback-watcher.js @@ -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_(); @@ -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 = () => { @@ -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); diff --git a/test/playback-watcher.test.js b/test/playback-watcher.test.js index 2cd511419..a60436416 100644 --- a/test/playback-watcher.test.js +++ b/test/playback-watcher.test.js @@ -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; @@ -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'