Skip to content

Commit 8b3533c

Browse files
fix: keep media update timeout alive so live playlists can recover from network issues (#1176)
1 parent e8230a9 commit 8b3533c

File tree

3 files changed

+125
-19
lines changed

3 files changed

+125
-19
lines changed

src/master-playlist-controller.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,25 @@ const shouldSwitchToMedia = function({
6363

6464
const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`;
6565

66-
// If the playlist is live, then we want to not take low water line into account.
67-
// This is because in LIVE, the player plays 3 segments from the end of the
68-
// playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
69-
// in those segments, a viewer will never experience a rendition upswitch.
70-
if (!currentPlaylist || !currentPlaylist.endList) {
71-
log(`${sharedLogLine} as current playlist ` + (!currentPlaylist ? 'is not set' : 'is live'));
66+
if (!currentPlaylist) {
67+
log(`${sharedLogLine} as current playlist is not set`);
7268
return true;
7369
}
7470

75-
// no need to switch playlist is the same
71+
// no need to switch if playlist is the same
7672
if (nextPlaylist.id === currentPlaylist.id) {
7773
return false;
7874
}
7975

76+
// If the playlist is live, then we want to not take low water line into account.
77+
// This is because in LIVE, the player plays 3 segments from the end of the
78+
// playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
79+
// in those segments, a viewer will never experience a rendition upswitch.
80+
if (!currentPlaylist.endList) {
81+
log(`${sharedLogLine} as current playlist is live`);
82+
return true;
83+
}
84+
8085
const maxBufferLowWaterLine = experimentalBufferBasedABR ?
8186
Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE;
8287

@@ -351,7 +356,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
351356
checkABR_() {
352357
const nextPlaylist = this.selectPlaylist();
353358

354-
if (this.shouldSwitchToMedia_(nextPlaylist)) {
359+
if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) {
355360
this.switchMedia_(nextPlaylist, 'abr');
356361
}
357362
}

src/playlist-loader.js

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -505,13 +505,7 @@ export default class PlaylistLoader extends EventTarget {
505505
this.trigger('playlistunchanged');
506506
}
507507

508-
// refresh live playlists after a target duration passes
509-
if (!this.media().endList) {
510-
window.clearTimeout(this.mediaUpdateTimeout);
511-
this.mediaUpdateTimeout = window.setTimeout(() => {
512-
this.trigger('mediaupdatetimeout');
513-
}, refreshDelay(this.media(), !!update));
514-
}
508+
this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update));
515509

516510
this.trigger('loadedplaylist');
517511
}
@@ -619,6 +613,13 @@ export default class PlaylistLoader extends EventTarget {
619613
return;
620614
}
621615

616+
// We update/set the timeout here so that live playlists
617+
// that are not a media change will "start" the loader as expected.
618+
// We expect that this function will start the media update timeout
619+
// cycle again. This also prevents a playlist switch failure from
620+
// causing us to stall during live.
621+
this.updateMediaUpdateTimeout_(refreshDelay(playlist, true));
622+
622623
// switching to the active playlist is a no-op
623624
if (!mediaChange) {
624625
return;
@@ -679,8 +680,12 @@ export default class PlaylistLoader extends EventTarget {
679680
* pause loading of the playlist
680681
*/
681682
pause() {
683+
if (this.mediaUpdateTimeout) {
684+
window.clearTimeout(this.mediaUpdateTimeout);
685+
this.mediaUpdateTimeout = null;
686+
}
687+
682688
this.stopRequest();
683-
window.clearTimeout(this.mediaUpdateTimeout);
684689
if (this.state === 'HAVE_NOTHING') {
685690
// If we pause the loader before any data has been retrieved, its as if we never
686691
// started, so reset to an unstarted state.
@@ -705,14 +710,20 @@ export default class PlaylistLoader extends EventTarget {
705710
* start loading of the playlist
706711
*/
707712
load(shouldDelay) {
708-
window.clearTimeout(this.mediaUpdateTimeout);
709-
713+
if (this.mediaUpdateTimeout) {
714+
window.clearTimeout(this.mediaUpdateTimeout);
715+
this.mediaUpdateTimeout = null;
716+
}
710717
const media = this.media();
711718

712719
if (shouldDelay) {
713720
const delay = media ? ((media.partTargetDuration || media.targetDuration) / 2) * 1000 : 5 * 1000;
714721

715-
this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay);
722+
this.mediaUpdateTimeout = window.setTimeout(() => {
723+
this.mediaUpdateTimeout = null;
724+
this.load();
725+
}, delay);
726+
716727
return;
717728
}
718729

@@ -728,6 +739,24 @@ export default class PlaylistLoader extends EventTarget {
728739
}
729740
}
730741

742+
updateMediaUpdateTimeout_(delay) {
743+
if (this.mediaUpdateTimeout) {
744+
window.clearTimeout(this.mediaUpdateTimeout);
745+
this.mediaUpdateTimeout = null;
746+
}
747+
748+
// we only have use mediaupdatetimeout for live playlists.
749+
if (!this.media() || this.media().endList) {
750+
return;
751+
}
752+
753+
this.mediaUpdateTimeout = window.setTimeout(() => {
754+
this.mediaUpdateTimeout = null;
755+
this.trigger('mediaupdatetimeout');
756+
this.updateMediaUpdateTimeout_(delay);
757+
}, delay);
758+
}
759+
731760
/**
732761
* start loading of the playlist
733762
*/

test/playlist-loader.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,22 @@ QUnit.module('Playlist Loader', function(hooks) {
683683
);
684684
});
685685

686+
QUnit.test('can delay load', function(assert) {
687+
const loader = new PlaylistLoader('master.m3u8', this.fakeVhs);
688+
689+
assert.notOk(loader.mediaUpdateTimeout, 'no media update timeout');
690+
691+
loader.load(true);
692+
693+
assert.ok(loader.mediaUpdateTimeout, 'have a media update timeout now');
694+
assert.strictEqual(this.requests.length, 0, 'have no requests');
695+
696+
this.clock.tick(5000);
697+
698+
assert.notOk(loader.mediaUpdateTimeout, 'media update timeout is gone');
699+
assert.strictEqual(this.requests.length, 1, 'playlist request after delay');
700+
});
701+
686702
QUnit.test('starts without any metadata', function(assert) {
687703
const loader = new PlaylistLoader('master.m3u8', this.fakeVhs);
688704

@@ -2101,6 +2117,62 @@ QUnit.module('Playlist Loader', function(hooks) {
21012117
);
21022118
});
21032119

2120+
QUnit.test('mediaupdatetimeout works as expected for live playlists', function(assert) {
2121+
const loader = new PlaylistLoader('master.m3u8', this.fakeVhs);
2122+
let media =
2123+
'#EXTM3U\n' +
2124+
'#EXT-X-MEDIA-SEQUENCE:0\n' +
2125+
'#EXTINF:5,\n' +
2126+
'low-0.ts\n' +
2127+
'#EXTINF:5,\n' +
2128+
'low-1.ts\n';
2129+
2130+
loader.load();
2131+
2132+
this.requests.shift().respond(
2133+
200, null,
2134+
'#EXTM3U\n' +
2135+
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
2136+
'media.m3u8\n' +
2137+
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
2138+
'media2.m3u8\n'
2139+
);
2140+
2141+
this.requests.shift().respond(200, null, media);
2142+
2143+
assert.ok(loader.mediaUpdateTimeout, 'has an initial media update timeout');
2144+
2145+
this.clock.tick(5000);
2146+
2147+
media += '#EXTINF:5\nlow-2.ts\n';
2148+
2149+
this.requests.shift().respond(200, null, media);
2150+
2151+
assert.ok(loader.mediaUpdateTimeout, 'media update timeout created another');
2152+
2153+
loader.pause();
2154+
assert.notOk(loader.mediaUpdateTimeout, 'media update timeout cleared');
2155+
2156+
loader.media(loader.master.playlists[0]);
2157+
2158+
assert.ok(loader.mediaUpdateTimeout, 'media update timeout created again');
2159+
assert.equal(this.requests.length, 0, 'no request');
2160+
2161+
loader.media(loader.master.playlists[1]);
2162+
2163+
assert.ok(loader.mediaUpdateTimeout, 'media update timeout created');
2164+
assert.equal(this.requests.length, 1, 'playlist requested');
2165+
2166+
this.requests.shift().respond(500, null, 'fail');
2167+
2168+
assert.ok(loader.mediaUpdateTimeout, 'media update timeout exists after request failure');
2169+
2170+
this.clock.tick(5000);
2171+
2172+
assert.ok(loader.mediaUpdateTimeout, 'media update timeout created again');
2173+
assert.equal(this.requests.length, 1, 'playlist re-requested');
2174+
});
2175+
21042176
QUnit.module('llhls', {
21052177
beforeEach() {
21062178
this.fakeVhs.options_ = {experimentalLLHLS: true};

0 commit comments

Comments
 (0)