Skip to content

Commit

Permalink
fix: detect demuxed video underflow gaps (#948)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey authored Sep 23, 2020
1 parent 043ccc6 commit d0ef298
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 19 deletions.
50 changes: 40 additions & 10 deletions src/playback-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,15 @@ export default class PlaybackWatcher {
return true;
}

const sourceUpdater = this.tech_.vhs.masterPlaylistController_.sourceUpdater_;
const buffered = this.tech_.buffered();
const nextRange = Ranges.findNextRange(buffered, currentTime);
const videoUnderflow = this.videoUnderflow_({
audioBuffered: sourceUpdater.audioBuffered(),
videoBuffered: sourceUpdater.videoBuffered(),
currentTime
});

if (this.videoUnderflow_(nextRange, buffered, currentTime)) {
if (videoUnderflow) {
// Even though the video underflowed and was stuck in a gap, the audio overplayed
// the gap, leading currentTime into a buffered range. Seeking to currentTime
// allows the video to catch up to the audio position without losing any audio
Expand All @@ -459,6 +464,7 @@ export default class PlaybackWatcher {
this.tech_.trigger({type: 'usage', name: 'hls-video-underflow'});
return true;
}
const nextRange = Ranges.findNextRange(buffered, currentTime);

// check for gap
if (nextRange.length > 0) {
Expand Down Expand Up @@ -512,18 +518,42 @@ export default class PlaybackWatcher {
return false;
}

videoUnderflow_(nextRange, buffered, currentTime) {
if (nextRange.length === 0) {
videoUnderflow_({videoBuffered, audioBuffered, currentTime}) {
// audio only content will not have video underflow :)
if (!videoBuffered) {
return;
}
let gap;

// find a gap in demuxed content.
if (videoBuffered.length && audioBuffered.length) {
// in Chrome audio will continue to play for ~3s when we run out of video
// so we have to check that the video buffer did have some buffer in the
// past.
const lastVideoRange = Ranges.findRange(videoBuffered, currentTime - 3);
const videoRange = Ranges.findRange(videoBuffered, currentTime);
const audioRange = Ranges.findRange(audioBuffered, currentTime);

if (audioRange.length && !videoRange.length && lastVideoRange.length) {
gap = {start: lastVideoRange.end(0), end: audioRange.end(0)};
}

// find a gap in muxed content.
} else {
const nextRange = Ranges.findNextRange(videoBuffered, currentTime);

// Even if there is no available next range, there is still a possibility we are
// stuck in a gap due to video underflow.
const gap = this.gapFromVideoUnderflow_(buffered, currentTime);
if (!nextRange.length) {
gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime);
}
}

if (gap) {
this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` +
`Seeking to current time ${currentTime}`);
if (gap) {
this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` +
`Seeking to current time ${currentTime}`);

return true;
}
return true;
}

return false;
Expand Down
84 changes: 77 additions & 7 deletions test/playback-watcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ QUnit.test('skips over gap in chrome without waiting event', function(assert) {
assert.equal(hlsGapSkipEvents, 1, 'there is one skipped gap');
});

QUnit.test('skips over gap in Chrome due to video underflow', function(assert) {
QUnit.test('skips over gap in Chrome due to muxed video underflow', function(assert) {
let vhsVideoUnderflowEvents = 0;
let hlsVideoUnderflowEvents = 0;

Expand All @@ -166,10 +166,61 @@ QUnit.test('skips over gap in Chrome due to video underflow', function(assert) {
}
});

this.player.tech_.buffered = () => {
// 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('playing');
this.clock.tick(1);

assert.equal(vhsVideoUnderflowEvents, 0, 'no video underflow event got triggered');
assert.equal(hlsVideoUnderflowEvents, 0, 'no video underflow event got triggered');

const mpc = this.player.tech_.vhs.masterPlaylistController_;

mpc.sourceUpdater_.videoBuffered = () => {
return videojs.createTimeRanges([[0, 10], [10.1, 20]]);
};

this.player.currentTime(13);

const seeks = [];

this.player.tech_.setCurrentTime = (time) => {
seeks.push(time);
};

this.player.tech_.trigger('waiting');

assert.equal(seeks.length, 1, 'one seek');
assert.equal(seeks[0], 13, 'player seeked to current time');
assert.equal(vhsVideoUnderflowEvents, 1, 'triggered a video underflow event');
assert.equal(hlsVideoUnderflowEvents, 1, 'triggered a video underflow event');
});

QUnit.test('skips over gap in Chrome due to demuxed video underflow', function(assert) {
let vhsVideoUnderflowEvents = 0;
let hlsVideoUnderflowEvents = 0;

this.player.autoplay(true);

this.player.tech_.on('usage', (event) => {
if (event.name === 'vhs-video-underflow') {
vhsVideoUnderflowEvents++;
}
if (event.name === 'hls-video-underflow') {
hlsVideoUnderflowEvents++;
}
});

// set an arbitrary source
this.player.src({
src: 'master.m3u8',
Expand All @@ -188,7 +239,17 @@ QUnit.test('skips over gap in Chrome due to video underflow', function(assert) {
assert.equal(vhsVideoUnderflowEvents, 0, 'no video underflow event got triggered');
assert.equal(hlsVideoUnderflowEvents, 0, 'no video underflow event got triggered');

this.player.currentTime(13);
const mpc = this.player.tech_.vhs.masterPlaylistController_;

mpc.sourceUpdater_.videoBuffered = () => {
return videojs.createTimeRanges([[0, 15]]);
};

mpc.sourceUpdater_.audioBuffered = () => {
return videojs.createTimeRanges([[0, 20]]);
};

this.player.currentTime(18);

const seeks = [];

Expand All @@ -199,7 +260,7 @@ QUnit.test('skips over gap in Chrome due to video underflow', function(assert) {
this.player.tech_.trigger('waiting');

assert.equal(seeks.length, 1, 'one seek');
assert.equal(seeks[0], 13, 'player seeked to current time');
assert.equal(seeks[0], 18, 'player seeked to current time');
assert.equal(vhsVideoUnderflowEvents, 1, 'triggered a video underflow event');
assert.equal(hlsVideoUnderflowEvents, 1, 'triggered a video underflow event');
});
Expand Down Expand Up @@ -429,7 +490,7 @@ QUnit.test('fires notifications when activated', function(assert) {
this.player.tech_.currentTime = function() {
return currentTime;
};
this.player.tech_.buffered = function() {
this.player.tech_.vhs.masterPlaylistController_.sourceUpdater_.videoBuffered = function() {
return {
length: buffered.length,
start(i) {
Expand Down Expand Up @@ -570,7 +631,16 @@ QUnit.test('corrects seek outside of seekable', function(assert) {
currentTime: () => currentTime,
// mocked out
paused: () => false,
buffered: () => videojs.createTimeRanges()
buffered: () => videojs.createTimeRanges(),
trigger: () => {},
vhs: {
masterPlaylistController_: {
sourceUpdater_: {
videoBuffered: () => {},
audioBuffered: () => {}
}
}
}
};

// waiting
Expand Down Expand Up @@ -1078,7 +1148,7 @@ QUnit.module('PlaybackWatcher isolated functions', {
}
});

QUnit.test('skips gap from video underflow', function(assert) {
QUnit.test('skips gap from muxed video underflow', function(assert) {
assert.equal(
this.playbackWatcher.gapFromVideoUnderflow_(videojs.createTimeRanges(), 0),
null,
Expand Down
15 changes: 13 additions & 2 deletions test/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,24 @@ export const useFakeEnvironment = function(assert) {
if (this.log && this.log[level] && this.log[level].restore) {
if (assert) {
const calls = (this.log[level].args || []).map((args) => {
return args.join(', ');
return args.reduce((acc, val) => {
if (acc) {
acc += ', ';
}

acc += val;

if (val.stack) {
acc += '\n' + val.stack;
}
return acc;
}, '');
}).join('\n ');

assert.equal(
this.log[level].callCount,
0,
'no unexpected logs at level "' + level + '":\n ' + calls
'no unexpected logs at level "' + level + '":\n' + calls
);
}
this.log[level].restore();
Expand Down

0 comments on commit d0ef298

Please sign in to comment.