Merge pull request #139 from videojs/feature/hlse2
Support segment-level AES-128 encryption
Showing
18 changed files
with
779 additions
and
32 deletions
... | @@ -11,3 +11,39 @@ distributed under the License is distributed on an "AS IS" BASIS, | ... | @@ -11,3 +11,39 @@ 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. | 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 | ... | ... |
docs/hlse.md
0 → 100644
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 | ``` |
... | @@ -28,6 +28,11 @@ | ... | @@ -28,6 +28,11 @@ |
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 | 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> | ||
35 | |||
31 | <!-- example MPEG2-TS segments --> | 36 | <!-- example MPEG2-TS segments --> |
32 | <!-- bipbop --> | 37 | <!-- bipbop --> |
33 | <!-- <script src="test/tsSegment.js"></script> --> | 38 | <!-- <script src="test/tsSegment.js"></script> --> |
... | @@ -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 | ... | ... |
... | @@ -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,7 +18,7 @@ | ... | @@ -18,7 +18,7 @@ |
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 | } | ... | ... |
src/decrypter.js
0 → 100644
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] === '') { |
36 | continue; | ||
37 | } | ||
21 | 38 | ||
22 | // This is not sexy, but gives us the resulting object we want. | 39 | // split the key and value |
23 | if (attr[1]) { | 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, ''); | ||
24 | attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); | 43 | attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); |
25 | if (attr[1].indexOf('"') !== -1) { | 44 | attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); |
26 | attr[1] = attr[1].split('"')[1]; | ||
27 | } | ||
28 | result[attr[0]] = attr[1]; | 45 | result[attr[0]] = attr[1]; |
29 | } else { | ||
30 | attrs[i - 1] = attrs[i - 1] + ',' + attr[0]; | ||
31 | } | ||
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 | */ | ... | ... |
test/decrypter_test.js
0 → 100644
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', | ... | ... |
test/manifest/encrypted.json
0 → 100644
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 | } |
test/manifest/encrypted.m3u8
0 → 100644
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,11 +43,21 @@ | ... | @@ -43,11 +43,21 @@ |
43 | <section> | 43 | <section> |
44 | <h2>Inputs</h2> | 44 | <h2>Inputs</h2> |
45 | <form id="inputs"> | 45 | <form id="inputs"> |
46 | <fieldset> | ||
46 | <label> | 47 | <label> |
47 | Your original MP2T segment: | 48 | Your original MP2T segment: |
48 | <input type="file" id="original"> | 49 | <input type="file" id="original"> |
49 | </label> | 50 | </label> |
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> | ||
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: |
53 | <input type="file" id="working"> | 63 | <input type="file" id="working"> |
... | @@ -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> | ... | ... |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment