97988fc1 by David LaPalomento

Merge pull request #440 from videojs/blacklist

Blacklist
2 parents 56919c5f 3b1a5651
......@@ -110,7 +110,7 @@
var match, event;
//strip whitespace
line = line.replace(/^\s+|\s+$/g, '');
line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, '');
if (line.length === 0) {
// ignore empty lines
return;
......
......@@ -7,17 +7,18 @@
'use strict';
var
// a fudge factor to apply to advertised playlist bitrates to account for
// A fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.2,
blacklistDuration = 5 * 60 * 1000, // 2 minute blacklist
TIME_FUDGE_FACTOR = 1 / 60, // Fudge factor to account for TimeRanges rounding
Component = videojs.getComponent('Component'),
// the amount of time to wait between checking the state of the buffer
// The amount of time to wait between checking the state of the buffer
bufferCheckInterval = 500,
keyFailed,
resolveUrl,
TIME_FUDGE_FACTOR = 1 / 60;
resolveUrl;
// returns true if a key has failed to download within a certain amount of retries
keyFailed = function(key) {
......@@ -179,15 +180,7 @@ videojs.HlsHandler.prototype.src = function(src) {
}.bind(this));
this.playlists.on('error', function() {
// close the media source with the appropriate error type
if (this.playlists.error.code === 2) {
this.mediaSource.endOfStream('network');
} else if (this.playlists.error.code === 4) {
this.mediaSource.endOfStream('decode');
}
// if this error is unrecognized, pass it along to the tech
this.tech_.error(this.playlists.error);
this.blacklistCurrentPlaylist_(this.playlists.error);
}.bind(this));
this.playlists.on('loadedplaylist', function() {
......@@ -916,6 +909,36 @@ videojs.HlsHandler.prototype.setBandwidth = function(xhr) {
this.tech_.trigger('bandwidthupdate');
};
videojs.HlsHandler.prototype.blacklistCurrentPlaylist_ = function(error) {
var currentPlaylist, nextPlaylist;
currentPlaylist = this.playlists.media();
// If there is no current playlist, then an error occurred while we were
// trying to load the master OR while we were disposing of the tech
if (!currentPlaylist) {
this.error = error;
return this.mediaSource.endOfStream('network');
}
// Select a new playlist
nextPlaylist = this.selectPlaylist();
if (nextPlaylist) {
videojs.log.warn('Problem encountered with the current HLS playlist. Switching to another playlist.');
// Blacklist this playlist
currentPlaylist.excludeUntil = Date.now() + blacklistDuration;
return this.playlists.media(nextPlaylist);
} else {
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');
}
};
videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
var
self = this,
......@@ -930,7 +953,11 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
this.segmentXhr_ = videojs.Hls.xhr({
uri: segmentInfo.uri,
responseType: 'arraybuffer',
withCredentials: this.source_.withCredentials
withCredentials: this.source_.withCredentials,
// Set xhr timeout to 150% of the segment duration to allow us
// some time to switch renditions in the event of a catastrophic
// decrease in network performance or a server issue.
timeout: (segment.duration * 1.5) * 1000
}, function(error, request) {
// the segment request is no longer outstanding
self.segmentXhr_ = null;
......@@ -943,13 +970,12 @@ videojs.HlsHandler.prototype.loadSegment = function(segmentInfo) {
// otherwise, trigger a network error
if (!request.aborted && error) {
self.error = {
self.pendingSegment_ = null;
return self.blacklistCurrentPlaylist_({
status: request.status,
message: 'HLS segment request error at URL: ' + segmentInfo.uri,
code: (request.status >= 500) ? 4 : 2
};
return self.mediaSource.endOfStream('network');
});
}
// stop processing if the request was aborted
......@@ -1021,9 +1047,10 @@ videojs.HlsHandler.prototype.drainBuffer = function(event) {
// if the key download failed, we want to skip this segment
// but if the key hasn't downloaded yet, we want to try again later
if (keyFailed(segment.key)) {
videojs.log.warn('Network error retrieving key from "' +
segment.key.uri + '"');
return this.mediaSource.endOfStream('network');
return this.blacklistCurrentPlaylist_({
message: 'HLS segment key request error.',
code: 4
});
} else if (!segment.key.bytes) {
// waiting for the key bytes, try again later
......
......@@ -831,21 +831,6 @@ test('selects a playlist after segment downloads', function() {
strictEqual(calls, 3, 'selects after additional segments');
});
test('reports an error if a segment is unreachable', function() {
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.hls.bandwidth = 20000;
standardXHRResponse(requests[0]); // master
standardXHRResponse(requests[1]); // media
requests[2].respond(400); // segment
strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered');
});
test('updates the duration after switching playlists', function() {
var selectedPlaylist = false;
player.src({
......@@ -1510,32 +1495,23 @@ test('playlist 404 should end stream with a network error', function() {
equal(player.tech_.hls.mediaSource.error_, 'network', 'set a network error');
});
test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
test('segment 404 should trigger blacklisting of media', function () {
var media;
player.src({
src: 'manifest/media.m3u8',
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
requests[1].respond(404);
ok(player.tech_.hls.error.message, 'an error message is available');
equal(2, player.tech_.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
});
test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.tech_.hls.bandwidth = 20000;
standardXHRResponse(requests[0]); // master
standardXHRResponse(requests[1]); // media
openMediaSource(player);
media = player.tech_.hls.playlists.media_;
standardXHRResponse(requests[0]);
requests[1].respond(500);
ok(player.tech_.hls.error.message, 'an error message is available');
equal(4, player.tech_.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
requests[2].respond(400); // segment
ok(media.excludeUntil > 0, 'original media blacklisted for some time');
});
test('seeking in an empty playlist is a non-erroring noop', function() {
......@@ -2458,8 +2434,8 @@ test('retries key requests once upon failure', function() {
equal(requests.length, 2, 'gives up after one retry');
});
test('errors if key requests fail more than once', function() {
var bytes = [];
test('blacklists playlist if key requests fail more than once', function() {
var bytes = [], media;
player.src({
src: 'https://example.com/encrypted-media.m3u8',
......@@ -2479,14 +2455,16 @@ test('errors if key requests fail more than once', function() {
player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) {
bytes.push(chunk);
};
media = player.tech_.hls.playlists.media_;
standardXHRResponse(requests.pop()); // segment 1
requests.shift().respond(404); // fail key
requests.shift().respond(404); // fail key, again
player.tech_.hls.checkBuffer_();
equal(player.tech_.hls.mediaSource.error_,
'network',
'triggered a network error');
ok(media.excludeUntil > 0,
'playlist blacklisted');
});
test('the key is supplied to the decrypter in the correct format', function() {
......@@ -2624,8 +2602,9 @@ test('resolves relative key URLs against the playlist', function() {
equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL');
});
test('treats invalid keys as a key request failure', function() {
var bytes = [];
test('treats invalid keys as a key request failure and blacklists playlist', function() {
var bytes = [], media;
player.src({
src: 'https://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -2644,6 +2623,8 @@ test('treats invalid keys as a key request failure', function() {
player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) {
bytes.push(chunk);
};
media = player.tech_.hls.playlists.media_;
// segment request
standardXHRResponse(requests.pop());
// keys should be 16 bytes long
......@@ -2657,10 +2638,9 @@ test('treats invalid keys as a key request failure', function() {
requests.shift().respond(200, null, '');
player.tech_.hls.checkBuffer_();
// two failed attempts is a network error
equal(player.tech_.hls.mediaSource.error_,
'network',
'triggered a network error');
// two failed attempts is an error - blacklist this playlist
ok(media.excludeUntil > 0,
'blacklisted playlist');
});
test('live stream should not call endOfStream', function(){
......