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) { ...@@ -637,6 +637,8 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
637 responseType: 'arraybuffer', 637 responseType: 'arraybuffer',
638 withCredentials: settings.withCredentials 638 withCredentials: settings.withCredentials
639 }, function(error, url) { 639 }, function(error, url) {
640 var segmentInfo;
641
640 // the segment request is no longer outstanding 642 // the segment request is no longer outstanding
641 tech.segmentXhr_ = null; 643 tech.segmentXhr_ = null;
642 644
...@@ -669,12 +671,26 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -669,12 +671,26 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
669 // if the segment is the start of a timestamp discontinuity, 671 // if the segment is the start of a timestamp discontinuity,
670 // we have to wait until the sourcebuffer is empty before 672 // we have to wait until the sourcebuffer is empty before
671 // aborting the source buffer processing 673 // aborting the source buffer processing
672 tech.segmentBuffer_.push({ 674 segmentInfo = {
675 // the segment's mediaIndex at the time it was received
673 mediaIndex: tech.mediaIndex, 676 mediaIndex: tech.mediaIndex,
677 // the segment's playlist
674 playlist: tech.playlists.media(), 678 playlist: tech.playlists.media(),
679 // optionally, a time offset to seek to within the segment
675 offset: offset, 680 offset: offset,
676 bytes: new Uint8Array(this.response) 681 // unencrypted bytes of the segment
677 }); 682 bytes: null,
683 // when a key is defined for this segment, the encrypted bytes
684 encryptedBytes: null,
685 // optionally, the decrypter that is unencrypting the segment
686 decrypter: null
687 };
688 if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) {
689 segmentInfo.encryptedBytes = new Uint8Array(this.response);
690 } else {
691 segmentInfo.bytes = new Uint8Array(this.response);
692 }
693 tech.segmentBuffer_.push(segmentInfo);
678 player.trigger('progress'); 694 player.trigger('progress');
679 tech.drainBuffer(); 695 tech.drainBuffer();
680 696
...@@ -689,12 +705,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -689,12 +705,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
689 videojs.Hls.prototype.drainBuffer = function(event) { 705 videojs.Hls.prototype.drainBuffer = function(event) {
690 var 706 var
691 i = 0, 707 i = 0,
708 segmentInfo,
692 mediaIndex, 709 mediaIndex,
693 playlist, 710 playlist,
694 offset, 711 offset,
695 tags, 712 tags,
696 bytes, 713 bytes,
697 segment, 714 segment,
715 decrypter,
716 segIv,
698 717
699 ptsTime, 718 ptsTime,
700 segmentOffset, 719 segmentOffset,
...@@ -704,28 +723,45 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -704,28 +723,45 @@ videojs.Hls.prototype.drainBuffer = function(event) {
704 return; 723 return;
705 } 724 }
706 725
707 mediaIndex = segmentBuffer[0].mediaIndex; 726 segmentInfo = segmentBuffer[0];
708 playlist = segmentBuffer[0].playlist; 727
709 offset = segmentBuffer[0].offset; 728 mediaIndex = segmentInfo.mediaIndex;
710 bytes = segmentBuffer[0].bytes; 729 playlist = segmentInfo.playlist;
730 offset = segmentInfo.offset;
731 bytes = segmentInfo.bytes;
711 segment = playlist.segments[mediaIndex]; 732 segment = playlist.segments[mediaIndex];
712 733
713 if (segment.key) { 734 if (segment.key && !bytes) {
735
714 // this is an encrypted segment 736 // this is an encrypted segment
715 // if the key download failed, we want to skip this segment 737 // if the key download failed, we want to skip this segment
716 // but if the key hasn't downloaded yet, we want to try again later 738 // but if the key hasn't downloaded yet, we want to try again later
717 if (keyFailed(segment.key)) { 739 if (keyFailed(segment.key)) {
718 return segmentBuffer.shift(); 740 return segmentBuffer.shift();
719 } else if (!segment.key.bytes) { 741 } else if (!segment.key.bytes) {
742
720 // trigger a key request if one is not already in-flight 743 // trigger a key request if one is not already in-flight
721 return this.fetchKeys_(); 744 return this.fetchKeys_();
745
746 } else if (segmentInfo.decrypter) {
747
748 // decryption is in progress, try again later
749 return;
750
722 } else { 751 } else {
723 // if the media sequence is greater than 2^32, the IV will be incorrect 752 // if the media sequence is greater than 2^32, the IV will be incorrect
724 // assuming 10s segments, that would be about 1300 years 753 // assuming 10s segments, that would be about 1300 years
725 var segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); 754 segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
726 bytes = videojs.Hls.decrypt(bytes, 755
727 segment.key.bytes, 756 // create a decrypter to incrementally decrypt the segment
728 segIv); 757 decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes,
758 segment.key.bytes,
759 segIv,
760 function(err, bytes) {
761 segmentInfo.bytes = bytes;
762 });
763 segmentInfo.decrypter = decrypter;
764 return;
729 } 765 }
730 } 766 }
731 767
......
1 (function(window, videojs, undefined) { 1 (function(window, videojs, unpad, undefined) {
2 'use strict'; 2 'use strict';
3 /* 3 /*
4 ======== A Handy Little QUnit Reference ======== 4 ======== A Handy Little QUnit Reference ========
...@@ -46,7 +46,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() { ...@@ -46,7 +46,7 @@ test('decrypts a single AES-128 with PKCS7 block', function() {
46 0x82, 0xa8, 0xf0, 0x67]); 46 0x82, 0xa8, 0xf0, 0x67]);
47 47
48 deepEqual('howdy folks', 48 deepEqual('howdy folks',
49 stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)), 49 stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))),
50 'decrypted with a byte array key'); 50 'decrypted with a byte array key');
51 }); 51 });
52 52
...@@ -68,8 +68,100 @@ test('decrypts multiple AES-128 blocks with CBC', function() { ...@@ -68,8 +68,100 @@ test('decrypts multiple AES-128 blocks with CBC', function() {
68 ]); 68 ]);
69 69
70 deepEqual('0123456789abcdef01234', 70 deepEqual('0123456789abcdef01234',
71 stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)), 71 stringFromBytes(unpad(videojs.Hls.decrypt(encrypted, key, initVector))),
72 'decrypted multiple blocks'); 72 'decrypted multiple blocks');
73 }); 73 });
74 74
75 })(window, window.videojs); 75 var clock;
76
77 module('Incremental Processing', {
78 setup: function() {
79 clock = sinon.useFakeTimers();
80 },
81 teardown: function() {
82 clock.restore();
83 }
84 });
85
86 test('executes a callback after a timeout', function() {
87 var asyncStream = new videojs.Hls.AsyncStream(),
88 calls = '';
89 asyncStream.push(function() {
90 calls += 'a';
91 });
92
93 clock.tick(asyncStream.delay);
94 equal(calls, 'a', 'invoked the callback once');
95 clock.tick(asyncStream.delay);
96 equal(calls, 'a', 'only invoked the callback once');
97 });
98
99 test('executes callback in series', function() {
100 var asyncStream = new videojs.Hls.AsyncStream(),
101 calls = '';
102 asyncStream.push(function() {
103 calls += 'a';
104 });
105 asyncStream.push(function() {
106 calls += 'b';
107 });
108
109 clock.tick(asyncStream.delay);
110 equal(calls, 'a', 'invoked the first callback');
111 clock.tick(asyncStream.delay);
112 equal(calls, 'ab', 'invoked the second');
113 });
114
115 var decrypter;
116
117 module('Incremental Decryption', {
118 setup: function() {
119 clock = sinon.useFakeTimers();
120 },
121 teardown: function() {
122 clock.restore();
123 }
124 });
125
126 test('asynchronously decrypts a 4-word block', function() {
127 var
128 key = new Uint32Array([0, 0, 0, 0]),
129 initVector = key,
130 // the string "howdy folks" encrypted
131 encrypted = new Uint8Array([
132 0xce, 0x90, 0x97, 0xd0,
133 0x08, 0x46, 0x4d, 0x18,
134 0x4f, 0xae, 0x01, 0x1c,
135 0x82, 0xa8, 0xf0, 0x67]),
136 decrypted;
137
138 decrypter = new videojs.Hls.Decrypter(encrypted, key, initVector, function(error, result) {
139 decrypted = result;
140 });
141 ok(!decrypted, 'asynchronously decrypts');
142
143 clock.tick(decrypter.asyncStream_.delay * 2);
144
145 ok(decrypted, 'completed decryption');
146 deepEqual('howdy folks',
147 stringFromBytes(decrypted),
148 'decrypts and unpads the result');
149 });
150
151 test('breaks up input greater than the step value', function() {
152 var encrypted = new Int32Array(videojs.Hls.Decrypter.STEP + 4),
153 done = false,
154 decrypter = new videojs.Hls.Decrypter(encrypted,
155 new Uint32Array(4),
156 new Uint32Array(4),
157 function() {
158 done = true;
159 });
160 clock.tick(decrypter.asyncStream_.delay * 2);
161 ok(!done, 'not finished after two ticks');
162
163 clock.tick(decrypter.asyncStream_.delay);
164 ok(done, 'finished after the last chunk is decrypted');
165 });
166
167 })(window, window.videojs, window.pkcs7.unpad);
......
...@@ -152,10 +152,8 @@ module('HLS', { ...@@ -152,10 +152,8 @@ module('HLS', {
152 152
153 oldNativeHlsSupport = videojs.Hls.supportsNativeHls; 153 oldNativeHlsSupport = videojs.Hls.supportsNativeHls;
154 154
155 oldDecrypt = videojs.Hls.decrypt; 155 oldDecrypt = videojs.Hls.Decrypter;
156 videojs.Hls.decrypt = function() { 156 videojs.Hls.Decrypter = function() {};
157 return new Uint8Array([0]);
158 };
159 157
160 // fake XHRs 158 // fake XHRs
161 xhr = sinon.useFakeXMLHttpRequest(); 159 xhr = sinon.useFakeXMLHttpRequest();
...@@ -173,7 +171,7 @@ module('HLS', { ...@@ -173,7 +171,7 @@ module('HLS', {
173 videojs.MediaSource.open = oldMediaSourceOpen; 171 videojs.MediaSource.open = oldMediaSourceOpen;
174 videojs.Hls.SegmentParser = oldSegmentParser; 172 videojs.Hls.SegmentParser = oldSegmentParser;
175 videojs.Hls.supportsNativeHls = oldNativeHlsSupport; 173 videojs.Hls.supportsNativeHls = oldNativeHlsSupport;
176 videojs.Hls.decrypt = oldDecrypt; 174 videojs.Hls.Decrypter = oldDecrypt;
177 videojs.SourceBuffer = oldSourceBuffer; 175 videojs.SourceBuffer = oldSourceBuffer;
178 window.setTimeout = oldSetTimeout; 176 window.setTimeout = oldSetTimeout;
179 window.clearTimeout = oldClearTimeout; 177 window.clearTimeout = oldClearTimeout;
...@@ -1904,6 +1902,10 @@ test('a new key XHR is created when a the segment is received', function() { ...@@ -1904,6 +1902,10 @@ test('a new key XHR is created when a the segment is received', function() {
1904 '#EXT-X-ENDLIST\n'); 1902 '#EXT-X-ENDLIST\n');
1905 standardXHRResponse(requests.shift()); // segment 1 1903 standardXHRResponse(requests.shift()); // segment 1
1906 standardXHRResponse(requests.shift()); // key 1 1904 standardXHRResponse(requests.shift()); // key 1
1905 // "finish" decrypting segment 1
1906 player.hls.segmentBuffer_[0].bytes = new Uint8Array(16);
1907 player.hls.checkBuffer_();
1908
1907 standardXHRResponse(requests.shift()); // segment 2 1909 standardXHRResponse(requests.shift()); // segment 2
1908 1910
1909 strictEqual(requests.length, 1, 'a key XHR is created'); 1911 strictEqual(requests.length, 1, 'a key XHR is created');
...@@ -2009,6 +2011,9 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2009,6 +2011,9 @@ test('skip segments if key requests fail more than once', function() {
2009 // key for second segment 2011 // key for second segment
2010 requests[0].response = new Uint32Array([0,0,0,0]).buffer; 2012 requests[0].response = new Uint32Array([0,0,0,0]).buffer;
2011 requests.shift().respond(200, null, ''); 2013 requests.shift().respond(200, null, '');
2014 // "finish" decryption
2015 player.hls.segmentBuffer_[0].bytes = new Uint8Array(16);
2016 player.hls.checkBuffer_();
2012 2017
2013 equal(bytes.length, 2, 'bytes from the second ts segment should be added'); 2018 equal(bytes.length, 2, 'bytes from the second ts segment should be added');
2014 equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); 2019 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() { ...@@ -2033,9 +2038,8 @@ test('the key is supplied to the decrypter in the correct format', function() {
2033 'http://media.example.com/fileSequence52-B.ts\n'); 2038 'http://media.example.com/fileSequence52-B.ts\n');
2034 2039
2035 2040
2036 videojs.Hls.decrypt = function(bytes, key) { 2041 videojs.Hls.Decrypter = function(encrypted, key) {
2037 keys.push(key); 2042 keys.push(key);
2038 return new Uint8Array([0]);
2039 }; 2043 };
2040 2044
2041 standardXHRResponse(requests.shift()); // segment 2045 standardXHRResponse(requests.shift()); // segment
...@@ -2043,7 +2047,7 @@ test('the key is supplied to the decrypter in the correct format', function() { ...@@ -2043,7 +2047,7 @@ test('the key is supplied to the decrypter in the correct format', function() {
2043 requests[0].respond(200, null, ''); 2047 requests[0].respond(200, null, '');
2044 requests.shift(); // key 2048 requests.shift(); // key
2045 2049
2046 equal(keys.length, 1, 'only one call to decrypt was made'); 2050 equal(keys.length, 1, 'only one Decrypter was constructed');
2047 deepEqual(keys[0], 2051 deepEqual(keys[0],
2048 new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]), 2052 new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
2049 'passed the specified segment key'); 2053 'passed the specified segment key');
...@@ -2068,9 +2072,8 @@ test('supplies the media sequence of current segment as the IV by default, if no ...@@ -2068,9 +2072,8 @@ test('supplies the media sequence of current segment as the IV by default, if no
2068 'http://media.example.com/fileSequence52-B.ts\n'); 2072 'http://media.example.com/fileSequence52-B.ts\n');
2069 2073
2070 2074
2071 videojs.Hls.decrypt = function(bytes, key, iv) { 2075 videojs.Hls.Decrypter = function(encrypted, key, iv) {
2072 ivs.push(iv); 2076 ivs.push(iv);
2073 return new Uint8Array([0]);
2074 }; 2077 };
2075 2078
2076 requests[0].response = new Uint32Array([0,0,0,0]).buffer; 2079 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 ...@@ -2078,7 +2081,7 @@ test('supplies the media sequence of current segment as the IV by default, if no
2078 requests.shift(); 2081 requests.shift();
2079 standardXHRResponse(requests.pop()); 2082 standardXHRResponse(requests.pop());
2080 2083
2081 equal(ivs.length, 1, 'only one call to decrypt was made'); 2084 equal(ivs.length, 1, 'only one Decrypter was constructed');
2082 deepEqual(ivs[0], 2085 deepEqual(ivs[0],
2083 new Uint32Array([0, 0, 0, 5]), 2086 new Uint32Array([0, 0, 0, 5]),
2084 'the IV for the segment is the media sequence'); 2087 'the IV for the segment is the media sequence');
......