Add fetching keys mechanism and tests.
playistloaded kicks off the key fetching mechanism for the whole playlist. If keys cannot be downloaded after two retries, the segment is skipped. Keylime pie for everyone.
Showing
2 changed files
with
340 additions
and
0 deletions
... | @@ -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,6 +123,8 @@ videojs.Hls.prototype.handleSourceOpen = function() { | ... | @@ -116,6 +123,8 @@ 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', function() { |
... | @@ -175,6 +184,14 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -175,6 +184,14 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
175 | this.segmentXhr_.abort(); | 184 | this.segmentXhr_.abort(); |
176 | } | 185 | } |
177 | 186 | ||
187 | // fetch new encryption keys, if necessary | ||
188 | if (keyXhr) { | ||
189 | keyXhr.aborted = true; | ||
190 | keyXhr.abort(); | ||
191 | keyXhr = null; | ||
192 | this.fetchKeys(this.playlists.media(), this.mediaIndex); | ||
193 | } | ||
194 | |||
178 | // clear out any buffered segments | 195 | // clear out any buffered segments |
179 | this.segmentBuffer_ = []; | 196 | this.segmentBuffer_ = []; |
180 | 197 | ||
... | @@ -212,6 +229,11 @@ videojs.Hls.prototype.dispose = function() { | ... | @@ -212,6 +229,11 @@ videojs.Hls.prototype.dispose = function() { |
212 | this.segmentXhr_.onreadystatechange = null; | 229 | this.segmentXhr_.onreadystatechange = null; |
213 | this.segmentXhr_.abort(); | 230 | this.segmentXhr_.abort(); |
214 | } | 231 | } |
232 | if (keyXhr) { | ||
233 | keyXhr.onreadystatechange = null; | ||
234 | keyXhr.abort(); | ||
235 | keyXhr = null; | ||
236 | } | ||
215 | if (this.playlists) { | 237 | if (this.playlists) { |
216 | this.playlists.dispose(); | 238 | this.playlists.dispose(); |
217 | } | 239 | } |
... | @@ -441,6 +463,17 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -441,6 +463,17 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
441 | tags = segmentBuffer[0].tags; | 463 | tags = segmentBuffer[0].tags; |
442 | segment = playlist.segments[mediaIndex]; | 464 | segment = playlist.segments[mediaIndex]; |
443 | 465 | ||
466 | if (segment.key) { | ||
467 | // this is an encrypted segment | ||
468 | // if the key download failed, we want to skip this segment | ||
469 | // but if the key hasn't downloaded yet, we want to try again later | ||
470 | if (keyFailed(segment.key)) { | ||
471 | return segmentBuffer.shift(); | ||
472 | } else if (!segment.key.bytes) { | ||
473 | return; | ||
474 | } | ||
475 | } | ||
476 | |||
444 | event = event || {}; | 477 | event = event || {}; |
445 | segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; | 478 | segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; |
446 | 479 | ||
... | @@ -492,6 +525,47 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -492,6 +525,47 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
492 | } | 525 | } |
493 | }; | 526 | }; |
494 | 527 | ||
528 | videojs.Hls.prototype.fetchKeys = function(playlist, index) { | ||
529 | var i, key, tech, player, settings; | ||
530 | |||
531 | // if there is a pending XHR or no segments, don't do anything | ||
532 | if (keyXhr || !playlist.segments) { | ||
533 | return; | ||
534 | } | ||
535 | |||
536 | tech = this; | ||
537 | player = this.player(); | ||
538 | settings = player.options().hls || {}; | ||
539 | |||
540 | // jshint -W083 | ||
541 | for (i = index; i < playlist.segments.length; i++) { | ||
542 | key = playlist.segments[i].key; | ||
543 | if (key && !key.bytes && !keyFailed(key)) { | ||
544 | keyXhr = videojs.Hls.xhr({ | ||
545 | url: key.uri, | ||
546 | responseType: 'arraybuffer', | ||
547 | withCredentials: settings.withCredentials | ||
548 | }, function(err, url) { | ||
549 | keyXhr = null; | ||
550 | |||
551 | if (err) { | ||
552 | key.retries = key.retries || 0; | ||
553 | key.retries++; | ||
554 | if (!this.aborted) { | ||
555 | tech.fetchKeys(playlist, i); | ||
556 | } | ||
557 | return; | ||
558 | } | ||
559 | |||
560 | key.bytes = this.response || new Uint8Array([1]); | ||
561 | tech.fetchKeys(playlist, i++, url); | ||
562 | }); | ||
563 | break; | ||
564 | } | ||
565 | } | ||
566 | // jshint +W083 | ||
567 | }; | ||
568 | |||
495 | /** | 569 | /** |
496 | * Whether the browser has built-in HLS support. | 570 | * Whether the browser has built-in HLS support. |
497 | */ | 571 | */ | ... | ... |
... | @@ -1276,4 +1276,270 @@ test('calling play() at the end of a video resets the media index', function() { | ... | @@ -1276,4 +1276,270 @@ test('calling play() at the end of a video resets the media index', function() { |
1276 | strictEqual(player.hls.mediaIndex, 0, 'index is 1 after the first segment'); | 1276 | strictEqual(player.hls.mediaIndex, 0, 'index is 1 after the first segment'); |
1277 | }); | 1277 | }); |
1278 | 1278 | ||
1279 | test('calling fetchKeys() when a new playlist is loaded will create an XHR', function() { | ||
1280 | player.src({ | ||
1281 | src: 'https://example.com/encrypted-media.m3u8', | ||
1282 | type: 'application/vnd.apple.mpegurl' | ||
1283 | }); | ||
1284 | openMediaSource(player); | ||
1285 | |||
1286 | var oldMedia = player.hls.playlists.media; | ||
1287 | player.hls.playlists.media = function() { | ||
1288 | return { | ||
1289 | segments: [{ | ||
1290 | key: { | ||
1291 | 'method': 'AES-128', | ||
1292 | 'uri': 'https://priv.example.com/key.php?r=52' | ||
1293 | }, | ||
1294 | uri: 'http://media.example.com/fileSequence52-A.ts' | ||
1295 | }, { | ||
1296 | key: { | ||
1297 | 'method': 'AES-128', | ||
1298 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
1299 | }, | ||
1300 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
1301 | }] | ||
1302 | }; | ||
1303 | }; | ||
1304 | |||
1305 | player.hls.playlists.trigger('loadedplaylist'); | ||
1306 | strictEqual(requests.length, 2, 'a key XHR is created'); | ||
1307 | strictEqual(requests[1].url, player.hls.playlists.media().segments[0].key.uri, 'a key XHR is created with correct uri'); | ||
1308 | |||
1309 | player.hls.playlists.media = oldMedia; | ||
1310 | }); | ||
1311 | |||
1312 | test('a new keys XHR is created when a previous key XHR finishes', function() { | ||
1313 | player.src({ | ||
1314 | src: 'https://example.com/encrypted-media.m3u8', | ||
1315 | type: 'application/vnd.apple.mpegurl' | ||
1316 | }); | ||
1317 | openMediaSource(player); | ||
1318 | |||
1319 | var oldMedia = player.hls.playlists.media; | ||
1320 | player.hls.playlists.media = function() { | ||
1321 | return { | ||
1322 | segments: [{ | ||
1323 | key: { | ||
1324 | 'method': 'AES-128', | ||
1325 | 'uri': 'https://priv.example.com/key.php?r=52' | ||
1326 | }, | ||
1327 | uri: 'http://media.example.com/fileSequence52-A.ts' | ||
1328 | }, { | ||
1329 | key: { | ||
1330 | 'method': 'AES-128', | ||
1331 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
1332 | }, | ||
1333 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
1334 | }] | ||
1335 | }; | ||
1336 | }; | ||
1337 | |||
1338 | player.hls.playlists.trigger('loadedplaylist'); | ||
1339 | requests.pop().respond(200, new Uint8Array([1]).buffer); | ||
1340 | strictEqual(requests.length, 2, 'a key XHR is created'); | ||
1341 | strictEqual(requests[1].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri'); | ||
1342 | |||
1343 | player.hls.playlists.media = oldMedia; | ||
1344 | }); | ||
1345 | |||
1346 | test('calling fetchKeys() when a seek happens will create an XHR', function() { | ||
1347 | player.src({ | ||
1348 | src: 'https://example.com/encrypted-media.m3u8', | ||
1349 | type: 'application/vnd.apple.mpegurl' | ||
1350 | }); | ||
1351 | openMediaSource(player); | ||
1352 | |||
1353 | var oldMedia = player.hls.playlists.media; | ||
1354 | player.hls.playlists.media = function() { | ||
1355 | return { | ||
1356 | segments: [{ | ||
1357 | duration: 10, | ||
1358 | key: { | ||
1359 | 'method': 'AES-128', | ||
1360 | 'uri': 'https://priv.example.com/key.php?r=52' | ||
1361 | }, | ||
1362 | uri: 'http://media.example.com/fileSequence52-A.ts' | ||
1363 | }, { | ||
1364 | duration: 10, | ||
1365 | key: { | ||
1366 | 'method': 'AES-128', | ||
1367 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
1368 | }, | ||
1369 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
1370 | }] | ||
1371 | }; | ||
1372 | }; | ||
1373 | |||
1374 | player.hls.fetchKeys(player.hls.playlists.media(), 0); | ||
1375 | player.currentTime(11); | ||
1376 | ok(requests[1].aborted, 'the key XHR should be aborted'); | ||
1377 | equal(requests.length, 3, 'we should get a new key XHR'); | ||
1378 | equal(requests[2].url, player.hls.playlists.media().segments[1].key.uri, 'urls should match'); | ||
1379 | |||
1380 | player.hls.playlists.media = oldMedia; | ||
1381 | }); | ||
1382 | |||
1383 | test('calling fetchKeys() when a key XHR is in progress will *not* create an XHR', function() { | ||
1384 | player.src({ | ||
1385 | src: 'https://example.com/encrypted-media.m3u8', | ||
1386 | type: 'application/vnd.apple.mpegurl' | ||
1387 | }); | ||
1388 | openMediaSource(player); | ||
1389 | |||
1390 | var oldMedia = player.hls.playlists.media; | ||
1391 | player.hls.playlists.media = function() { | ||
1392 | return { | ||
1393 | segments: [{ | ||
1394 | key: { | ||
1395 | 'method': 'AES-128', | ||
1396 | 'uri': 'https://priv.example.com/key.php?r=52' | ||
1397 | }, | ||
1398 | uri: 'http://media.example.com/fileSequence52-A.ts' | ||
1399 | }, { | ||
1400 | key: { | ||
1401 | 'method': 'AES-128', | ||
1402 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
1403 | }, | ||
1404 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
1405 | }] | ||
1406 | }; | ||
1407 | }; | ||
1408 | |||
1409 | strictEqual(requests.length, 1, 'no key XHR created for the player'); | ||
1410 | player.hls.playlists.trigger('loadedplaylist'); | ||
1411 | player.hls.fetchKeys(player.hls.playlists.media(), 0); | ||
1412 | strictEqual(requests.length, 2, 'only the original XHR is available'); | ||
1413 | |||
1414 | player.hls.playlists.media = oldMedia; | ||
1415 | }); | ||
1416 | |||
1417 | test('calling fetchKeys() when all keys are fetched, will *not* create an XHR', function() { | ||
1418 | player.src({ | ||
1419 | src: 'https://example.com/encrypted-media.m3u8', | ||
1420 | type: 'application/vnd.apple.mpegurl' | ||
1421 | }); | ||
1422 | openMediaSource(player); | ||
1423 | |||
1424 | var oldMedia = player.hls.playlists.media; | ||
1425 | player.hls.playlists.media = function() { | ||
1426 | return { | ||
1427 | segments: [{ | ||
1428 | key: { | ||
1429 | 'method': 'AES-128', | ||
1430 | 'uri': 'https://priv.example.com/key.php?r=52', | ||
1431 | bytes: new Uint8Array([1]) | ||
1432 | }, | ||
1433 | uri: 'http://media.example.com/fileSequence52-A.ts' | ||
1434 | }, { | ||
1435 | key: { | ||
1436 | 'method': 'AES-128', | ||
1437 | 'uri': 'https://priv.example.com/key.php?r=53', | ||
1438 | bytes: new Uint8Array([1]) | ||
1439 | }, | ||
1440 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
1441 | }] | ||
1442 | }; | ||
1443 | }; | ||
1444 | |||
1445 | player.hls.fetchKeys(player.hls.playlists.media(), 0); | ||
1446 | strictEqual(requests.length, 1, 'no XHR for keys created since they were all downloaded'); | ||
1447 | |||
1448 | player.hls.playlists.media = oldMedia; | ||
1449 | }); | ||
1450 | |||
1451 | test('retries key requests once upon failure', function() { | ||
1452 | player.src({ | ||
1453 | src: 'https://example.com/encrypted-media.m3u8', | ||
1454 | type: 'application/vnd.apple.mpegurl' | ||
1455 | }); | ||
1456 | openMediaSource(player); | ||
1457 | |||
1458 | var oldMedia = player.hls.playlists.media; | ||
1459 | player.hls.playlists.media = function() { | ||
1460 | return { | ||
1461 | segments: [{ | ||
1462 | key: { | ||
1463 | 'method': 'AES-128', | ||
1464 | 'uri': 'https://priv.example.com/key.php?r=52' | ||
1465 | }, | ||
1466 | uri: 'http://media.example.com/fileSequence52-A.ts' | ||
1467 | }, { | ||
1468 | key: { | ||
1469 | 'method': 'AES-128', | ||
1470 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
1471 | }, | ||
1472 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
1473 | }] | ||
1474 | }; | ||
1475 | }; | ||
1476 | |||
1477 | player.hls.fetchKeys(player.hls.playlists.media(), 0); | ||
1478 | |||
1479 | requests[1].respond(404); | ||
1480 | equal(requests.length, 3, 'create a new XHR for the same key'); | ||
1481 | equal(requests[2].url, requests[1].url, 'should be the same key'); | ||
1482 | |||
1483 | requests[2].respond(404); | ||
1484 | equal(requests.length, 4, 'create a new XHR for the same key'); | ||
1485 | notEqual(requests[3].url, requests[2].url, 'should be the same key'); | ||
1486 | equal(requests[3].url, player.hls.playlists.media().segments[1].key.uri); | ||
1487 | |||
1488 | player.hls.playlists.media = oldMedia; | ||
1489 | }); | ||
1490 | |||
1491 | test('skip segments if key requests fail more than once', function() { | ||
1492 | var bytes = [], | ||
1493 | tags = [{ pats: 0, bytes: 0 }]; | ||
1494 | |||
1495 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1496 | window.videojs.SourceBuffer = function() { | ||
1497 | this.appendBuffer = function(chunk) { | ||
1498 | bytes.push(chunk); | ||
1499 | }; | ||
1500 | this.abort = function() {}; | ||
1501 | }; | ||
1502 | |||
1503 | player.src({ | ||
1504 | src: 'https://example.com/encrypted-media.m3u8', | ||
1505 | type: 'application/vnd.apple.mpegurl' | ||
1506 | }); | ||
1507 | openMediaSource(player); | ||
1508 | |||
1509 | requests.pop().respond(200, null, | ||
1510 | '#EXTM3U\n' + | ||
1511 | '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' + | ||
1512 | '#EXTINF:2.833,\n' + | ||
1513 | 'http://media.example.com/fileSequence52-A.ts\n' + | ||
1514 | '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' + | ||
1515 | '#EXTINF:15.0,\n' + | ||
1516 | 'http://media.example.com/fileSequence53-A.ts\n'); | ||
1517 | |||
1518 | player.hls.playlists.trigger('loadedplaylist'); | ||
1519 | |||
1520 | player.trigger('timeupdate'); | ||
1521 | |||
1522 | // respond to ts segment | ||
1523 | standardXHRResponse(requests.pop()); | ||
1524 | // fail key | ||
1525 | requests.pop().respond(404); | ||
1526 | // fail key, again | ||
1527 | requests.pop().respond(404); | ||
1528 | |||
1529 | // key for second segment | ||
1530 | standardXHRResponse(requests.pop()); | ||
1531 | |||
1532 | equal(bytes.length, 1, 'bytes from the ts segments should not be added'); | ||
1533 | |||
1534 | player.trigger('timeupdate'); | ||
1535 | |||
1536 | tags.push({pts: 0, bytes: 1}); | ||
1537 | |||
1538 | // second segment | ||
1539 | standardXHRResponse(requests.pop()); | ||
1540 | |||
1541 | equal(bytes.length, 2, 'bytes from the second ts segment should be added'); | ||
1542 | equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); | ||
1543 | }); | ||
1544 | |||
1279 | })(window, window.videojs); | 1545 | })(window, window.videojs); | ... | ... |
-
Please register or sign in to post a comment