e37356d8 by David LaPalomento

Merge pull request #251 from videojs/hlse-perf

Optimize decryption
2 parents 77591534 48f0f290
......@@ -637,6 +637,8 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
}, function(error, url) {
var segmentInfo;
// the segment request is no longer outstanding
tech.segmentXhr_ = null;
......@@ -669,12 +671,26 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
// if the segment is the start of a timestamp discontinuity,
// we have to wait until the sourcebuffer is empty before
// aborting the source buffer processing
tech.segmentBuffer_.push({
segmentInfo = {
// the segment's mediaIndex at the time it was received
mediaIndex: tech.mediaIndex,
// the segment's playlist
playlist: tech.playlists.media(),
// optionally, a time offset to seek to within the segment
offset: offset,
bytes: new Uint8Array(this.response)
});
// unencrypted bytes of the segment
bytes: null,
// when a key is defined for this segment, the encrypted bytes
encryptedBytes: null,
// optionally, the decrypter that is unencrypting the segment
decrypter: null
};
if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) {
segmentInfo.encryptedBytes = new Uint8Array(this.response);
} else {
segmentInfo.bytes = new Uint8Array(this.response);
}
tech.segmentBuffer_.push(segmentInfo);
player.trigger('progress');
tech.drainBuffer();
......@@ -689,12 +705,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
videojs.Hls.prototype.drainBuffer = function(event) {
var
i = 0,
segmentInfo,
mediaIndex,
playlist,
offset,
tags,
bytes,
segment,
decrypter,
segIv,
ptsTime,
segmentOffset,
......@@ -704,28 +723,45 @@ videojs.Hls.prototype.drainBuffer = function(event) {
return;
}
mediaIndex = segmentBuffer[0].mediaIndex;
playlist = segmentBuffer[0].playlist;
offset = segmentBuffer[0].offset;
bytes = segmentBuffer[0].bytes;
segmentInfo = segmentBuffer[0];
mediaIndex = segmentInfo.mediaIndex;
playlist = segmentInfo.playlist;
offset = segmentInfo.offset;
bytes = segmentInfo.bytes;
segment = playlist.segments[mediaIndex];
if (segment.key) {
if (segment.key && !bytes) {
// 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) {
// trigger a key request if one is not already in-flight
return this.fetchKeys_();
} else if (segmentInfo.decrypter) {
// decryption is in progress, try again later
return;
} else {
// if the media sequence is greater than 2^32, the IV will be incorrect
// assuming 10s segments, that would be about 1300 years
var segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
bytes = videojs.Hls.decrypt(bytes,
segment.key.bytes,
segIv);
segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
// create a decrypter to incrementally decrypt the segment
decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes,
segment.key.bytes,
segIv,
function(err, bytes) {
segmentInfo.bytes = bytes;
});
segmentInfo.decrypter = decrypter;
return;
}
}
......
(function(window, videojs, undefined) {
(function(window, videojs, unpad, undefined) {
'use strict';
/*
======== A Handy Little QUnit Reference ========
......@@ -46,7 +46,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() {
0x82, 0xa8, 0xf0, 0x67]);
deepEqual('howdy folks',
stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)),
stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))),
'decrypted with a byte array key');
});
......@@ -68,8 +68,100 @@ test('decrypts multiple AES-128 blocks with CBC', function() {
]);
deepEqual('0123456789abcdef01234',
stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)),
stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))),
'decrypted multiple blocks');
});
})(window, window.videojs);
var clock;
module('Incremental Processing', {
setup: function() {
clock = sinon.useFakeTimers();
},
teardown: function() {
clock.restore();
}
});
test('executes a callback after a timeout', function() {
var asyncStream = new videojs.Hls.AsyncStream(),
calls = '';
asyncStream.push(function() {
calls += 'a';
});
clock.tick(asyncStream.delay);
equal(calls, 'a', 'invoked the callback once');
clock.tick(asyncStream.delay);
equal(calls, 'a', 'only invoked the callback once');
});
test('executes callback in series', function() {
var asyncStream = new videojs.Hls.AsyncStream(),
calls = '';
asyncStream.push(function() {
calls += 'a';
});
asyncStream.push(function() {
calls += 'b';
});
clock.tick(asyncStream.delay);
equal(calls, 'a', 'invoked the first callback');
clock.tick(asyncStream.delay);
equal(calls, 'ab', 'invoked the second');
});
var decrypter;
module('Incremental Decryption', {
setup: function() {
clock = sinon.useFakeTimers();
},
teardown: function() {
clock.restore();
}
});
test('asynchronously decrypts a 4-word block', function() {
var
key = new Uint32Array([0, 0, 0, 0]),
initVector = key,
// the string "howdy folks" encrypted
encrypted = new Uint8Array([
0xce, 0x90, 0x97, 0xd0,
0x08, 0x46, 0x4d, 0x18,
0x4f, 0xae, 0x01, 0x1c,
0x82, 0xa8, 0xf0, 0x67]),
decrypted;
decrypter = new videojs.Hls.Decrypter(encrypted, key, initVector, function(error, result) {
decrypted = result;
});
ok(!decrypted, 'asynchronously decrypts');
clock.tick(decrypter.asyncStream_.delay * 2);
ok(decrypted, 'completed decryption');
deepEqual('howdy folks',
stringFromBytes(decrypted),
'decrypts and unpads the result');
});
test('breaks up input greater than the step value', function() {
var encrypted = new Int32Array(videojs.Hls.Decrypter.STEP + 4),
done = false,
decrypter = new videojs.Hls.Decrypter(encrypted,
new Uint32Array(4),
new Uint32Array(4),
function() {
done = true;
});
clock.tick(decrypter.asyncStream_.delay * 2);
ok(!done, 'not finished after two ticks');
clock.tick(decrypter.asyncStream_.delay);
ok(done, 'finished after the last chunk is decrypted');
});
})(window, window.videojs, window.pkcs7.unpad);
......
......@@ -152,10 +152,8 @@ module('HLS', {
oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
oldDecrypt = videojs.Hls.decrypt;
videojs.Hls.decrypt = function() {
return new Uint8Array([0]);
};
oldDecrypt = videojs.Hls.Decrypter;
videojs.Hls.Decrypter = function() {};
// fake XHRs
xhr = sinon.useFakeXMLHttpRequest();
......@@ -173,7 +171,7 @@ module('HLS', {
videojs.MediaSource.open = oldMediaSourceOpen;
videojs.Hls.SegmentParser = oldSegmentParser;
videojs.Hls.supportsNativeHls = oldNativeHlsSupport;
videojs.Hls.decrypt = oldDecrypt;
videojs.Hls.Decrypter = oldDecrypt;
videojs.SourceBuffer = oldSourceBuffer;
window.setTimeout = oldSetTimeout;
window.clearTimeout = oldClearTimeout;
......@@ -1904,6 +1902,10 @@ test('a new key XHR is created when a the segment is received', function() {
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.shift()); // segment 1
standardXHRResponse(requests.shift()); // key 1
// "finish" decrypting segment 1
player.hls.segmentBuffer_[0].bytes = new Uint8Array(16);
player.hls.checkBuffer_();
standardXHRResponse(requests.shift()); // segment 2
strictEqual(requests.length, 1, 'a key XHR is created');
......@@ -2009,6 +2011,9 @@ test('skip segments if key requests fail more than once', function() {
// key for second segment
requests[0].response = new Uint32Array([0,0,0,0]).buffer;
requests.shift().respond(200, null, '');
// "finish" decryption
player.hls.segmentBuffer_[0].bytes = new Uint8Array(16);
player.hls.checkBuffer_();
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');
......@@ -2033,9 +2038,8 @@ test('the key is supplied to the decrypter in the correct format', function() {
'http://media.example.com/fileSequence52-B.ts\n');
videojs.Hls.decrypt = function(bytes, key) {
videojs.Hls.Decrypter = function(encrypted, key) {
keys.push(key);
return new Uint8Array([0]);
};
standardXHRResponse(requests.shift()); // segment
......@@ -2043,7 +2047,7 @@ test('the key is supplied to the decrypter in the correct format', function() {
requests[0].respond(200, null, '');
requests.shift(); // key
equal(keys.length, 1, 'only one call to decrypt was made');
equal(keys.length, 1, 'only one Decrypter was constructed');
deepEqual(keys[0],
new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
'passed the specified segment key');
......@@ -2068,9 +2072,8 @@ test('supplies the media sequence of current segment as the IV by default, if no
'http://media.example.com/fileSequence52-B.ts\n');
videojs.Hls.decrypt = function(bytes, key, iv) {
videojs.Hls.Decrypter = function(encrypted, key, iv) {
ivs.push(iv);
return new Uint8Array([0]);
};
requests[0].response = new Uint32Array([0,0,0,0]).buffer;
......@@ -2078,7 +2081,7 @@ test('supplies the media sequence of current segment as the IV by default, if no
requests.shift();
standardXHRResponse(requests.pop());
equal(ivs.length, 1, 'only one call to decrypt was made');
equal(ivs.length, 1, 'only one Decrypter was constructed');
deepEqual(ivs[0],
new Uint32Array([0, 0, 0, 5]),
'the IV for the segment is the media sequence');
......