ee937d81 by David LaPalomento

Merge pull request #139 from videojs/feature/hlse2

Support segment-level AES-128 encryption
2 parents bf982b0b 93113083
...@@ -74,6 +74,7 @@ module.exports = function(grunt) { ...@@ -74,6 +74,7 @@ module.exports = function(grunt) {
74 connect: { 74 connect: {
75 dev: { 75 dev: {
76 options: { 76 options: {
77 hostname: '*',
77 port: 9999, 78 port: 9999,
78 keepalive: true 79 keepalive: true
79 } 80 }
......
...@@ -10,4 +10,40 @@ Unless required by applicable law or agreed to in writing, software ...@@ -10,4 +10,40 @@ Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS, 10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and 12 See the License for the specific language governing permissions and
13 limitations under the License.
...\ No newline at end of file ...\ No newline at end of file
13 limitations under the License.
14
15 The AES decryption implementation in this project is derived from the
16 Stanford Javascript Cryptography Library
17 (http://bitwiseshiftleft.github.io/sjcl/). That work is covered by the
18 following copyright and permission notice:
19
20 Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
21 All rights reserved.
22
23 Redistribution and use in source and binary forms, with or without
24 modification, are permitted provided that the following conditions are
25 met:
26
27 1. Redistributions of source code must retain the above copyright
28 notice, this list of conditions and the following disclaimer.
29
30 2. Redistributions in binary form must reproduce the above
31 copyright notice, this list of conditions and the following
32 disclaimer in the documentation and/or other materials provided
33 with the distribution.
34
35 THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
36 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
37 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38 DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
39 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
40 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
41 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
42 BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
43 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
44 OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
45 IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46
47 The views and conclusions contained in the software and documentation
48 are those of the authors and should not be interpreted as representing
49 official policies, either expressed or implied, of the authors.
...\ No newline at end of file ...\ No newline at end of file
......
1 # Encrypted HTTP Live Streaming
2 The [HLS spec](http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#section-6.2.3) requires segments to be encrypted with AES-128 in CBC mode with PKCS7 padding. You can encrypt data to that specification with a combination of [OpenSSL](https://www.openssl.org/) and the [pkcs7 utility](https://github.com/brightcove/pkcs7). From the command-line:
3
4 ```sh
5 # encrypt the text "hello" into a file
6 # since this is for testing, skip the key salting so the output is stable
7 # using -nosalt outside of testing is a terrible idea!
8 echo -n "hello" | pkcs7 | \
9 openssl enc -aes-128-cbc -nopad -nosalt -K $KEY -iv $IV > hello.encrypted
10
11 # xxd is a handy way of translating binary into a format easily consumed by
12 # javascript
13 xxd -i hello.encrypted
14 ```
15
16 Later, you can decrypt it:
17
18 ```sh
19 openssl enc -d -nopad -aes-128-cbc -K $KEY -iv $IV
20 ```
...@@ -27,6 +27,11 @@ ...@@ -27,6 +27,11 @@
27 <script src="src/stream.js"></script> 27 <script src="src/stream.js"></script>
28 <script src="src/m3u8/m3u8-parser.js"></script> 28 <script src="src/m3u8/m3u8-parser.js"></script>
29 <script src="src/playlist-loader.js"></script> 29 <script src="src/playlist-loader.js"></script>
30
31 <script src="node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
32 <script src="src/decrypter.js"></script>
33
34 <script src="src/bin-utils.js"></script>
30 35
31 <!-- example MPEG2-TS segments --> 36 <!-- example MPEG2-TS segments -->
32 <!-- bipbop --> 37 <!-- bipbop -->
...@@ -37,12 +42,14 @@ ...@@ -37,12 +42,14 @@
37 <style> 42 <style>
38 body { 43 body {
39 font-family: Arial, sans-serif; 44 font-family: Arial, sans-serif;
45 margin: 20px;
40 } 46 }
41 .info { 47 .info {
42 background-color: #eee; 48 background-color: #eee;
43 border: thin solid #333; 49 border: thin solid #333;
44 border-radius: 3px; 50 border-radius: 3px;
45 padding: 0 5px; 51 padding: 0 5px;
52 margin: 20px 0;
46 } 53 }
47 </style> 54 </style>
48 55
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
37 "video.js": "^4.7.2" 37 "video.js": "^4.7.2"
38 }, 38 },
39 "dependencies": { 39 "dependencies": {
40 "pkcs7": "^0.2.2",
40 "videojs-contrib-media-sources": "^0.3.0" 41 "videojs-contrib-media-sources": "^0.3.0"
41 } 42 }
42 } 43 }
......
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
4 var 4 var
5 bytes = Array.prototype.slice.call(data), 5 bytes = Array.prototype.slice.call(data),
6 step = 16, 6 step = 16,
7 formatHexString = function(e) { 7 formatHexString = function(e, i) {
8 var value = e.toString(16); 8 var value = e.toString(16);
9 return "00".substring(0, 2 - value.length) + value; 9 return "00".substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
10 }, 10 },
11 formatAsciiString = function(e) { 11 formatAsciiString = function(e) {
12 if (e > 32 && e < 125) { 12 if (e >= 0x20 && e < 0x7e) {
13 return String.fromCharCode(e); 13 return String.fromCharCode(e);
14 } 14 }
15 return '.'; 15 return '.';
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
18 hex, 18 hex,
19 ascii; 19 ascii;
20 for (var j = 0; j < bytes.length / step; j++) { 20 for (var j = 0; j < bytes.length / step; j++) {
21 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(' '); 21 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
22 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 22 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
23 result += hex + ' ' + ascii + '\n'; 23 result += hex + ' ' + ascii + '\n';
24 } 24 }
25 return result; 25 return result;
26 }, 26 },
......
1 /*
2 * videojs-hls
3 *
4 * Copyright (c) 2014 Brightcove
5 * All rights reserved.
6 *
7 * This file contains an adaptation of the AES decryption algorithm
8 * from the Standford Javascript Cryptography Library. That work is
9 * covered by the following copyright and permissions notice:
10 *
11 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
12 * All rights reserved.
13 *
14 * Redistribution and use in source and binary forms, with or without
15 * modification, are permitted provided that the following conditions are
16 * met:
17 *
18 * 1. Redistributions of source code must retain the above copyright
19 * notice, this list of conditions and the following disclaimer.
20 *
21 * 2. Redistributions in binary form must reproduce the above
22 * copyright notice, this list of conditions and the following
23 * disclaimer in the documentation and/or other materials provided
24 * with the distribution.
25 *
26 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
27 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
28 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
30 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
33 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
34 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
35 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
36 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 *
38 * The views and conclusions contained in the software and documentation
39 * are those of the authors and should not be interpreted as representing
40 * official policies, either expressed or implied, of the authors.
41 */
42 (function(window, videojs, unpad) {
43 'use strict';
44
45 var AES, decrypt;
46
47 /**
48 * Schedule out an AES key for both encryption and decryption. This
49 * is a low-level class. Use a cipher mode to do bulk encryption.
50 *
51 * @constructor
52 * @param key {Array} The key as an array of 4, 6 or 8 words.
53 */
54 AES = function (key) {
55 this._precompute();
56
57 var i, j, tmp,
58 encKey, decKey,
59 sbox = this._tables[0][4], decTable = this._tables[1],
60 keyLen = key.length, rcon = 1;
61
62 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
63 throw new Error("Invalid aes key size");
64 }
65
66 encKey = key.slice(0);
67 decKey = [];
68 this._key = [encKey, decKey];
69
70 // schedule encryption keys
71 for (i = keyLen; i < 4 * keyLen + 28; i++) {
72 tmp = encKey[i-1];
73
74 // apply sbox
75 if (i%keyLen === 0 || (keyLen === 8 && i%keyLen === 4)) {
76 tmp = sbox[tmp>>>24]<<24 ^ sbox[tmp>>16&255]<<16 ^ sbox[tmp>>8&255]<<8 ^ sbox[tmp&255];
77
78 // shift rows and add rcon
79 if (i%keyLen === 0) {
80 tmp = tmp<<8 ^ tmp>>>24 ^ rcon<<24;
81 rcon = rcon<<1 ^ (rcon>>7)*283;
82 }
83 }
84
85 encKey[i] = encKey[i-keyLen] ^ tmp;
86 }
87
88 // schedule decryption keys
89 for (j = 0; i; j++, i--) {
90 tmp = encKey[j&3 ? i : i - 4];
91 if (i<=4 || j<4) {
92 decKey[j] = tmp;
93 } else {
94 decKey[j] = decTable[0][sbox[tmp>>>24 ]] ^
95 decTable[1][sbox[tmp>>16 & 255]] ^
96 decTable[2][sbox[tmp>>8 & 255]] ^
97 decTable[3][sbox[tmp & 255]];
98 }
99 }
100 };
101
102 AES.prototype = {
103 /**
104 * The expanded S-box and inverse S-box tables. These will be computed
105 * on the client so that we don't have to send them down the wire.
106 *
107 * There are two tables, _tables[0] is for encryption and
108 * _tables[1] is for decryption.
109 *
110 * The first 4 sub-tables are the expanded S-box with MixColumns. The
111 * last (_tables[01][4]) is the S-box itself.
112 *
113 * @private
114 */
115 _tables: [[[],[],[],[],[]],[[],[],[],[],[]]],
116
117 /**
118 * Expand the S-box tables.
119 *
120 * @private
121 */
122 _precompute: function () {
123 var encTable = this._tables[0], decTable = this._tables[1],
124 sbox = encTable[4], sboxInv = decTable[4],
125 i, x, xInv, d=[], th=[], x2, x4, x8, s, tEnc, tDec;
126
127 // Compute double and third tables
128 for (i = 0; i < 256; i++) {
129 th[( d[i] = i<<1 ^ (i>>7)*283 )^i]=i;
130 }
131
132 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
133 // Compute sbox
134 s = xInv ^ xInv<<1 ^ xInv<<2 ^ xInv<<3 ^ xInv<<4;
135 s = s>>8 ^ s&255 ^ 99;
136 sbox[x] = s;
137 sboxInv[s] = x;
138
139 // Compute MixColumns
140 x8 = d[x4 = d[x2 = d[x]]];
141 tDec = x8*0x1010101 ^ x4*0x10001 ^ x2*0x101 ^ x*0x1010100;
142 tEnc = d[s]*0x101 ^ s*0x1010100;
143
144 for (i = 0; i < 4; i++) {
145 encTable[i][x] = tEnc = tEnc<<24 ^ tEnc>>>8;
146 decTable[i][s] = tDec = tDec<<24 ^ tDec>>>8;
147 }
148 }
149
150 // Compactify. Considerable speedup on Firefox.
151 for (i = 0; i < 5; i++) {
152 encTable[i] = encTable[i].slice(0);
153 decTable[i] = decTable[i].slice(0);
154 }
155 },
156
157 /**
158 * Decrypt an array of 4 big-endian words.
159 * @param {Array} data The ciphertext.
160 * @return {Array} The plaintext.
161 */
162 decrypt:function (input) {
163 if (input.length !== 4) {
164 throw new Error("Invalid aes block size");
165 }
166
167 var key = this._key[1],
168 // state variables a,b,c,d are loaded with pre-whitened data
169 a = input[0] ^ key[0],
170 b = input[3] ^ key[1],
171 c = input[2] ^ key[2],
172 d = input[1] ^ key[3],
173 a2, b2, c2,
174
175 nInnerRounds = key.length/4 - 2,
176 i,
177 kIndex = 4,
178 out = [0,0,0,0],
179 table = this._tables[1],
180
181 // load up the tables
182 t0 = table[0],
183 t1 = table[1],
184 t2 = table[2],
185 t3 = table[3],
186 sbox = table[4];
187
188 // Inner rounds. Cribbed from OpenSSL.
189 for (i = 0; i < nInnerRounds; i++) {
190 a2 = t0[a>>>24] ^ t1[b>>16 & 255] ^ t2[c>>8 & 255] ^ t3[d & 255] ^ key[kIndex];
191 b2 = t0[b>>>24] ^ t1[c>>16 & 255] ^ t2[d>>8 & 255] ^ t3[a & 255] ^ key[kIndex + 1];
192 c2 = t0[c>>>24] ^ t1[d>>16 & 255] ^ t2[a>>8 & 255] ^ t3[b & 255] ^ key[kIndex + 2];
193 d = t0[d>>>24] ^ t1[a>>16 & 255] ^ t2[b>>8 & 255] ^ t3[c & 255] ^ key[kIndex + 3];
194 kIndex += 4;
195 a=a2; b=b2; c=c2;
196 }
197
198 // Last round.
199 for (i = 0; i < 4; i++) {
200 out[3 & -i] =
201 sbox[a>>>24 ]<<24 ^
202 sbox[b>>16 & 255]<<16 ^
203 sbox[c>>8 & 255]<<8 ^
204 sbox[d & 255] ^
205 key[kIndex++];
206 a2=a; a=b; b=c; c=d; d=a2;
207 }
208
209 return out;
210 }
211 };
212
213 decrypt = function(encrypted, key, initVector) {
214 var
215 encryptedView = new DataView(encrypted.buffer),
216 platformEndian = new Uint32Array(encrypted.byteLength / 4),
217 decipher = new AES(Array.prototype.slice.call(key)),
218 decrypted = new Uint8Array(encrypted.byteLength),
219 decryptedView = new DataView(decrypted.buffer),
220 decryptedBlock,
221 word,
222 byte;
223
224 // convert big-endian input to platform byte order for decryption
225 for (byte = 0; byte < encrypted.byteLength; byte += 4) {
226 platformEndian[byte >>> 2] = encryptedView.getUint32(byte);
227 }
228 // decrypt four word sequences, applying cipher-block chaining (CBC)
229 // to each decrypted block
230 for (word = 0; word < platformEndian.length; word += 4) {
231 // decrypt the block
232 decryptedBlock = decipher.decrypt(platformEndian.subarray(word, word + 4));
233
234 // XOR with the IV, and restore network byte-order to obtain the
235 // plaintext
236 byte = word << 2;
237 decryptedView.setUint32(byte, decryptedBlock[0] ^ initVector[0]);
238 decryptedView.setUint32(byte + 4, decryptedBlock[1] ^ initVector[1]);
239 decryptedView.setUint32(byte + 8, decryptedBlock[2] ^ initVector[2]);
240 decryptedView.setUint32(byte + 12, decryptedBlock[3] ^ initVector[3]);
241
242 // setup the IV for the next round
243 initVector = platformEndian.subarray(word, word + 4);
244 }
245
246 // remove any padding
247 return unpad(decrypted);
248 };
249
250 // exports
251 videojs.Hls.decrypt = decrypt;
252
253 })(window, window.videojs, window.pkcs7.unpad);
1 /** 1 /**
2 * Utilities for parsing M3U8 files. If the entire manifest is available, 2 * Utilities for parsing M3U8 files. If the entire manifest is available,
3 * `Parser` will create a object representation with enough detail for managing 3 * `Parser` will create an object representation with enough detail for managing
4 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives 4 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives
5 * that do not assume the entirety of the manifest is ready and expose a 5 * that do not assume the entirety of the manifest is ready and expose a
6 * ReadableStream-like interface. 6 * ReadableStream-like interface.
...@@ -8,27 +8,41 @@ ...@@ -8,27 +8,41 @@
8 (function(videojs, parseInt, isFinite, mergeOptions, undefined) { 8 (function(videojs, parseInt, isFinite, mergeOptions, undefined) {
9 var 9 var
10 noop = function() {}, 10 noop = function() {},
11
12 // "forgiving" attribute list psuedo-grammar:
13 // attributes -> keyvalue (',' keyvalue)*
14 // keyvalue -> key '=' value
15 // key -> [^=]*
16 // value -> '"' [^"]* '"' | [^,]*
17 attributeSeparator = (function() {
18 var
19 key = '[^=]*',
20 value = '"[^"]*"|[^,]*',
21 keyvalue = '(?:' + key + ')=(?:' + value + ')';
22
23 return new RegExp('(?:^|,)(' + keyvalue + ')');
24 })(),
11 parseAttributes = function(attributes) { 25 parseAttributes = function(attributes) {
12 var 26 var
13 attrs = attributes.split(','), 27 // split the string using attributes as the separator
28 attrs = attributes.split(attributeSeparator),
14 i = attrs.length, 29 i = attrs.length,
15 result = {}, 30 result = {},
16 attr; 31 attr;
17 32
18 while (i--) { 33 while (i--) {
19 attr = attrs[i].split('='); 34 // filter out unmatched portions of the string
20 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 35 if (attrs[i] === '') {
21 36 continue;
22 // This is not sexy, but gives us the resulting object we want.
23 if (attr[1]) {
24 attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
25 if (attr[1].indexOf('"') !== -1) {
26 attr[1] = attr[1].split('"')[1];
27 }
28 result[attr[0]] = attr[1];
29 } else {
30 attrs[i - 1] = attrs[i - 1] + ',' + attr[0];
31 } 37 }
38
39 // split the key and value
40 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1);
41 // trim whitespace and remove optional quotes around the value
42 attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
43 attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
44 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
45 result[attr[0]] = attr[1];
32 } 46 }
33 return result; 47 return result;
34 }, 48 },
...@@ -281,6 +295,27 @@ ...@@ -281,6 +295,27 @@
281 }); 295 });
282 return; 296 return;
283 } 297 }
298 match = (/^#EXT-X-KEY:?(.*)$/).exec(line);
299 if (match) {
300 event = {
301 type: 'tag',
302 tagType: 'key'
303 };
304 if (match[1]) {
305 event.attributes = parseAttributes(match[1]);
306 // parse the IV string into a Uint32Array
307 if (event.attributes.IV) {
308 event.attributes.IV = event.attributes.IV.match(/.{8}/g);
309 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
310 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
311 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
312 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
313 event.attributes.IV = new Uint32Array(event.attributes.IV);
314 }
315 }
316 this.trigger('data', event);
317 return;
318 }
284 319
285 // unknown tag type 320 // unknown tag type
286 this.trigger('data', { 321 this.trigger('data', {
...@@ -311,7 +346,8 @@ ...@@ -311,7 +346,8 @@
311 var 346 var
312 self = this, 347 self = this,
313 uris = [], 348 uris = [],
314 currentUri = {}; 349 currentUri = {},
350 key;
315 Parser.prototype.init.call(this); 351 Parser.prototype.init.call(this);
316 352
317 this.lineStream = new LineStream(); 353 this.lineStream = new LineStream();
...@@ -373,6 +409,36 @@ ...@@ -373,6 +409,36 @@
373 this.manifest.segments = uris; 409 this.manifest.segments = uris;
374 410
375 }, 411 },
412 'key': function() {
413 if (!entry.attributes) {
414 this.trigger('warn', {
415 message: 'ignoring key declaration without attribute list'
416 });
417 return;
418 }
419 // clear the active encryption key
420 if (entry.attributes.METHOD === 'NONE') {
421 key = null;
422 return;
423 }
424 if (!entry.attributes.URI) {
425 this.trigger('warn', {
426 message: 'ignoring key declaration without URI'
427 });
428 return;
429 }
430 if (!entry.attributes.METHOD) {
431 this.trigger('warn', {
432 message: 'defaulting key method to AES-128'
433 });
434 }
435
436 // setup an encryption key for upcoming segments
437 key = {
438 method: entry.attributes.METHOD || 'AES-128',
439 uri: entry.attributes.URI
440 };
441 },
376 'media-sequence': function() { 442 'media-sequence': function() {
377 if (!isFinite(entry.number)) { 443 if (!isFinite(entry.number)) {
378 this.trigger('warn', { 444 this.trigger('warn', {
...@@ -442,6 +508,10 @@ ...@@ -442,6 +508,10 @@
442 }); 508 });
443 currentUri.duration = this.manifest.targetDuration; 509 currentUri.duration = this.manifest.targetDuration;
444 } 510 }
511 // annotate with encryption information, if necessary
512 if (key) {
513 currentUri.key = key;
514 }
445 515
446 // prepare for the next URI 516 // prepare for the next URI
447 currentUri = {}; 517 currentUri = {};
......
...@@ -12,8 +12,15 @@ var ...@@ -12,8 +12,15 @@ var
12 // a fudge factor to apply to advertised playlist bitrates to account for 12 // a fudge factor to apply to advertised playlist bitrates to account for
13 // temporary flucations in client bandwidth 13 // temporary flucations in client bandwidth
14 bandwidthVariance = 1.1, 14 bandwidthVariance = 1.1,
15 keyXhr,
16 keyFailed,
15 resolveUrl; 17 resolveUrl;
16 18
19 // returns true if a key has failed to download within a certain amount of retries
20 keyFailed = function(key) {
21 return key.retries && key.retries >= 2;
22 };
23
17 videojs.Hls = videojs.Flash.extend({ 24 videojs.Hls = videojs.Flash.extend({
18 init: function(player, options, ready) { 25 init: function(player, options, ready) {
19 var 26 var
...@@ -116,11 +123,20 @@ videojs.Hls.prototype.handleSourceOpen = function() { ...@@ -116,11 +123,20 @@ videojs.Hls.prototype.handleSourceOpen = function() {
116 this.updateDuration(this.playlists.media()); 123 this.updateDuration(this.playlists.media());
117 this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist); 124 this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
118 oldMediaPlaylist = updatedPlaylist; 125 oldMediaPlaylist = updatedPlaylist;
126
127 this.fetchKeys(updatedPlaylist, this.mediaIndex);
119 })); 128 }));
120 129
121 this.playlists.on('mediachange', function() { 130 this.playlists.on('mediachange', videojs.bind(this, function() {
131 // abort outstanding key requests and check if new keys need to be retrieved
132 if (keyXhr) {
133 keyXhr.abort();
134 keyXhr = null;
135 this.fetchKeys(this.playlists.media(), this.mediaIndex);
136 }
137
122 player.trigger('mediachange'); 138 player.trigger('mediachange');
123 }); 139 }));
124 140
125 // if autoplay is enabled, begin playback. This is duplicative of 141 // if autoplay is enabled, begin playback. This is duplicative of
126 // code in video.js but is required because play() must be invoked 142 // code in video.js but is required because play() must be invoked
...@@ -175,6 +191,14 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -175,6 +191,14 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
175 this.segmentXhr_.abort(); 191 this.segmentXhr_.abort();
176 } 192 }
177 193
194 // fetch new encryption keys, if necessary
195 if (keyXhr) {
196 keyXhr.aborted = true;
197 keyXhr.abort();
198 keyXhr = null;
199 this.fetchKeys(this.playlists.media(), this.mediaIndex);
200 }
201
178 // clear out any buffered segments 202 // clear out any buffered segments
179 this.segmentBuffer_ = []; 203 this.segmentBuffer_ = [];
180 204
...@@ -212,6 +236,11 @@ videojs.Hls.prototype.dispose = function() { ...@@ -212,6 +236,11 @@ videojs.Hls.prototype.dispose = function() {
212 this.segmentXhr_.onreadystatechange = null; 236 this.segmentXhr_.onreadystatechange = null;
213 this.segmentXhr_.abort(); 237 this.segmentXhr_.abort();
214 } 238 }
239 if (keyXhr) {
240 keyXhr.onreadystatechange = null;
241 keyXhr.abort();
242 keyXhr = null;
243 }
215 if (this.playlists) { 244 if (this.playlists) {
216 this.playlists.dispose(); 245 this.playlists.dispose();
217 } 246 }
...@@ -357,8 +386,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -357,8 +386,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
357 responseType: 'arraybuffer', 386 responseType: 'arraybuffer',
358 withCredentials: settings.withCredentials 387 withCredentials: settings.withCredentials
359 }, function(error, url) { 388 }, function(error, url) {
360 var tags;
361
362 // the segment request is no longer outstanding 389 // the segment request is no longer outstanding
363 tech.segmentXhr_ = null; 390 tech.segmentXhr_ = null;
364 391
...@@ -390,23 +417,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -390,23 +417,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
390 tech.bandwidth = (this.response.byteLength / tech.segmentXhrTime) * 8 * 1000; 417 tech.bandwidth = (this.response.byteLength / tech.segmentXhrTime) * 8 * 1000;
391 tech.bytesReceived += this.response.byteLength; 418 tech.bytesReceived += this.response.byteLength;
392 419
393 // transmux the segment data from MP2T to FLV
394 tech.segmentParser_.parseSegmentBinaryData(new Uint8Array(this.response));
395 tech.segmentParser_.flushTags();
396
397 // package up all the work to append the segment 420 // package up all the work to append the segment
398 // if the segment is the start of a timestamp discontinuity, 421 // if the segment is the start of a timestamp discontinuity,
399 // we have to wait until the sourcebuffer is empty before 422 // we have to wait until the sourcebuffer is empty before
400 // aborting the source buffer processing 423 // aborting the source buffer processing
401 tags = [];
402 while (tech.segmentParser_.tagsAvailable()) {
403 tags.push(tech.segmentParser_.getNextTag());
404 }
405 tech.segmentBuffer_.push({ 424 tech.segmentBuffer_.push({
406 mediaIndex: tech.mediaIndex, 425 mediaIndex: tech.mediaIndex,
407 playlist: tech.playlists.media(), 426 playlist: tech.playlists.media(),
408 offset: offset, 427 offset: offset,
409 tags: tags 428 bytes: new Uint8Array(this.response)
410 }); 429 });
411 tech.drainBuffer(); 430 tech.drainBuffer();
412 431
...@@ -425,6 +444,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -425,6 +444,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
425 playlist, 444 playlist,
426 offset, 445 offset,
427 tags, 446 tags,
447 bytes,
428 segment, 448 segment,
429 449
430 ptsTime, 450 ptsTime,
...@@ -438,9 +458,28 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -438,9 +458,28 @@ videojs.Hls.prototype.drainBuffer = function(event) {
438 mediaIndex = segmentBuffer[0].mediaIndex; 458 mediaIndex = segmentBuffer[0].mediaIndex;
439 playlist = segmentBuffer[0].playlist; 459 playlist = segmentBuffer[0].playlist;
440 offset = segmentBuffer[0].offset; 460 offset = segmentBuffer[0].offset;
441 tags = segmentBuffer[0].tags; 461 bytes = segmentBuffer[0].bytes;
442 segment = playlist.segments[mediaIndex]; 462 segment = playlist.segments[mediaIndex];
443 463
464 if (segment.key) {
465 // this is an encrypted segment
466 // if the key download failed, we want to skip this segment
467 // but if the key hasn't downloaded yet, we want to try again later
468 if (keyFailed(segment.key)) {
469 return segmentBuffer.shift();
470 } else if (!segment.key.bytes) {
471 return;
472 } else {
473 // if the media sequence is greater than 2^32, the IV will be incorrect
474 // assuming 10s segments, that would be about 1300 years
475 bytes = videojs.Hls.decrypt(bytes,
476 segment.key.bytes,
477 new Uint32Array([
478 0, 0, 0,
479 mediaIndex + playlist.mediaSequence]));
480 }
481 }
482
444 event = event || {}; 483 event = event || {};
445 segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; 484 segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
446 485
...@@ -456,6 +495,15 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -456,6 +495,15 @@ videojs.Hls.prototype.drainBuffer = function(event) {
456 this.el().vjs_setProperty('currentTime', segmentOffset * 0.001); 495 this.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
457 } 496 }
458 497
498 // transmux the segment data from MP2T to FLV
499 this.segmentParser_.parseSegmentBinaryData(bytes);
500 this.segmentParser_.flushTags();
501
502 tags = [];
503 while (this.segmentParser_.tagsAvailable()) {
504 tags.push(this.segmentParser_.getNextTag());
505 }
506
459 // if we're refilling the buffer after a seek, scan through the muxed 507 // if we're refilling the buffer after a seek, scan through the muxed
460 // FLV tags until we find the one that is closest to the desired 508 // FLV tags until we find the one that is closest to the desired
461 // playback time 509 // playback time
...@@ -492,6 +540,53 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -492,6 +540,53 @@ videojs.Hls.prototype.drainBuffer = function(event) {
492 } 540 }
493 }; 541 };
494 542
543 videojs.Hls.prototype.fetchKeys = function(playlist, index) {
544 var i, key, tech, player, settings, view;
545
546 // if there is a pending XHR or no segments, don't do anything
547 if (keyXhr || !playlist.segments) {
548 return;
549 }
550
551 tech = this;
552 player = this.player();
553 settings = player.options().hls || {};
554
555 // jshint -W083
556 for (i = index; i < playlist.segments.length; i++) {
557 key = playlist.segments[i].key;
558 if (key && !key.bytes && !keyFailed(key)) {
559 keyXhr = videojs.Hls.xhr({
560 url: resolveUrl(playlist.uri, key.uri),
561 responseType: 'arraybuffer',
562 withCredentials: settings.withCredentials
563 }, function(err, url) {
564 keyXhr = null;
565
566 if (err || !this.response || this.response.byteLength !== 16) {
567 key.retries = key.retries || 0;
568 key.retries++;
569 if (!this.aborted) {
570 tech.fetchKeys(playlist, i);
571 }
572 return;
573 }
574
575 view = new DataView(this.response);
576 key.bytes = new Uint32Array([
577 view.getUint32(0),
578 view.getUint32(4),
579 view.getUint32(8),
580 view.getUint32(12)
581 ]);
582 tech.fetchKeys(playlist, i++, url);
583 });
584 break;
585 }
586 }
587 // jshint +W083
588 };
589
495 /** 590 /**
496 * Whether the browser has built-in HLS support. 591 * Whether the browser has built-in HLS support.
497 */ 592 */
......
1 (function(window, videojs, undefined) {
2 'use strict';
3 /*
4 ======== A Handy Little QUnit Reference ========
5 http://api.qunitjs.com/
6
7 Test methods:
8 module(name, {[setup][ ,teardown]})
9 test(name, callback)
10 expect(numberOfAssertions)
11 stop(increment)
12 start(decrement)
13 Test assertions:
14 ok(value, [message])
15 equal(actual, expected, [message])
16 notEqual(actual, expected, [message])
17 deepEqual(actual, expected, [message])
18 notDeepEqual(actual, expected, [message])
19 strictEqual(actual, expected, [message])
20 notStrictEqual(actual, expected, [message])
21 throws(block, [expected], [message])
22 */
23
24 // see docs/hlse.md for instructions on how test data was generated
25
26 var stringFromBytes = function(bytes) {
27 var result = '', i;
28
29 for (i = 0; i < bytes.length; i++) {
30 result += String.fromCharCode(bytes[i]);
31 }
32 return result;
33 };
34
35 module('Decryption');
36
37 test('decrypts a single AES-128 with PKCS7 block', function() {
38 var
39 key = new Uint32Array([0, 0, 0, 0]),
40 initVector = key,
41 // the string "howdy folks" encrypted
42 encrypted = new Uint8Array([
43 0xce, 0x90, 0x97, 0xd0,
44 0x08, 0x46, 0x4d, 0x18,
45 0x4f, 0xae, 0x01, 0x1c,
46 0x82, 0xa8, 0xf0, 0x67]);
47
48 deepEqual('howdy folks',
49 stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)),
50 'decrypted with a byte array key');
51 });
52
53 test('decrypts multiple AES-128 blocks with CBC', function() {
54 var
55 key = new Uint32Array([0, 0, 0, 0]),
56 initVector = key,
57 // the string "0123456789abcdef01234" encrypted
58 encrypted = new Uint8Array([
59 0x14, 0xf5, 0xfe, 0x74,
60 0x69, 0x66, 0xf2, 0x92,
61 0x65, 0x1c, 0x22, 0x88,
62 0xbb, 0xff, 0x46, 0x09,
63
64 0x0b, 0xde, 0x5e, 0x71,
65 0x77, 0x87, 0xeb, 0x84,
66 0xa9, 0x54, 0xc2, 0x45,
67 0xe9, 0x4e, 0x29, 0xb3
68 ]);
69
70 deepEqual('0123456789abcdef01234',
71 stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)),
72 'decrypted multiple blocks');
73 });
74
75 })(window, window.videojs);
...@@ -77,6 +77,7 @@ module.exports = function(config) { ...@@ -77,6 +77,7 @@ module.exports = function(config) {
77 '../node_modules/sinon/lib/sinon/util/fake_timers.js', 77 '../node_modules/sinon/lib/sinon/util/fake_timers.js',
78 '../node_modules/video.js/dist/video-js/video.js', 78 '../node_modules/video.js/dist/video-js/video.js',
79 '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', 79 '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
80 '../node_modules/pkcs7/dist/pkcs7.unpad.js',
80 '../test/karma-qunit-shim.js', 81 '../test/karma-qunit-shim.js',
81 '../src/videojs-hls.js', 82 '../src/videojs-hls.js',
82 '../src/xhr.js', 83 '../src/xhr.js',
...@@ -88,6 +89,7 @@ module.exports = function(config) { ...@@ -88,6 +89,7 @@ module.exports = function(config) {
88 '../src/stream.js', 89 '../src/stream.js',
89 '../src/m3u8/m3u8-parser.js', 90 '../src/m3u8/m3u8-parser.js',
90 '../src/playlist-loader.js', 91 '../src/playlist-loader.js',
92 '../src/decrypter.js',
91 '../tmp/manifests.js', 93 '../tmp/manifests.js',
92 '../tmp/expected.js', 94 '../tmp/expected.js',
93 'tsSegment-bc.js', 95 'tsSegment-bc.js',
......
...@@ -41,6 +41,7 @@ module.exports = function(config) { ...@@ -41,6 +41,7 @@ module.exports = function(config) {
41 '../node_modules/sinon/lib/sinon/util/fake_timers.js', 41 '../node_modules/sinon/lib/sinon/util/fake_timers.js',
42 '../node_modules/video.js/dist/video-js/video.js', 42 '../node_modules/video.js/dist/video-js/video.js',
43 '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', 43 '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
44 '../node_modules/pkcs7/dist/pkcs7.unpad.js',
44 '../test/karma-qunit-shim.js', 45 '../test/karma-qunit-shim.js',
45 '../src/videojs-hls.js', 46 '../src/videojs-hls.js',
46 '../src/xhr.js', 47 '../src/xhr.js',
...@@ -52,6 +53,7 @@ module.exports = function(config) { ...@@ -52,6 +53,7 @@ module.exports = function(config) {
52 '../src/stream.js', 53 '../src/stream.js',
53 '../src/m3u8/m3u8-parser.js', 54 '../src/m3u8/m3u8-parser.js',
54 '../src/playlist-loader.js', 55 '../src/playlist-loader.js',
56 '../src/decrypter.js',
55 '../tmp/manifests.js', 57 '../tmp/manifests.js',
56 '../tmp/expected.js', 58 '../tmp/expected.js',
57 'tsSegment-bc.js', 59 'tsSegment-bc.js',
......
...@@ -512,6 +512,94 @@ ...@@ -512,6 +512,94 @@
512 strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf'); 512 strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
513 }); 513 });
514 514
515 // #EXT-X-KEY
516 test('parses valid #EXT-X-KEY tags', function() {
517 var
518 manifest = '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n',
519 element;
520 parseStream.on('data', function(elem) {
521 element = elem;
522 });
523 lineStream.push(manifest);
524
525 ok(element, 'an event was triggered');
526 deepEqual(element, {
527 type: 'tag',
528 tagType: 'key',
529 attributes: {
530 METHOD: 'AES-128',
531 URI: 'https://priv.example.com/key.php?r=52'
532 }
533 }, 'parsed a valid key');
534
535 manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n';
536 lineStream.push(manifest);
537 ok(element, 'an event was triggered');
538 deepEqual(element, {
539 type: 'tag',
540 tagType: 'key',
541 attributes: {
542 METHOD: 'FutureType-1024',
543 URI: 'https://example.com/key#1'
544 }
545 }, 'parsed the attribute list independent of order');
546
547 manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n';
548 lineStream.push(manifest);
549 ok(element.attributes.IV, 'detected an IV attribute');
550 deepEqual(element.attributes.IV, new Uint32Array([
551 0x12345678,
552 0x90abcdef,
553 0x12345678,
554 0x90abcdef
555 ]), 'parsed an IV value');
556 });
557
558 test('parses minimal #EXT-X-KEY tags', function() {
559 var
560 manifest = '#EXT-X-KEY:\n',
561 element;
562 parseStream.on('data', function(elem) {
563 element = elem;
564 });
565 lineStream.push(manifest);
566
567 ok(element, 'an event was triggered');
568 deepEqual(element, {
569 type: 'tag',
570 tagType: 'key'
571 }, 'parsed a minimal key tag');
572 });
573
574 test('parses lightly-broken #EXT-X-KEY tags', function() {
575 var
576 manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n',
577 element;
578 parseStream.on('data', function(elem) {
579 element = elem;
580 });
581 lineStream.push(manifest);
582
583 strictEqual(element.attributes.URI,
584 'https://example.com/single-quote',
585 'parsed a single-quoted uri');
586
587 element = null;
588 manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
589 lineStream.push(manifest);
590 strictEqual(element.tagType, 'key', 'parsed the tag type');
591 strictEqual(element.attributes.URI,
592 'https://example.com/key',
593 'inferred a colon after the tag type');
594
595 element = null;
596 manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
597 lineStream.push(manifest);
598 strictEqual(element.attributes.URI,
599 'https://example.com/key',
600 'trims and removes quotes around the URI');
601 });
602
515 test('ignores empty lines', function() { 603 test('ignores empty lines', function() {
516 var 604 var
517 manifest = '\n', 605 manifest = '\n',
......
1 {
2 "allowCache": true,
3 "mediaSequence": 7794,
4 "segments": [
5 {
6 "duration": 2.833,
7 "key": {
8 "method": "AES-128",
9 "uri": "https://priv.example.com/key.php?r=52"
10 },
11 "uri": "http://media.example.com/fileSequence52-A.ts"
12 },
13 {
14 "duration": 15,
15 "key": {
16 "method": "AES-128",
17 "uri": "https://priv.example.com/key.php?r=52"
18 },
19 "uri": "http://media.example.com/fileSequence52-B.ts"
20 },
21 {
22 "duration": 13.333,
23 "key": {
24 "method": "AES-128",
25 "uri": "https://priv.example.com/key.php?r=52"
26 },
27 "uri": "http://media.example.com/fileSequence52-C.ts"
28 },
29 {
30 "duration": 15,
31 "key": {
32 "method": "AES-128",
33 "uri": "https://priv.example.com/key.php?r=53"
34 },
35 "uri": "http://media.example.com/fileSequence53-A.ts"
36 },
37 {
38 "duration": 15,
39 "uri": "http://media.example.com/fileSequence53-B.ts"
40 }
41 ],
42 "targetDuration": 15
43 }
1 #EXTM3U
2 #EXT-X-VERSION:3
3 #EXT-X-MEDIA-SEQUENCE:7794
4 #EXT-X-TARGETDURATION:15
5
6 #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
7
8 #EXTINF:2.833,
9 http://media.example.com/fileSequence52-A.ts
10 #EXTINF:15.0,
11 http://media.example.com/fileSequence52-B.ts
12 #EXTINF:13.333,
13 http://media.example.com/fileSequence52-C.ts
14
15 #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53"
16
17 #EXTINF:15.0,
18 http://media.example.com/fileSequence53-A.ts
19
20 #EXT-X-KEY:METHOD=NONE
21
22 #EXTINF:15.0,
23 http://media.example.com/fileSequence53-B.ts
...\ No newline at end of file ...\ No newline at end of file
...@@ -43,10 +43,20 @@ ...@@ -43,10 +43,20 @@
43 <section> 43 <section>
44 <h2>Inputs</h2> 44 <h2>Inputs</h2>
45 <form id="inputs"> 45 <form id="inputs">
46 <label> 46 <fieldset>
47 Your original MP2T segment: 47 <label>
48 <input type="file" id="original"> 48 Your original MP2T segment:
49 </label> 49 <input type="file" id="original">
50 </label>
51 <label>
52 Key (optional):
53 <input type="text" id="key">
54 </label>
55 <label>
56 IV (optional):
57 <input type="text" id="iv">
58 </label>
59 </fieldset>
50 <label> 60 <label>
51 A working, FLV version of the underlying stream 61 A working, FLV version of the underlying stream
52 produced by another tool: 62 produced by another tool:
...@@ -105,11 +115,15 @@ ...@@ -105,11 +115,15 @@
105 <script src="../../src/h264-stream.js"></script> 115 <script src="../../src/h264-stream.js"></script>
106 <script src="../../src/aac-stream.js"></script> 116 <script src="../../src/aac-stream.js"></script>
107 <script src="../../src/segment-parser.js"></script> 117 <script src="../../src/segment-parser.js"></script>
118 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
119 <script src="../../src/decrypter.js"></script>
108 120
109 <script src="../../src/bin-utils.js"></script> 121 <script src="../../src/bin-utils.js"></script>
110 <script> 122 <script>
111 var inputs = document.getElementById('inputs'), 123 var inputs = document.getElementById('inputs'),
112 original = document.getElementById('original'), 124 original = document.getElementById('original'),
125 key = document.getElementById('key'),
126 iv = document.getElementById('iv'),
113 working = document.getElementById('working'), 127 working = document.getElementById('working'),
114 128
115 vjsTags = document.querySelector('.vjs-tags'), 129 vjsTags = document.querySelector('.vjs-tags'),
...@@ -132,6 +146,7 @@ ...@@ -132,6 +146,7 @@
132 var parser = new videojs.Hls.SegmentParser(), 146 var parser = new videojs.Hls.SegmentParser(),
133 tags = [parser.getFlvHeader()], 147 tags = [parser.getFlvHeader()],
134 tag, 148 tag,
149 bytes,
135 hex, 150 hex,
136 li, 151 li,
137 byteLength = 0, 152 byteLength = 0,
...@@ -142,7 +157,20 @@ ...@@ -142,7 +157,20 @@
142 // clear old tag info 157 // clear old tag info
143 vjsTags.innerHTML = ''; 158 vjsTags.innerHTML = '';
144 159
145 parser.parseSegmentBinaryData(new Uint8Array(reader.result)); 160 // optionally, decrypt the segment
161 if (key.value && iv.value) {
162 bytes = videojs.Hls.decrypt(new Uint8Array(reader.result),
163 key.value.match(/([0-9a-f]{8})/gi).map(function(e) {
164 return parseInt(e, 16);
165 }),
166 iv.value.match(/([0-9a-f]{8})/gi).map(function(e) {
167 return parseInt(e, 16);
168 }));
169 } else {
170 bytes = new Uint8Array(reader.result);
171 }
172
173 parser.parseSegmentBinaryData(bytes);
146 174
147 // collect all the tags 175 // collect all the tags
148 while (parser.tagsAvailable()) { 176 while (parser.tagsAvailable()) {
......
...@@ -31,6 +31,8 @@ ...@@ -31,6 +31,8 @@
31 <script src="../src/stream.js"></script> 31 <script src="../src/stream.js"></script>
32 <script src="../src/m3u8/m3u8-parser.js"></script> 32 <script src="../src/m3u8/m3u8-parser.js"></script>
33 <script src="../src/playlist-loader.js"></script> 33 <script src="../src/playlist-loader.js"></script>
34 <script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
35 <script src="../src/decrypter.js"></script>
34 <!-- M3U8 TEST DATA --> 36 <!-- M3U8 TEST DATA -->
35 <script src="../tmp/manifests.js"></script> 37 <script src="../tmp/manifests.js"></script>
36 <script src="../tmp/expected.js"></script> 38 <script src="../tmp/expected.js"></script>
...@@ -55,6 +57,7 @@ ...@@ -55,6 +57,7 @@
55 <script src="flv-tag_test.js"></script> 57 <script src="flv-tag_test.js"></script>
56 <script src="m3u8_test.js"></script> 58 <script src="m3u8_test.js"></script>
57 <script src="playlist-loader_test.js"></script> 59 <script src="playlist-loader_test.js"></script>
60 <script src="decrypter_test.js"></script>
58 </head> 61 </head>
59 <body> 62 <body>
60 <div id="qunit"></div> 63 <div id="qunit"></div>
......