Browse Source

blacklist the playlist that has stopped being updated and never blacklist the final playlist available (#1039)

* blacklist the playlist that has stopped being updated

* pull out the playlistEnd calculation to a separate function, rewrite seekable and playlistEnd to reduce the amount of repeat code and add tests for playlistEnd

* handle the expired for VOD case, check for null on expired instead of checking the sync points and rename playlist update check function

* add event listener for playlist isn't a fresh update in playlist-loader

* never blacklist final available final rendition

* reset the loader state after error occurs to keep requesting the final rendition

* delay the requests for final rendition and get the segment loader start loading after errors
pull/6/head
zhuangs 8 years ago
committed by Garrett
parent
commit
aeceda31ca
  1. 78
      src/master-playlist-controller.js
  2. 30
      src/playlist-loader.js
  3. 71
      src/playlist.js
  4. 2
      src/sync-controller.js
  5. 55
      test/master-playlist-controller.test.js
  6. 2
      test/playlist-loader.test.js
  7. 77
      test/playlist.test.js
  8. 160
      test/videojs-contrib-hls.test.js

78
src/master-playlist-controller.js

@ -359,6 +359,13 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
this.updateDuration();
// If the player isn't paused, ensure that the segment loader is running,
// as it is possible that it was temporarily stopped while waiting for
// a playlist (e.g., in case the playlist errored and we re-requested it).
if (!this.tech_.paused()) {
this.mainSegmentLoader_.load();
}
if (!updatedPlaylist.endList) {
let addSeekableRange = () => {
let seekable = this.seekable();
@ -428,6 +435,23 @@ export class MasterPlaylistController extends videojs.EventTarget {
bubbles: true
});
});
this.masterPlaylistLoader_.on('playlistunchanged', () => {
let updatedPlaylist = this.masterPlaylistLoader_.media();
let playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist);
if (playlistOutdated) {
// Playlist has stopped updating and we're stuck at its end. Try to
// blacklist it and switch to another playlist in the hope that that
// one is updating (and give the player a chance to re-adjust to the
// safe live point).
this.blacklistCurrentPlaylist({
message: 'Playlist no longer updating.'
});
// useful for monitoring QoS
this.tech_.trigger('playliststuck');
}
});
}
/**
@ -795,6 +819,37 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.trigger('sourceopen');
}
/**
* Check if a playlist has stopped being updated
* @param {Object} playlist the media playlist object
* @return {boolean} whether the playlist has stopped being updated or not
*/
stuckAtPlaylistEnd_(playlist) {
let seekable = this.seekable();
if (!seekable.length) {
// playlist doesn't have enough information to determine whether we are stuck
return false;
}
// does not use the safe live end to calculate playlist end, since we
// don't want to say we are stuck while there is still content
let absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist);
let currentTime = this.tech_.currentTime();
let buffered = this.tech_.buffered();
if (!buffered.length) {
// return true if the playhead reached the absolute end of the playlist
return absolutePlaylistEnd - currentTime <= Ranges.TIME_FUDGE_FACTOR;
}
let bufferedEnd = buffered.end(buffered.length - 1);
// return true if there is too little buffer left and
// buffer has reached absolute end of playlist
return bufferedEnd - currentTime <= Ranges.TIME_FUDGE_FACTOR &&
absolutePlaylistEnd - bufferedEnd <= Ranges.TIME_FUDGE_FACTOR;
}
/**
* Blacklists a playlist when an error occurs for a set amount of time
* making it unavailable for selection by the rendition selection algorithm
@ -820,23 +875,26 @@ export class MasterPlaylistController extends videojs.EventTarget {
return this.mediaSource.endOfStream('network');
}
let isFinalRendition = this.masterPlaylistLoader_.isFinalRendition_();
if (isFinalRendition) {
// Never blacklisting this playlist because it's final rendition
videojs.log.warn('Problem encountered with the current ' +
'HLS playlist. Trying again since it is the final playlist.');
return this.masterPlaylistLoader_.load(isFinalRendition);
}
// Blacklist this playlist
currentPlaylist.excludeUntil = Date.now() + BLACKLIST_DURATION;
// Select a new playlist
nextPlaylist = this.selectPlaylist();
if (nextPlaylist) {
videojs.log.warn('Problem encountered with the current ' +
'HLS playlist. Switching to another playlist.');
videojs.log.warn('Problem encountered with the current HLS playlist.' +
(error.message ? ' ' + error.message : '') +
' Switching to another playlist.');
return this.masterPlaylistLoader_.media(nextPlaylist);
}
videojs.log.warn('Problem encountered with the current ' +
'HLS playlist. No suitable alternatives found.');
// We have no more playlists we can select so we must fail
this.error = error;
return this.mediaSource.endOfStream('network');
return this.masterPlaylistLoader_.media(nextPlaylist);
}
/**

30
src/playlist-loader.js

@ -109,7 +109,7 @@ const updateMaster = function(master, media) {
};
/**
* Load a playlist from a remote loacation
* Load a playlist from a remote location
*
* @class PlaylistLoader
* @extends Stream
@ -186,6 +186,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
refreshDelay /= 2;
loader.trigger('playlistunchanged');
}
// refresh live playlists after a target duration passes
@ -264,6 +265,15 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
}).length === 0);
};
/**
* Returns whether the current playlist is the final available rendition
*
* @return {Boolean} true if on final rendition
*/
loader.isFinalRendition_ = function() {
return (loader.master.playlists.filter(isEnabled).length === 1);
};
/**
* When called without any arguments, returns the currently
* active media playlist. When called with a single argument,
@ -272,7 +282,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
* loader is in the HAVE_NOTHING causes an error to be emitted
* but otherwise has no effect.
*
* @param {Object=} playlis tthe parsed media playlist
* @param {Object=} playlist the parsed media playlist
* object to switch to
* @return {Playlist} the current loaded media
*/
@ -392,7 +402,7 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
}
if (error) {
return playlistRequestError(request, loader.media().uri);
return playlistRequestError(request, loader.media().uri, 'HAVE_METADATA');
}
haveMetadata(request, loader.media().uri);
});
@ -421,9 +431,16 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
/**
* start loading of the playlist
*/
loader.load = () => {
loader.load = (isFinalRendition) => {
window.clearTimeout(mediaUpdateTimeout);
if (isFinalRendition) {
let refreshDelay = loader.media() ? (loader.media().targetDuration / 2) * 1000 : 5 * 1000;
mediaUpdateTimeout = window.setTimeout(loader.load.bind(null, false), refreshDelay);
return;
}
if (loader.started) {
if (!loader.media().endList) {
if (loader.media() && !loader.media().endList) {
loader.trigger('mediaupdatetimeout');
} else {
loader.trigger('loadedplaylist');
@ -464,6 +481,9 @@ const PlaylistLoader = function(srcUrl, hls, withCredentials) {
// MEDIA_ERR_NETWORK
code: 2
};
if (loader.state === 'HAVE_NOTHING') {
loader.started = false;
}
return loader.trigger('error');
}

71
src/playlist.js

@ -235,9 +235,7 @@ const getPlaylistSyncPoints = function(playlist) {
if (!playlist || !playlist.segments) {
return [null, null];
}
let expiredSync = playlist.syncInfo || null;
let expiredSync = playlist.syncInfo || (playlist.endList ? { time: 0, mediaSequence: 0} : null);
let segmentSync = null;
// Find the first segment with timing information
@ -268,10 +266,12 @@ const getPlaylistSyncPoints = function(playlist) {
* @returns {Number} the amount of time expired from the playlist
* @function calculateExpiredTime
*/
const calculateExpiredTime = function(playlist, expiredSync, segmentSync) {
const calculateExpiredTime = function(playlist) {
// If we have both an expired sync point and a segment sync point
// determine which sync point is closest to the start of the playlist
// so the minimal amount of timing estimation is done.
let { expiredSync, segmentSync } = getPlaylistSyncPoints(playlist);
if (expiredSync && segmentSync) {
let expiredDiff = expiredSync.mediaSequence - playlist.mediaSequence;
let segmentDiff = segmentSync.mediaSequence - playlist.mediaSequence;
@ -305,6 +305,38 @@ const calculateExpiredTime = function(playlist, expiredSync, segmentSync) {
return segmentSync.time - sumDurations(playlist, syncIndex, 0);
}
return null;
};
/**
* Calculates the playlist end time
*
* @param {Object} playlist a media playlist object
* @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the playlist
* end calculation should consider the safe live end (truncate the playlist
* end by three segments). This is normally used for calculating the end of
* the playlist's seekable range.
* @returns {Number} the end time of playlist
* @function playlistEnd
*/
export const playlistEnd = function(playlist, useSafeLiveEnd) {
if (!playlist || !playlist.segments) {
return null;
}
if (playlist.endList) {
return duration(playlist);
}
let expired = calculateExpiredTime(playlist);
if (expired === null) {
return null;
}
let endSequence = useSafeLiveEnd ? Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS) :
Math.max(0, playlist.segments.length);
return intervalDuration(playlist,
playlist.mediaSequence + endSequence,
expired);
};
/**
@ -321,34 +353,14 @@ const calculateExpiredTime = function(playlist, expiredSync, segmentSync) {
* for seeking
*/
export const seekable = function(playlist) {
// without segments, there are no seekable ranges
if (!playlist || !playlist.segments) {
return createTimeRange();
}
// when the playlist is complete, the entire duration is seekable
if (playlist.endList) {
return createTimeRange(0, duration(playlist));
}
let { expiredSync, segmentSync } = getPlaylistSyncPoints(playlist);
let useSafeLiveEnd = true;
let seekableStart = calculateExpiredTime(playlist);
let seekableEnd = playlistEnd(playlist, useSafeLiveEnd);
// We have no sync information for this playlist so we can't create a seekable range
if (!expiredSync && !segmentSync) {
if (seekableEnd === null) {
return createTimeRange();
}
let expired = calculateExpiredTime(playlist, expiredSync, segmentSync);
// live playlists should not expose three segment durations worth
// of content from the end of the playlist
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
let start = expired;
let endSequence = Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS);
let end = intervalDuration(playlist,
playlist.mediaSequence + endSequence,
expired);
return createTimeRange(start, end);
return createTimeRange(seekableStart, seekableEnd);
};
const isWholeNumber = function(num) {
@ -483,6 +495,7 @@ Playlist.seekable = seekable;
Playlist.getMediaInfoForTime_ = getMediaInfoForTime_;
Playlist.isEnabled = isEnabled;
Playlist.isBlacklisted = isBlacklisted;
Playlist.playlistEnd = playlistEnd;
// exports
export default Playlist;

2
src/sync-controller.js

@ -224,7 +224,7 @@ export default class SyncController extends videojs.EventTarget {
for (let i = mediaSequenceDiff - 1; i >= 0; i--) {
let lastRemovedSegment = oldPlaylist.segments[i];
if (typeof lastRemovedSegment.start !== 'undefined') {
if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') {
newPlaylist.syncInfo = {
mediaSequence: oldPlaylist.mediaSequence + i,
time: lastRemovedSegment.start

55
test/master-playlist-controller.test.js

@ -377,6 +377,61 @@ QUnit.test('updates the enabled track when switching audio groups', function(ass
'enabled a track in the new audio group');
});
QUnit.test('detects if the player is stuck at the playlist end', function(assert) {
let playlistCopy = Hls.Playlist.playlistEnd;
this.masterPlaylistController.mediaSource.trigger('sourceopen');
this.standardXHRResponse(this.requests.shift());
let playlist = this.player.tech_.hls.selectPlaylist();
// not stuck at playlist end when no seekable, even if empty buffer
// and positive currentTime
this.masterPlaylistController.seekable = () => videojs.createTimeRange();
this.player.tech_.buffered = () => videojs.createTimeRange();
this.player.tech_.setCurrentTime(170);
assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'not stuck at playlist end');
// not stuck at playlist end when no seekable, even if empty buffer
// and currentTime 0
this.player.tech_.setCurrentTime(0);
assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'not stuck at playlist end');
// not stuck at playlist end when no seekable but current time is at
// the end of the buffered range
this.player.tech_.buffered = () => videojs.createTimeRange(0, 170);
assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'not stuck at playlist end');
// not stuck at playlist end when currentTime not at seekable end
// even if the buffer is empty
this.masterPlaylistController.seekable = () => videojs.createTimeRange(0, 130);
this.player.tech_.setCurrentTime(50);
this.player.tech_.buffered = () => videojs.createTimeRange();
Hls.Playlist.playlistEnd = () => 130;
assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'not stuck at playlist end');
// not stuck at playlist end when buffer reached the absolute end of the playlist
// and current time is in the buffered range
this.player.tech_.setCurrentTime(159);
this.player.tech_.buffered = () => videojs.createTimeRange(0, 160);
Hls.Playlist.playlistEnd = () => 160;
assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'not stuck at playlist end');
// stuck at playlist end when there is no buffer and playhead
// reached absolute end of playlist
this.player.tech_.setCurrentTime(160);
assert.ok(this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'stuck at playlist end');
// stuck at playlist end when current time reached the buffer end
// and buffer has reached absolute end of playlist
this.masterPlaylistController.seekable = () => videojs.createTimeRange(90, 130);
this.player.tech_.buffered = () => videojs.createTimeRange(0, 170);
this.player.tech_.setCurrentTime(170);
Hls.Playlist.playlistEnd = () => 170;
assert.ok(this.masterPlaylistController.stuckAtPlaylistEnd_(playlist), 'stuck at playlist end');
Hls.Playlist.playlistEnd = playlistCopy;
});
QUnit.test('blacklists switching from video+audio playlists to audio only', function(assert) {
let audioPlaylist;

2
test/playlist-loader.test.js

@ -691,7 +691,7 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function(assert
assert.strictEqual(this.requests[0].aborted, true, 'aborted refresh request');
assert.ok(!this.requests[0].onreadystatechange,
'onreadystatechange handlers should be removed on abort');
assert.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is set accoring to the startingState');
});
QUnit.test('switching to the active playlist is a no-op', function(assert) {

77
test/playlist.test.js

@ -273,7 +273,7 @@ QUnit.test('a non-positive length interval has zero duration', function(assert)
QUnit.module('Playlist Seekable');
QUnit.test('calculates seekable time ranges from the available segments', function(assert) {
QUnit.test('calculates seekable time ranges from available segments', function(assert) {
let playlist = {
mediaSequence: 0,
segments: [{
@ -292,16 +292,35 @@ QUnit.test('calculates seekable time ranges from the available segments', functi
assert.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
});
QUnit.test('master playlists have empty seekable ranges', function(assert) {
let seekable = Playlist.seekable({
QUnit.test('calculates playlist end time from the available segments', function(assert) {
let playlistEnd = Playlist.playlistEnd({
mediaSequence: 0,
segments: [{
duration: 10,
uri: '0.ts'
}, {
duration: 10,
uri: '1.ts'
}],
endList: true
});
assert.equal(playlistEnd, 20, 'paylist end at the duration');
});
QUnit.test('master playlists have empty seekable ranges and no playlist end', function(assert) {
let playlist = {
playlists: [{
uri: 'low.m3u8'
}, {
uri: 'high.m3u8'
}]
});
};
let seekable = Playlist.seekable(playlist);
let playlistEnd = Playlist.playlistEnd(playlist);
assert.equal(seekable.length, 0, 'no seekable ranges from a master playlist');
assert.equal(playlistEnd, null, 'no playlist end from a master playlist');
});
QUnit.test('seekable end is three target durations from the actual end of live playlists',
@ -333,8 +352,8 @@ function(assert) {
assert.equal(seekable.end(0), 7, 'ends three target durations from the last segment');
});
QUnit.test('seekable end accounts for non-standard target durations', function(assert) {
let seekable = Playlist.seekable({
QUnit.test('seekable end and playlist end account for non-standard target durations', function(assert) {
let playlist = {
targetDuration: 2,
mediaSequence: 0,
syncInfo: {
@ -357,16 +376,19 @@ QUnit.test('seekable end accounts for non-standard target durations', function(a
duration: 2,
uri: '4.ts'
}]
});
};
let seekable = Playlist.seekable(playlist);
let playlistEnd = Playlist.playlistEnd(playlist);
assert.equal(seekable.start(0), 0, 'starts at the earliest available segment');
assert.equal(seekable.end(0),
9 - (2 + 2 + 1),
'allows seeking no further than three segments from the end');
assert.equal(playlistEnd, 9, 'playlist end at the last segment');
});
QUnit.test('playlist with no sync points has empty seekable range', function(assert) {
let seekable = Playlist.seekable({
QUnit.test('playlist with no sync points has empty seekable range and empty playlist end', function(assert) {
let playlist = {
targetDuration: 10,
mediaSequence: 0,
segments: [{
@ -382,16 +404,17 @@ QUnit.test('playlist with no sync points has empty seekable range', function(ass
duration: 10,
uri: '3.ts'
}]
});
};
let seekable = Playlist.seekable(playlist);
let playlistEnd = Playlist.playlistEnd(playlist);
assert.equal(seekable.length, 0, 'no seekable range for playlist with no sync points');
assert.equal(playlistEnd, null, 'no playlist end for playlist with no sync points');
});
QUnit.test('seekable uses available sync points for calculating seekable range',
QUnit.test('seekable and playlistEnd use available sync points for calculating',
function(assert) {
let seekable;
seekable = Playlist.seekable({
let playlist = {
targetDuration: 10,
mediaSequence: 100,
syncInfo: {
@ -420,13 +443,16 @@ QUnit.test('seekable uses available sync points for calculating seekable range',
uri: '4.ts'
}
]
});
};
let seekable = Playlist.seekable(playlist);
let playlistEnd = Playlist.playlistEnd(playlist);
assert.ok(seekable.length, 'seekable range calculated');
assert.equal(seekable.start(0), 100, 'estimated start time based on expired sync point');
assert.equal(seekable.end(0), 120, 'allows seeking no further than three segments from the end');
assert.equal(playlistEnd, 150, 'playlist end at the last segment end');
seekable = Playlist.seekable({
playlist = {
targetDuration: 10,
mediaSequence: 100,
segments: [
@ -453,13 +479,16 @@ QUnit.test('seekable uses available sync points for calculating seekable range',
uri: '4.ts'
}
]
});
};
seekable = Playlist.seekable(playlist);
playlistEnd = Playlist.playlistEnd(playlist);
assert.ok(seekable.length, 'seekable range calculated');
assert.equal(seekable.start(0), 98.5, 'estimated start time using segmentSync');
assert.equal(seekable.end(0), 118.4, 'allows seeking no further than three segments from the end');
assert.equal(playlistEnd, 148.4, 'playlist end at the last segment end');
seekable = Playlist.seekable({
playlist = {
targetDuration: 10,
mediaSequence: 100,
syncInfo: {
@ -490,13 +519,16 @@ QUnit.test('seekable uses available sync points for calculating seekable range',
uri: '4.ts'
}
]
});
};
seekable = Playlist.seekable(playlist);
playlistEnd = Playlist.playlistEnd(playlist);
assert.ok(seekable.length, 'seekable range calculated');
assert.equal(seekable.start(0), 98.5, 'estimated start time using nearest sync point (segmentSync in this case)');
assert.equal(seekable.end(0), 118.5, 'allows seeking no further than three segments from the end');
assert.equal(playlistEnd, 148.5, 'playlist end at the last segment end');
seekable = Playlist.seekable({
playlist = {
targetDuration: 10,
mediaSequence: 100,
syncInfo: {
@ -527,11 +559,14 @@ QUnit.test('seekable uses available sync points for calculating seekable range',
uri: '4.ts'
}
]
});
};
seekable = Playlist.seekable(playlist);
playlistEnd = Playlist.playlistEnd(playlist);
assert.ok(seekable.length, 'seekable range calculated');
assert.equal(seekable.start(0), 100.8, 'estimated start time using nearest sync point (expiredSync in this case)');
assert.equal(seekable.end(0), 118.5, 'allows seeking no further than three segments from the end');
assert.equal(playlistEnd, 148.5, 'playlist end at the last segment end');
});
QUnit.module('Playlist Media Index For Time', {

160
test/videojs-contrib-hls.test.js

@ -1028,6 +1028,9 @@ QUnit.test('playlist 404 should end stream with a network error', function(asser
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
assert.ok(!this.player.tech_.hls.mediaSource.error_, 'no media source error');
this.requests.pop().respond(404);
assert.equal(this.player.tech_.hls.mediaSource.error_, 'network', 'set a network error');
@ -1077,24 +1080,153 @@ QUnit.test('playlist 404 should blacklist media', function(assert) {
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n');
assert.equal(typeof this.player.tech_.hls.playlists.media_,
'undefined',
'no media is initially set');
// media
this.requests[1].respond(400);
this.requests[1].respond(404);
url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
media = this.player.tech_.hls.playlists.master.playlists[url];
assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(this.env.log.warn.args[0],
'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
'log generic error message');
// request for the final available media
this.requests[2].respond(404);
url = this.requests[2].url.slice(this.requests[2].url.lastIndexOf('/') + 1);
media = this.player.tech_.hls.playlists.master.playlists[url];
// media wasn't blacklisted because it's final rendition
assert.ok(!media.excludeUntil, 'media not blacklisted after playlist 404');
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(this.env.log.warn.args[1],
'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
'log specific error message for final playlist');
this.clock.tick(2 * 1000);
// no new request was made since it hasn't been half the segment duration
assert.strictEqual(3, this.requests.length, 'no new request was made');
this.clock.tick(3 * 1000);
// continue loading the final remaining playlist after it wasn't blacklisted
// when half the segment duaration passed
assert.strictEqual(4, this.requests.length, 'one more request was made');
assert.strictEqual(this.requests[3].url,
absoluteUrl('manifest/media1.m3u8'),
'media playlist requested');
// verify stats
assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
});
QUnit.test('blacklists playlist if it has stopped being updated', function(assert) {
let playliststuck = 0;
this.player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.player.tech_.triggerReady();
this.standardXHRResponse(this.requests.shift());
this.player.tech_.hls.masterPlaylistController_.seekable = function() {
return videojs.createTimeRange(90, 130);
};
this.player.tech_.setCurrentTime(170);
this.player.tech_.buffered = function() {
return videojs.createTimeRange(0, 170);
};
Hls.Playlist.playlistEnd = function() {
return 170;
};
this.player.tech_.on('playliststuck', () => playliststuck++);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:16\n' +
'#EXTINF:10,\n' +
'16.ts\n');
assert.ok(!this.player.tech_.hls.playlists.media().excludeUntil, 'playlist was not blacklisted');
assert.equal(this.env.log.warn.calls, 0, 'no warning logged for blacklist');
assert.equal(playliststuck, 0, 'there is no stuck playlist');
this.player.tech_.trigger('play');
this.player.tech_.trigger('playing');
// trigger a refresh
this.clock.tick(10 * 1000);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:16\n' +
'#EXTINF:10,\n' +
'16.ts\n');
assert.ok(this.player.tech_.hls.playlists.media().excludeUntil > 0, 'playlist blacklisted for some time');
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(this.env.log.warn.args[0],
'Problem encountered with the current HLS playlist. Playlist no longer updating. Switching to another playlist.',
'log specific error message for not updated playlist');
assert.equal(playliststuck, 1, 'there is one stuck playlist');
});
QUnit.test('never blacklist the playlist if it is the only playlist', function(assert) {
let media;
this.player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.clock.tick(10 * 1000);
this.requests.shift().respond(404);
media = this.player.tech_.hls.playlists.media();
// media wasn't blacklisted because it's final rendition
assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(this.env.log.warn.args[0],
'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
'log specific error message for final playlist');
});
QUnit.test('error on the first playlist request does not trigger an error ' +
'when there is master playlist with only one media playlist', function(assert) {
this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
this.requests[0]
.respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
'media.m3u8\n');
this.requests[1].respond(404);
let url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
let media = this.player.tech_.hls.playlists.master.playlists[url];
// media wasn't blacklisted because it's final rendition
assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
assert.equal(this.env.log.warn.args[0],
'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
'log specific error message for final playlist');
});
QUnit.test('seeking in an empty playlist is a non-erroring noop', function(assert) {
let requestsLength;
@ -2762,11 +2894,18 @@ QUnit.module('HLS - Encryption', {
QUnit.test('blacklists playlist if key requests fail', function(assert) {
let hls = HlsSourceHandler('html5').handleSource({
src: 'manifest/encrypted-media.m3u8',
src: 'manifest/encrypted-master.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
hls.mediaSource.trigger('sourceopen');
this.requests.shift()
.respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n');
this.requests.shift()
.respond(200, null,
'#EXTM3U\n' +
@ -2795,11 +2934,18 @@ QUnit.test('blacklists playlist if key requests fail', function(assert) {
QUnit.test('treats invalid keys as a key request failure and blacklists playlist', function(assert) {
let hls = HlsSourceHandler('html5').handleSource({
src: 'manifest/encrypted-media.m3u8',
src: 'manifest/encrypted-master.m3u8',
type: 'application/vnd.apple.mpegurl'
}, this.tech);
hls.mediaSource.trigger('sourceopen');
this.requests.shift()
.respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n');
this.requests.shift()
.respond(200, null,
'#EXTM3U\n' +

Loading…
Cancel
Save