Skip to content

Commit 44905d4

Browse files
evanfarinagkatsevbrandonocasey
authored
feat: allow clients to limit the number of times a playlist attempts to reload following an error (#1098)
Introduce a new option `maxPlaylistRetries`, which defaults to Infinity to match existing behavior. If set, this will cause a playlist to be excluded indefinitely when the threshold is crossed. When a playlist is requested successfully, the count is reset. If a stream has multiple playlists, all playlists will need to cross the max retries threshold for playback to completely fail. Co-authored-by: Evan farina <efarina@linkedin.com> Co-authored-by: Gary Katsevman <git@gkatsev.com> Co-authored-by: brandonocasey <brandonocasey@gmail.com>
1 parent 5405c18 commit 44905d4

8 files changed

Lines changed: 104 additions & 5 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Video.js Compatibility: 6.0, 7.0
4545
- [parse708captions](#parse708captions)
4646
- [overrideNative](#overridenative)
4747
- [blacklistDuration](#blacklistduration)
48+
- [maxPlaylistRetries](#maxplaylistretries)
4849
- [bandwidth](#bandwidth)
4950
- [useBandwidthFromLocalStorage](#usebandwidthfromlocalstorage)
5051
- [enableLowInitialPlaylist](#enablelowinitialplaylist)
@@ -362,6 +363,14 @@ if a playlist is blacklisted, it will be blacklisted for a period of that
362363
customized duration. This enables the blacklist duration to be configured
363364
by the user.
364365

366+
#### maxPlaylistRetries
367+
* Type: `number`
368+
* Default: `Infinity`
369+
* can be used as an initialization option
370+
371+
The max number of times that a playlist will retry loading following an error
372+
before being indefinitely excluded from the rendition selection algorithm. Note: the number of retry attempts needs to _exceed_ this value before a playlist will be excluded.
373+
365374
##### bandwidth
366375
* Type: `number`
367376
* can be used as an initialization option

src/manifest.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const forEachMediaGroup = (master, callback) => {
151151
*/
152152
export const setupMediaPlaylist = ({ playlist, uri, id }) => {
153153
playlist.id = id;
154+
playlist.playlistErrors_ = 0;
154155

155156
if (uri) {
156157
// For media playlists, m3u8-parser does not have access to a URI, as HLS media

src/master-playlist-controller.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
140140
bandwidth,
141141
externVhs,
142142
useCueTags,
143+
maxPlaylistRetries,
143144
blacklistDuration,
144145
enableLowInitialPlaylist,
145146
sourceType,
@@ -160,6 +161,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
160161
this.sourceType_ = sourceType;
161162
this.useCueTags_ = useCueTags;
162163
this.blacklistDuration = blacklistDuration;
164+
this.maxPlaylistRetries = maxPlaylistRetries;
163165
this.enableLowInitialPlaylist = enableLowInitialPlaylist;
164166
if (this.useCueTags_) {
165167
this.cueTagsTrack_ = this.tech_.addTextTrack(
@@ -172,6 +174,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
172174
this.requestOptions_ = {
173175
withCredentials,
174176
handleManifestRedirects,
177+
maxPlaylistRetries,
175178
timeout: null
176179
};
177180

@@ -1132,6 +1135,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
11321135
return;
11331136
}
11341137

1138+
currentPlaylist.playlistErrors_++;
1139+
11351140
const playlists = this.masterPlaylistLoader_.master.playlists;
11361141
const enabledPlaylists = playlists.filter(isEnabled);
11371142
const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === currentPlaylist;
@@ -1179,7 +1184,16 @@ export class MasterPlaylistController extends videojs.EventTarget {
11791184
}
11801185

11811186
// Blacklist this playlist
1182-
currentPlaylist.excludeUntil = Date.now() + (blacklistDuration * 1000);
1187+
let excludeUntil;
1188+
1189+
if (currentPlaylist.playlistErrors_ > this.maxPlaylistRetries) {
1190+
excludeUntil = Infinity;
1191+
} else {
1192+
excludeUntil = Date.now() + (blacklistDuration * 1000);
1193+
}
1194+
1195+
currentPlaylist.excludeUntil = excludeUntil;
1196+
11831197
if (error.reason) {
11841198
currentPlaylist.lastExcludeReason_ = error.reason;
11851199
}

src/segment-loader.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,9 +2177,7 @@ export default class SegmentLoader extends videojs.EventTarget {
21772177
`video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `);
21782178
this.error({
21792179
message: 'Quota exceeded error with append of a single segment of content',
2180-
// To prevent any possible repeated downloads for content we can't actually
2181-
// append, blacklist forever.
2182-
blacklistDuration: Infinity
2180+
excludeUntil: Infinity
21832181
});
21842182
this.trigger('error');
21852183
return;

test/manifest.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@ QUnit.module('manifest', function() {
7979
const expectedPlaylist0 = {
8080
attributes: {},
8181
resolvedUri: urlTo('uri-0'),
82+
playlistErrors_: 0,
8283
uri: 'uri-0',
8384
id: '0-uri-0'
8485
};
8586
const expectedPlaylist1 = {
8687
attributes: {},
8788
resolvedUri: urlTo('uri-1'),
89+
playlistErrors_: 0,
8890
uri: 'uri-1',
8991
id: '1-uri-1'
9092
};

test/master-playlist-controller.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5973,6 +5973,45 @@ QUnit.test('switch playlists if current playlist gets excluded and re-include if
59735973
this.env.log.warn.callCount = 0;
59745974
});
59755975

5976+
QUnit.test('Playlist is excluded indefinitely if number of playlistErrors_ exceeds maxPlaylistRetries', function(assert) {
5977+
this.requests.length = 0;
5978+
this.player.dispose();
5979+
this.player = createPlayer({ html5: { vhs: { maxPlaylistRetries: 1 } } });
5980+
this.player.src({
5981+
src: 'manifest/two-renditions.m3u8',
5982+
type: 'application/vnd.apple.mpegurl'
5983+
});
5984+
5985+
this.clock.tick(1);
5986+
5987+
this.masterPlaylistController = this.player.tech_.vhs.masterPlaylistController_;
5988+
5989+
// main
5990+
this.standardXHRResponse(this.requests.shift());
5991+
// media
5992+
this.standardXHRResponse(this.requests.shift());
5993+
5994+
const mpc = this.masterPlaylistController;
5995+
const mpl = mpc.masterPlaylistLoader_;
5996+
const playlist = mpl.master.playlists[0];
5997+
5998+
assert.equal(playlist.playlistErrors_, 0, 'playlistErrors_ starts at zero');
5999+
6000+
mpc.blacklistCurrentPlaylist();
6001+
6002+
assert.ok('excludeUntil' in playlist, 'playlist was excluded');
6003+
assert.equal(playlist.playlistErrors_, 1, 'we incremented playlistErrors_');
6004+
assert.notEqual(playlist.excludeUntil, Infinity, 'The playlist was not excluded indefinitely');
6005+
6006+
mpc.blacklistCurrentPlaylist();
6007+
6008+
assert.equal(playlist.playlistErrors_, 2, 'we incremented playlistErrors_');
6009+
assert.equal(playlist.excludeUntil, Infinity, 'The playlist was excluded indefinitely');
6010+
assert.equal(this.env.log.warn.callCount, 2, 'logged a warning each time a playlist was excluded');
6011+
6012+
this.env.log.warn.callCount = 0;
6013+
});
6014+
59766015
QUnit.test('should delay loading of new playlist if lastRequest was less than half target duration', function(assert) {
59776016
this.requests.length = 0;
59786017
this.player.dispose();

test/playlist-loader.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,42 @@ QUnit.module('Playlist Loader', function(hooks) {
20202020
assert.strictEqual(loadedMetadata, 1, 'still one loadedmetadata');
20212021
});
20222022

2023+
QUnit.test('playlistErrors_ are reset on a successful response', function(assert) {
2024+
const loader = new PlaylistLoader('manifest/master.m3u8', this.fakeVhs);
2025+
2026+
loader.load();
2027+
2028+
// master
2029+
this.requests.shift().respond(
2030+
200, null,
2031+
'#EXTM3U\n' +
2032+
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
2033+
'playlist/playlist.m3u8\n' +
2034+
'#EXT-X-STREAM-INF:PROGRAM-ID=2,BANDWIDTH=170\n' +
2035+
'playlist/playlist2.m3u8\n' +
2036+
'#EXT-X-ENDLIST\n'
2037+
);
2038+
2039+
loader.master.playlists[0].playlistErrors_ = 3;
2040+
2041+
// playlist
2042+
this.requests.shift().respond(404);
2043+
2044+
loader.media(loader.master.playlists[1]);
2045+
loader.media(loader.master.playlists[0]);
2046+
2047+
assert.equal(loader.master.playlists[0].playlistErrors_, 3, 'we have 3 playlistErrors_');
2048+
2049+
this.requests[1].respond(
2050+
200, null,
2051+
'#EXTM3U\n' +
2052+
'#EXT-X-MEDIA-SEQUENCE:0\n' +
2053+
'#EXTINF:10,\n' +
2054+
'0.ts\n'
2055+
);
2056+
assert.equal(loader.master.playlists[0].playlistErrors_, 0, 'playlistErrors_ resets to zero when a playlist sucessfully loads');
2057+
});
2058+
20232059
QUnit.test(
20242060
'does not misintrepret playlists missing newlines at the end',
20252061
function(assert) {

test/segment-loader.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4413,7 +4413,7 @@ QUnit.module('SegmentLoader', function(hooks) {
44134413
loader.error_,
44144414
{
44154415
message: 'Quota exceeded error with append of a single segment of content',
4416-
blacklistDuration: Infinity
4416+
excludeUntil: Infinity
44174417
},
44184418
'loader triggered and saved the error'
44194419
);

0 commit comments

Comments
 (0)