Merge pull request #251 from videojs/hlse-perf
Optimize decryption
Showing
4 changed files
with
318 additions
and
71 deletions
... | @@ -38,7 +38,18 @@ | ... | @@ -38,7 +38,18 @@ |
38 | (function(window, videojs, unpad) { | 38 | (function(window, videojs, unpad) { |
39 | 'use strict'; | 39 | 'use strict'; |
40 | 40 | ||
41 | var AES, decrypt; | 41 | var AES, AsyncStream, Decrypter, decrypt, ntoh; |
42 | |||
43 | /** | ||
44 | * Convert network-order (big-endian) bytes into their little-endian | ||
45 | * representation. | ||
46 | */ | ||
47 | ntoh = function(word) { | ||
48 | return (word << 24) | | ||
49 | ((word & 0xff00) << 8) | | ||
50 | ((word & 0xff0000) >> 8) | | ||
51 | (word >>> 24); | ||
52 | }; | ||
42 | 53 | ||
43 | /** | 54 | /** |
44 | * Schedule out an AES key for both encryption and decryption. This | 55 | * Schedule out an AES key for both encryption and decryption. This |
... | @@ -151,49 +162,51 @@ AES.prototype = { | ... | @@ -151,49 +162,51 @@ AES.prototype = { |
151 | }, | 162 | }, |
152 | 163 | ||
153 | /** | 164 | /** |
154 | * Decrypt an array of 4 big-endian words. | 165 | * Decrypt 16 bytes, specified as four 32-bit words. |
155 | * @param {Array} data The ciphertext. | 166 | * @param encrypted0 {number} the first word to decrypt |
167 | * @param encrypted1 {number} the second word to decrypt | ||
168 | * @param encrypted2 {number} the third word to decrypt | ||
169 | * @param encrypted3 {number} the fourth word to decrypt | ||
170 | * @param out {Int32Array} the array to write the decrypted words | ||
171 | * into | ||
172 | * @param offset {number} the offset into the output array to start | ||
173 | * writing results | ||
156 | * @return {Array} The plaintext. | 174 | * @return {Array} The plaintext. |
157 | */ | 175 | */ |
158 | decrypt:function (input) { | 176 | decrypt:function (encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { |
159 | if (input.length !== 4) { | ||
160 | throw new Error("Invalid aes block size"); | ||
161 | } | ||
162 | |||
163 | var key = this._key[1], | 177 | var key = this._key[1], |
164 | // state variables a,b,c,d are loaded with pre-whitened data | 178 | // state variables a,b,c,d are loaded with pre-whitened data |
165 | a = input[0] ^ key[0], | 179 | a = encrypted0 ^ key[0], |
166 | b = input[3] ^ key[1], | 180 | b = encrypted3 ^ key[1], |
167 | c = input[2] ^ key[2], | 181 | c = encrypted2 ^ key[2], |
168 | d = input[1] ^ key[3], | 182 | d = encrypted1 ^ key[3], |
169 | a2, b2, c2, | 183 | a2, b2, c2, |
170 | 184 | ||
171 | nInnerRounds = key.length/4 - 2, | 185 | nInnerRounds = key.length / 4 - 2, // key.length === 2 ? |
172 | i, | 186 | i, |
173 | kIndex = 4, | 187 | kIndex = 4, |
174 | out = [0,0,0,0], | ||
175 | table = this._tables[1], | 188 | table = this._tables[1], |
176 | 189 | ||
177 | // load up the tables | 190 | // load up the tables |
178 | t0 = table[0], | 191 | table0 = table[0], |
179 | t1 = table[1], | 192 | table1 = table[1], |
180 | t2 = table[2], | 193 | table2 = table[2], |
181 | t3 = table[3], | 194 | table3 = table[3], |
182 | sbox = table[4]; | 195 | sbox = table[4]; |
183 | 196 | ||
184 | // Inner rounds. Cribbed from OpenSSL. | 197 | // Inner rounds. Cribbed from OpenSSL. |
185 | for (i = 0; i < nInnerRounds; i++) { | 198 | for (i = 0; i < nInnerRounds; i++) { |
186 | a2 = t0[a>>>24] ^ t1[b>>16 & 255] ^ t2[c>>8 & 255] ^ t3[d & 255] ^ key[kIndex]; | 199 | a2 = table0[a>>>24] ^ table1[b>>16 & 255] ^ table2[c>>8 & 255] ^ table3[d & 255] ^ key[kIndex]; |
187 | b2 = t0[b>>>24] ^ t1[c>>16 & 255] ^ t2[d>>8 & 255] ^ t3[a & 255] ^ key[kIndex + 1]; | 200 | b2 = table0[b>>>24] ^ table1[c>>16 & 255] ^ table2[d>>8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; |
188 | c2 = t0[c>>>24] ^ t1[d>>16 & 255] ^ t2[a>>8 & 255] ^ t3[b & 255] ^ key[kIndex + 2]; | 201 | c2 = table0[c>>>24] ^ table1[d>>16 & 255] ^ table2[a>>8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; |
189 | d = t0[d>>>24] ^ t1[a>>16 & 255] ^ t2[b>>8 & 255] ^ t3[c & 255] ^ key[kIndex + 3]; | 202 | d = table0[d>>>24] ^ table1[a>>16 & 255] ^ table2[b>>8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; |
190 | kIndex += 4; | 203 | kIndex += 4; |
191 | a=a2; b=b2; c=c2; | 204 | a=a2; b=b2; c=c2; |
192 | } | 205 | } |
193 | 206 | ||
194 | // Last round. | 207 | // Last round. |
195 | for (i = 0; i < 4; i++) { | 208 | for (i = 0; i < 4; i++) { |
196 | out[3 & -i] = | 209 | out[(3 & -i) + offset] = |
197 | sbox[a>>>24 ]<<24 ^ | 210 | sbox[a>>>24 ]<<24 ^ |
198 | sbox[b>>16 & 255]<<16 ^ | 211 | sbox[b>>16 & 255]<<16 ^ |
199 | sbox[c>>8 & 255]<<8 ^ | 212 | sbox[c>>8 & 255]<<8 ^ |
... | @@ -201,49 +214,152 @@ AES.prototype = { | ... | @@ -201,49 +214,152 @@ AES.prototype = { |
201 | key[kIndex++]; | 214 | key[kIndex++]; |
202 | a2=a; a=b; b=c; c=d; d=a2; | 215 | a2=a; a=b; b=c; c=d; d=a2; |
203 | } | 216 | } |
204 | |||
205 | return out; | ||
206 | } | 217 | } |
207 | }; | 218 | }; |
208 | 219 | ||
220 | /** | ||
221 | * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. | ||
222 | * @param encrypted {Uint8Array} the encrypted bytes | ||
223 | * @param key {Uint32Array} the bytes of the decryption key | ||
224 | * @param initVector {Uint32Array} the initialization vector (IV) to | ||
225 | * use for the first round of CBC. | ||
226 | * @return {Uint8Array} the decrypted bytes | ||
227 | * | ||
228 | * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard | ||
229 | * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 | ||
230 | * @see https://tools.ietf.org/html/rfc2315 | ||
231 | */ | ||
209 | decrypt = function(encrypted, key, initVector) { | 232 | decrypt = function(encrypted, key, initVector) { |
210 | var | 233 | var |
211 | encryptedView = new DataView(encrypted.buffer), | 234 | // word-level access to the encrypted bytes |
212 | platformEndian = new Uint32Array(encrypted.byteLength / 4), | 235 | encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2), |
236 | |||
213 | decipher = new AES(Array.prototype.slice.call(key)), | 237 | decipher = new AES(Array.prototype.slice.call(key)), |
238 | |||
239 | // byte and word-level access for the decrypted output | ||
214 | decrypted = new Uint8Array(encrypted.byteLength), | 240 | decrypted = new Uint8Array(encrypted.byteLength), |
215 | decryptedView = new DataView(decrypted.buffer), | 241 | decrypted32 = new Int32Array(decrypted.buffer), |
216 | decryptedBlock, | 242 | |
217 | word, | 243 | // temporary variables for working with the IV, encrypted, and |
218 | byte; | 244 | // decrypted data |
245 | init0, init1, init2, init3, | ||
246 | encrypted0, encrypted1, encrypted2, encrypted3, | ||
247 | |||
248 | // iteration variable | ||
249 | wordIx; | ||
250 | |||
251 | // pull out the words of the IV to ensure we don't modify the | ||
252 | // passed-in reference and easier access | ||
253 | init0 = initVector[0]; | ||
254 | init1 = initVector[1]; | ||
255 | init2 = initVector[2]; | ||
256 | init3 = initVector[3]; | ||
219 | 257 | ||
220 | // convert big-endian input to platform byte order for decryption | ||
221 | for (byte = 0; byte < encrypted.byteLength; byte += 4) { | ||
222 | platformEndian[byte >>> 2] = encryptedView.getUint32(byte); | ||
223 | } | ||
224 | // decrypt four word sequences, applying cipher-block chaining (CBC) | 258 | // decrypt four word sequences, applying cipher-block chaining (CBC) |
225 | // to each decrypted block | 259 | // to each decrypted block |
226 | for (word = 0; word < platformEndian.length; word += 4) { | 260 | for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { |
261 | // convert big-endian (network order) words into little-endian | ||
262 | // (javascript order) | ||
263 | encrypted0 = ntoh(encrypted32[wordIx]); | ||
264 | encrypted1 = ntoh(encrypted32[wordIx + 1]); | ||
265 | encrypted2 = ntoh(encrypted32[wordIx + 2]); | ||
266 | encrypted3 = ntoh(encrypted32[wordIx + 3]); | ||
267 | |||
227 | // decrypt the block | 268 | // decrypt the block |
228 | decryptedBlock = decipher.decrypt(platformEndian.subarray(word, word + 4)); | 269 | decipher.decrypt(encrypted0, |
270 | encrypted1, | ||
271 | encrypted2, | ||
272 | encrypted3, | ||
273 | decrypted32, | ||
274 | wordIx); | ||
229 | 275 | ||
230 | // XOR with the IV, and restore network byte-order to obtain the | 276 | // XOR with the IV, and restore network byte-order to obtain the |
231 | // plaintext | 277 | // plaintext |
232 | byte = word << 2; | 278 | decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); |
233 | decryptedView.setUint32(byte, decryptedBlock[0] ^ initVector[0]); | 279 | decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); |
234 | decryptedView.setUint32(byte + 4, decryptedBlock[1] ^ initVector[1]); | 280 | decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); |
235 | decryptedView.setUint32(byte + 8, decryptedBlock[2] ^ initVector[2]); | 281 | decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); |
236 | decryptedView.setUint32(byte + 12, decryptedBlock[3] ^ initVector[3]); | ||
237 | 282 | ||
238 | // setup the IV for the next round | 283 | // setup the IV for the next round |
239 | initVector = platformEndian.subarray(word, word + 4); | 284 | init0 = encrypted0; |
285 | init1 = encrypted1; | ||
286 | init2 = encrypted2; | ||
287 | init3 = encrypted3; | ||
240 | } | 288 | } |
241 | 289 | ||
242 | // remove any padding | 290 | return decrypted; |
243 | return unpad(decrypted); | 291 | }; |
292 | |||
293 | AsyncStream = function() { | ||
294 | this.jobs = []; | ||
295 | this.delay = 1; | ||
296 | this.timeout_ = null; | ||
297 | }; | ||
298 | AsyncStream.prototype = new videojs.Hls.Stream(); | ||
299 | AsyncStream.prototype.processJob_ = function() { | ||
300 | this.jobs.shift()(); | ||
301 | if (this.jobs.length) { | ||
302 | this.timeout_ = setTimeout(videojs.bind(this, this.processJob_), | ||
303 | this.delay); | ||
304 | } else { | ||
305 | this.timeout_ = null; | ||
306 | } | ||
307 | }; | ||
308 | AsyncStream.prototype.push = function(job) { | ||
309 | this.jobs.push(job); | ||
310 | if (!this.timeout_) { | ||
311 | this.timeout_ = setTimeout(videojs.bind(this, this.processJob_), | ||
312 | this.delay); | ||
313 | } | ||
314 | }; | ||
315 | |||
316 | Decrypter = function(encrypted, key, initVector, done) { | ||
317 | var | ||
318 | step = Decrypter.STEP, | ||
319 | encrypted32 = new Int32Array(encrypted.buffer), | ||
320 | decrypted = new Uint8Array(encrypted.byteLength), | ||
321 | i = 0; | ||
322 | this.asyncStream_ = new AsyncStream(); | ||
323 | |||
324 | // split up the encryption job and do the individual chunks asynchronously | ||
325 | this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), | ||
326 | key, | ||
327 | initVector, | ||
328 | decrypted, | ||
329 | i)); | ||
330 | for (i = step; i < encrypted32.length; i += step) { | ||
331 | initVector = new Uint32Array([ | ||
332 | ntoh(encrypted32[i - 4]), | ||
333 | ntoh(encrypted32[i - 3]), | ||
334 | ntoh(encrypted32[i - 2]), | ||
335 | ntoh(encrypted32[i - 1]) | ||
336 | ]); | ||
337 | this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), | ||
338 | key, | ||
339 | initVector, | ||
340 | decrypted)); | ||
341 | } | ||
342 | // invoke the done() callback when everything is finished | ||
343 | this.asyncStream_.push(function() { | ||
344 | // remove pkcs#7 padding from the decrypted bytes | ||
345 | done(null, unpad(decrypted)); | ||
346 | }); | ||
347 | }; | ||
348 | Decrypter.prototype = new videojs.Hls.Stream(); | ||
349 | Decrypter.prototype.decryptChunk_ = function(encrypted, key, initVector, decrypted) { | ||
350 | return function() { | ||
351 | var bytes = decrypt(encrypted, | ||
352 | key, | ||
353 | initVector); | ||
354 | decrypted.set(bytes, encrypted.byteOffset); | ||
355 | }; | ||
244 | }; | 356 | }; |
357 | // the maximum number of bytes to process at one time | ||
358 | Decrypter.STEP = 4 * 8000; | ||
245 | 359 | ||
246 | // exports | 360 | // exports |
247 | videojs.Hls.decrypt = decrypt; | 361 | videojs.Hls.decrypt = decrypt; |
362 | videojs.Hls.Decrypter = Decrypter; | ||
363 | videojs.Hls.AsyncStream = AsyncStream; | ||
248 | 364 | ||
249 | })(window, window.videojs, window.pkcs7.unpad); | 365 | })(window, window.videojs, window.pkcs7.unpad); | ... | ... |
... | @@ -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 | |
756 | // create a decrypter to incrementally decrypt the segment | ||
757 | decrypter = new videojs.Hls.Decrypter(segmentInfo.encryptedBytes, | ||
727 | segment.key.bytes, | 758 | segment.key.bytes, |
728 | segIv); | 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'); | ... | ... |
-
Please register or sign in to post a comment