3b8970dc by Gary Katsevman Committed by David LaPalomento

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.
1 parent 4761d623
...@@ -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);
......