3b8970dc by Gary Katsevman Committed by David LaPalomento

Add fetching keys mechanism and tests.

playistloaded kicks off the key fetching mechanism for the whole
playlist.
If keys cannot be downloaded after two retries, the segment is skipped.
Keylime pie for everyone.
1 parent 4761d623
......@@ -12,8 +12,15 @@ var
// a fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.1,
keyXhr,
keyFailed,
resolveUrl;
// returns true if a key has failed to download within a certain amount of retries
keyFailed = function(key) {
return key.retries && key.retries >= 2;
};
videojs.Hls = videojs.Flash.extend({
init: function(player, options, ready) {
var
......@@ -116,6 +123,8 @@ videojs.Hls.prototype.handleSourceOpen = function() {
this.updateDuration(this.playlists.media());
this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
oldMediaPlaylist = updatedPlaylist;
this.fetchKeys(updatedPlaylist, this.mediaIndex);
}));
this.playlists.on('mediachange', function() {
......@@ -175,6 +184,14 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
this.segmentXhr_.abort();
}
// fetch new encryption keys, if necessary
if (keyXhr) {
keyXhr.aborted = true;
keyXhr.abort();
keyXhr = null;
this.fetchKeys(this.playlists.media(), this.mediaIndex);
}
// clear out any buffered segments
this.segmentBuffer_ = [];
......@@ -212,6 +229,11 @@ videojs.Hls.prototype.dispose = function() {
this.segmentXhr_.onreadystatechange = null;
this.segmentXhr_.abort();
}
if (keyXhr) {
keyXhr.onreadystatechange = null;
keyXhr.abort();
keyXhr = null;
}
if (this.playlists) {
this.playlists.dispose();
}
......@@ -441,6 +463,17 @@ videojs.Hls.prototype.drainBuffer = function(event) {
tags = segmentBuffer[0].tags;
segment = playlist.segments[mediaIndex];
if (segment.key) {
// this is an encrypted segment
// 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)) {
return segmentBuffer.shift();
} else if (!segment.key.bytes) {
return;
}
}
event = event || {};
segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
......@@ -492,6 +525,47 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
};
videojs.Hls.prototype.fetchKeys = function(playlist, index) {
var i, key, tech, player, settings;
// if there is a pending XHR or no segments, don't do anything
if (keyXhr || !playlist.segments) {
return;
}
tech = this;
player = this.player();
settings = player.options().hls || {};
// jshint -W083
for (i = index; i < playlist.segments.length; i++) {
key = playlist.segments[i].key;
if (key && !key.bytes && !keyFailed(key)) {
keyXhr = videojs.Hls.xhr({
url: key.uri,
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, function(err, url) {
keyXhr = null;
if (err) {
key.retries = key.retries || 0;
key.retries++;
if (!this.aborted) {
tech.fetchKeys(playlist, i);
}
return;
}
key.bytes = this.response || new Uint8Array([1]);
tech.fetchKeys(playlist, i++, url);
});
break;
}
}
// jshint +W083
};
/**
* Whether the browser has built-in HLS support.
*/
......
......@@ -1276,4 +1276,270 @@ test('calling play() at the end of a video resets the media index', function() {
strictEqual(player.hls.mediaIndex, 0, 'index is 1 after the first segment');
});
test('calling fetchKeys() when a new playlist is loaded will create an XHR', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.playlists.trigger('loadedplaylist');
strictEqual(requests.length, 2, 'a key XHR is created');
strictEqual(requests[1].url, player.hls.playlists.media().segments[0].key.uri, 'a key XHR is created with correct uri');
player.hls.playlists.media = oldMedia;
});
test('a new keys XHR is created when a previous key XHR finishes', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.playlists.trigger('loadedplaylist');
requests.pop().respond(200, new Uint8Array([1]).buffer);
strictEqual(requests.length, 2, 'a key XHR is created');
strictEqual(requests[1].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri');
player.hls.playlists.media = oldMedia;
});
test('calling fetchKeys() when a seek happens will create an XHR', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
duration: 10,
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
duration: 10,
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.fetchKeys(player.hls.playlists.media(), 0);
player.currentTime(11);
ok(requests[1].aborted, 'the key XHR should be aborted');
equal(requests.length, 3, 'we should get a new key XHR');
equal(requests[2].url, player.hls.playlists.media().segments[1].key.uri, 'urls should match');
player.hls.playlists.media = oldMedia;
});
test('calling fetchKeys() when a key XHR is in progress will *not* create an XHR', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
strictEqual(requests.length, 1, 'no key XHR created for the player');
player.hls.playlists.trigger('loadedplaylist');
player.hls.fetchKeys(player.hls.playlists.media(), 0);
strictEqual(requests.length, 2, 'only the original XHR is available');
player.hls.playlists.media = oldMedia;
});
test('calling fetchKeys() when all keys are fetched, will *not* create an XHR', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52',
bytes: new Uint8Array([1])
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53',
bytes: new Uint8Array([1])
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.fetchKeys(player.hls.playlists.media(), 0);
strictEqual(requests.length, 1, 'no XHR for keys created since they were all downloaded');
player.hls.playlists.media = oldMedia;
});
test('retries key requests once upon failure', function() {
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
var oldMedia = player.hls.playlists.media;
player.hls.playlists.media = function() {
return {
segments: [{
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=52'
},
uri: 'http://media.example.com/fileSequence52-A.ts'
}, {
key: {
'method': 'AES-128',
'uri': 'https://priv.example.com/key.php?r=53'
},
uri: 'http://media.example.com/fileSequence53-B.ts'
}]
};
};
player.hls.fetchKeys(player.hls.playlists.media(), 0);
requests[1].respond(404);
equal(requests.length, 3, 'create a new XHR for the same key');
equal(requests[2].url, requests[1].url, 'should be the same key');
requests[2].respond(404);
equal(requests.length, 4, 'create a new XHR for the same key');
notEqual(requests[3].url, requests[2].url, 'should be the same key');
equal(requests[3].url, player.hls.playlists.media().segments[1].key.uri);
player.hls.playlists.media = oldMedia;
});
test('skip segments if key requests fail more than once', function() {
var bytes = [],
tags = [{ pats: 0, bytes: 0 }];
videojs.Hls.SegmentParser = mockSegmentParser(tags);
window.videojs.SourceBuffer = function() {
this.appendBuffer = function(chunk) {
bytes.push(chunk);
};
this.abort = function() {};
};
player.src({
src: 'https://example.com/encrypted-media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
'#EXTINF:2.833,\n' +
'http://media.example.com/fileSequence52-A.ts\n' +
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence53-A.ts\n');
player.hls.playlists.trigger('loadedplaylist');
player.trigger('timeupdate');
// respond to ts segment
standardXHRResponse(requests.pop());
// fail key
requests.pop().respond(404);
// fail key, again
requests.pop().respond(404);
// key for second segment
standardXHRResponse(requests.pop());
equal(bytes.length, 1, 'bytes from the ts segments should not be added');
player.trigger('timeupdate');
tags.push({pts: 0, bytes: 1});
// second segment
standardXHRResponse(requests.pop());
equal(bytes.length, 2, 'bytes from the second ts segment should be added');
equal(bytes[1], 1, 'the bytes from the second segment are added and not the first');
});
})(window, window.videojs);
......