Skip to content

Commit fe3b378

Browse files
brandonocaseygkatsev
authored andcommitted
fix: clear the blacklist for other playlists if final rendition errors (#479)
Ignore unsupported renditions (those with an excludeUntil of Infinity) This time use a separate timer for the final rendition. Second try at #396. Reverts #471.
1 parent faf1198 commit fe3b378

File tree

3 files changed

+110
-29
lines changed

3 files changed

+110
-29
lines changed

src/master-playlist-controller.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -789,15 +789,35 @@ export class MasterPlaylistController extends videojs.EventTarget {
789789

790790
let isFinalRendition =
791791
this.masterPlaylistLoader_.master.playlists.filter(isEnabled).length === 1;
792+
let playlists = this.masterPlaylistLoader_.master.playlists;
792793

793-
if (isFinalRendition) {
794-
// Never blacklisting this playlist because it's final rendition
794+
if (playlists.length === 1) {
795+
// Never blacklisting this playlist because it's the only playlist
795796
videojs.log.warn('Problem encountered with the current ' +
796-
'HLS playlist. Trying again since it is the final playlist.');
797+
'HLS playlist. Trying again since it is the only playlist.');
797798

798799
this.tech_.trigger('retryplaylist');
799800
return this.masterPlaylistLoader_.load(isFinalRendition);
800801
}
802+
803+
if (isFinalRendition) {
804+
// Since we're on the final non-blacklisted playlist, and we're about to blacklist
805+
// it, instead of erring the player or retrying this playlist, clear out the current
806+
// blacklist. This allows other playlists to be attempted in case any have been
807+
// fixed.
808+
videojs.log.warn('Removing all playlists from the blacklist because the last ' +
809+
'rendition is about to be blacklisted.');
810+
playlists.forEach((playlist) => {
811+
if (playlist.excludeUntil !== Infinity) {
812+
delete playlist.excludeUntil;
813+
}
814+
});
815+
// Technically we are retrying a playlist, in that we are simply retrying a previous
816+
// playlist. This is needed for users relying on the retryplaylist event to catch a
817+
// case where the player might be stuck and looping through "dead" playlists.
818+
this.tech_.trigger('retryplaylist');
819+
}
820+
801821
// Blacklist this playlist
802822
currentPlaylist.excludeUntil = Date.now() + (blacklistDuration * 1000);
803823
this.tech_.trigger('blacklistplaylist');
@@ -809,7 +829,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
809829
(error.message ? ' ' + error.message : '') +
810830
' Switching to another playlist.');
811831

812-
return this.masterPlaylistLoader_.media(nextPlaylist);
832+
return this.masterPlaylistLoader_.media(nextPlaylist, isFinalRendition);
813833
}
814834

815835
/**

src/playlist-loader.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export default class PlaylistLoader extends EventTarget {
258258
this.error = {
259259
playlist: this.master.playlists[url],
260260
status: xhr.status,
261-
message: 'HLS playlist request error at URL: ' + url,
261+
message: `HLS playlist request error at URL: ${url}.`,
262262
responseText: xhr.responseText,
263263
code: (xhr.status >= 500) ? 4 : 2
264264
};
@@ -317,6 +317,7 @@ export default class PlaylistLoader extends EventTarget {
317317
dispose() {
318318
this.stopRequest();
319319
window.clearTimeout(this.mediaUpdateTimeout);
320+
window.clearTimeout(this.finalRenditionTimeout);
320321
}
321322

322323
stopRequest() {
@@ -339,9 +340,11 @@ export default class PlaylistLoader extends EventTarget {
339340
*
340341
* @param {Object=} playlist the parsed media playlist
341342
* object to switch to
343+
* @param {Boolean=} is this the last available playlist
344+
*
342345
* @return {Playlist} the current loaded media
343346
*/
344-
media(playlist) {
347+
media(playlist, isFinalRendition) {
345348
// getter
346349
if (!playlist) {
347350
return this.media_;
@@ -352,8 +355,6 @@ export default class PlaylistLoader extends EventTarget {
352355
throw new Error('Cannot switch media playlist from ' + this.state);
353356
}
354357

355-
const startingState = this.state;
356-
357358
// find the playlist object if the target playlist has been
358359
// specified by URI
359360
if (typeof playlist === 'string') {
@@ -363,6 +364,17 @@ export default class PlaylistLoader extends EventTarget {
363364
playlist = this.master.playlists[playlist];
364365
}
365366

367+
window.clearTimeout(this.finalRenditionTimeout);
368+
369+
if (isFinalRendition) {
370+
const delay = (playlist.targetDuration / 2) * 1000 || 5 * 1000;
371+
372+
this.finalRenditionTimeout =
373+
window.setTimeout(this.media.bind(this, playlist, false), delay);
374+
return;
375+
}
376+
377+
const startingState = this.state;
366378
const mediaChange = !this.media_ || playlist.uri !== this.media_.uri;
367379

368380
// switch to fully loaded playlists immediately
@@ -509,7 +521,7 @@ export default class PlaylistLoader extends EventTarget {
509521
if (error) {
510522
this.error = {
511523
status: req.status,
512-
message: 'HLS playlist request error at URL: ' + this.srcUrl,
524+
message: `HLS playlist request error at URL: ${this.srcUrl}.`,
513525
responseText: req.responseText,
514526
// MEDIA_ERR_NETWORK
515527
code: 2

test/videojs-http-streaming.test.js

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ function(assert) {
570570

571571
assert.equal(error.code, 2, 'error has correct code');
572572
assert.equal(error.message,
573-
'HLS playlist request error at URL: manifest/master.m3u8',
573+
'HLS playlist request error at URL: manifest/master.m3u8.',
574574
'error has correct message');
575575
assert.equal(errLogs.length, 1, 'logged an error');
576576

@@ -1237,6 +1237,45 @@ QUnit.test('segment 404 should trigger blacklisting of media', function(assert)
12371237
assert.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
12381238
});
12391239

1240+
QUnit.test('unsupported playlist should not be re-included when excluding last playlist', function(assert) {
1241+
this.player.src({
1242+
src: 'manifest/master.m3u8',
1243+
type: 'application/vnd.apple.mpegurl'
1244+
});
1245+
1246+
this.clock.tick(1);
1247+
1248+
openMediaSource(this.player, this.clock);
1249+
1250+
this.player.tech_.hls.bandwidth = 1;
1251+
// master
1252+
this.requests.shift()
1253+
.respond(200, null,
1254+
'#EXTM3U\n' +
1255+
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.2"\n' +
1256+
'media.m3u8\n' +
1257+
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,not-an-audio-codec"\n' +
1258+
'media1.m3u8\n');
1259+
// media
1260+
this.standardXHRResponse(this.requests.shift());
1261+
1262+
let master = this.player.tech_.hls.playlists.master;
1263+
let media = this.player.tech_.hls.playlists.media_;
1264+
1265+
// segment
1266+
this.requests.shift().respond(400);
1267+
1268+
assert.ok(master.playlists[0].excludeUntil > 0, 'original media excluded for some time');
1269+
assert.strictEqual(master.playlists[1].excludeUntil,
1270+
Infinity,
1271+
'blacklisted invalid audio codec');
1272+
1273+
assert.equal(this.env.log.warn.calls, 2, 'warning logged for blacklist');
1274+
assert.equal(this.env.log.warn.args[0],
1275+
'Removing all playlists from the blacklist because the last rendition is about to be blacklisted.',
1276+
'log generic error message');
1277+
});
1278+
12401279
QUnit.test('playlist 404 should blacklist media', function(assert) {
12411280
let media;
12421281
let url;
@@ -1281,7 +1320,7 @@ QUnit.test('playlist 404 should blacklist media', function(assert) {
12811320
assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
12821321
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
12831322
assert.equal(this.env.log.warn.args[0],
1284-
'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
1323+
'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8. Switching to another playlist.',
12851324
'log generic error message');
12861325
assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
12871326
assert.equal(hlsRenditionBlacklistedEvents, 1, 'an hls-rendition-blacklisted event was fired');
@@ -1292,27 +1331,36 @@ QUnit.test('playlist 404 should blacklist media', function(assert) {
12921331
url = this.requests[2].url.slice(this.requests[2].url.lastIndexOf('/') + 1);
12931332
media = this.player.tech_.hls.playlists.master.playlists[url];
12941333

1295-
// media wasn't blacklisted because it's final rendition
1296-
assert.ok(!media.excludeUntil, 'media not blacklisted after playlist 404');
1297-
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1334+
assert.ok(media.excludeUntil > 0, 'second media was blacklisted after playlist 404');
1335+
assert.equal(this.env.log.warn.calls, 2, 'warning logged for blacklist');
12981336
assert.equal(this.env.log.warn.args[1],
1299-
'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
1300-
'log specific error message for final playlist');
1301-
assert.equal(retryplaylist, 1, 'retried final playlist for once');
1302-
assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
1303-
assert.equal(hlsRenditionBlacklistedEvents, 1, 'an hls-rendition-blacklisted event was fired');
1337+
'Removing all playlists from the blacklist because the last rendition is about to be blacklisted.',
1338+
'log generic error message');
1339+
assert.equal(this.env.log.warn.args[2],
1340+
'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media1.m3u8. ' +
1341+
'Switching to another playlist.',
1342+
'log generic error message');
1343+
assert.equal(retryplaylist, 1, 'fired a retryplaylist event');
1344+
assert.equal(blacklistplaylist, 2, 'media1 is blacklisted');
13041345

13051346
this.clock.tick(2 * 1000);
13061347
// no new request was made since it hasn't been half the segment duration
13071348
assert.strictEqual(3, this.requests.length, 'no new request was made');
13081349

13091350
this.clock.tick(3 * 1000);
1310-
// continue loading the final remaining playlist after it wasn't blacklisted
1351+
// loading the first playlist since the blacklist duration was cleared
13111352
// when half the segment duaration passed
1353+
13121354
assert.strictEqual(4, this.requests.length, 'one more request was made');
13131355

1356+
url = this.requests[3].url.slice(this.requests[3].url.lastIndexOf('/') + 1);
1357+
media = this.player.tech_.hls.playlists.master.playlists[url];
1358+
1359+
// the first media was unblacklisted after a refresh delay
1360+
assert.ok(!media.excludeUntil, 'removed first media from blacklist');
1361+
13141362
assert.strictEqual(this.requests[3].url,
1315-
absoluteUrl('manifest/media1.m3u8'),
1363+
absoluteUrl('manifest/media.m3u8'),
13161364
'media playlist requested');
13171365

13181366
// verify stats
@@ -1390,11 +1438,11 @@ QUnit.test('never blacklist the playlist if it is the only playlist', function(a
13901438
this.requests.shift().respond(404);
13911439
media = this.player.tech_.hls.playlists.media();
13921440

1393-
// media wasn't blacklisted because it's final rendition
1441+
// media wasn't blacklisted because it's the only rendition
13941442
assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
13951443
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
13961444
assert.equal(this.env.log.warn.args[0],
1397-
'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
1445+
'Problem encountered with the current HLS playlist. Trying again since it is the only playlist.',
13981446
'log specific error message for final playlist');
13991447
});
14001448

@@ -1417,12 +1465,12 @@ QUnit.test('error on the first playlist request does not trigger an error ' +
14171465
let url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
14181466
let media = this.player.tech_.hls.playlists.master.playlists[url];
14191467

1420-
// media wasn't blacklisted because it's final rendition
1468+
// media wasn't blacklisted because it's only rendition
14211469
assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
14221470
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
14231471
assert.equal(this.env.log.warn.args[0],
1424-
'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
1425-
'log specific error message for final playlist');
1472+
'Problem encountered with the current HLS playlist. Trying again since it is the only playlist.',
1473+
'log specific error message for the only playlist');
14261474
});
14271475

14281476
QUnit.test('seeking in an empty playlist is a non-erroring noop', function(assert) {
@@ -1797,15 +1845,16 @@ QUnit.test('playlist blacklisting duration is set through options', function(ass
17971845
assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
17981846
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
17991847
assert.equal(this.env.log.warn.args[0],
1800-
'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
1848+
'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8. Switching to another playlist.',
18011849
'log generic error message');
1850+
// this takes one millisecond
1851+
this.standardXHRResponse(this.requests[2]);
18021852

1803-
this.clock.tick(2 * 60 * 1000);
1853+
this.clock.tick(2 * 60 * 1000 - 1);
18041854
assert.ok(media.excludeUntil - Date.now() > 0, 'original media still be blacklisted');
18051855

18061856
this.clock.tick(1 * 60 * 1000);
18071857
assert.equal(media.excludeUntil, Date.now(), 'media\'s exclude time reach to the current time');
1808-
assert.equal(this.env.log.warn.calls, 3, 'warning logged for blacklist');
18091858

18101859
videojs.options.hls = hlsOptions;
18111860
});

0 commit comments

Comments
 (0)