619fa74f by David LaPalomento

Fix discontinuities

Never match media indices on playlist reload based on segment URI because some streams may re-use the same segment in multiple positions. Change fetchKeys() so that it operates on buffered segments instead of directly modifying a version of the playlist. Before that change, live playlists with low segment durations could stall because the key would be applied to the previous version of the live playlist and segments would get blocked up forever in the queue waiting for their key to arrive. Use a much less destructive mechanism for playing across discontinuities. vjs_discontinuity() on the SWF allows us to signal a timestamp discontinuity without flushing the playback buffer. That means we don't have to wait until the buffer is empty when a discontinuity is encountered and feeding data to the SWF doesn't have to block either. Update tests to reflect new key-segment request ordering.
1 parent 021896e3
...@@ -221,14 +221,13 @@ videojs.Hls.prototype.src = function(src) { ...@@ -221,14 +221,13 @@ videojs.Hls.prototype.src = function(src) {
221 this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist); 221 this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
222 oldMediaPlaylist = updatedPlaylist; 222 oldMediaPlaylist = updatedPlaylist;
223 223
224 this.fetchKeys(updatedPlaylist, this.mediaIndex); 224 this.fetchKeys_();
225 })); 225 }));
226 226
227 this.playlists.on('mediachange', videojs.bind(this, function() { 227 this.playlists.on('mediachange', videojs.bind(this, function() {
228 // abort outstanding key requests and check if new keys need to be retrieved 228 // abort outstanding key requests and check if new keys need to be retrieved
229 if (keyXhr) { 229 if (keyXhr) {
230 this.cancelKeyXhr(); 230 this.cancelKeyXhr();
231 this.fetchKeys(this.playlists.media(), this.mediaIndex);
232 } 231 }
233 232
234 player.trigger('mediachange'); 233 player.trigger('mediachange');
...@@ -330,11 +329,10 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -330,11 +329,10 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
330 // cancel outstanding requests and buffer appends 329 // cancel outstanding requests and buffer appends
331 this.cancelSegmentXhr(); 330 this.cancelSegmentXhr();
332 331
333 // fetch new encryption keys, if necessary 332 // abort outstanding key requests, if necessary
334 if (keyXhr) { 333 if (keyXhr) {
335 keyXhr.aborted = true; 334 keyXhr.aborted = true;
336 this.cancelKeyXhr(); 335 this.cancelKeyXhr();
337 this.fetchKeys(this.playlists.media(), this.mediaIndex);
338 } 336 }
339 337
340 // clear out any buffered segments 338 // clear out any buffered segments
...@@ -659,6 +657,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -659,6 +657,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
659 offset: offset, 657 offset: offset,
660 bytes: new Uint8Array(this.response) 658 bytes: new Uint8Array(this.response)
661 }); 659 });
660 player.trigger('progress');
662 tech.drainBuffer(); 661 tech.drainBuffer();
663 662
664 tech.mediaIndex++; 663 tech.mediaIndex++;
...@@ -700,7 +699,8 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -700,7 +699,8 @@ videojs.Hls.prototype.drainBuffer = function(event) {
700 if (keyFailed(segment.key)) { 699 if (keyFailed(segment.key)) {
701 return segmentBuffer.shift(); 700 return segmentBuffer.shift();
702 } else if (!segment.key.bytes) { 701 } else if (!segment.key.bytes) {
703 return; 702 // trigger a key request if one is not already in-flight
703 return this.fetchKeys_();
704 } else { 704 } else {
705 // if the media sequence is greater than 2^32, the IV will be incorrect 705 // if the media sequence is greater than 2^32, the IV will be incorrect
706 // assuming 10s segments, that would be about 1300 years 706 // assuming 10s segments, that would be about 1300 years
...@@ -714,23 +714,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -714,23 +714,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
714 event = event || {}; 714 event = event || {};
715 segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000; 715 segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
716 716
717 // abort() clears any data queued in the source buffer so wait
718 // until it empties before calling it when a discontinuity is
719 // next in the buffer
720 if (segment.discontinuity) {
721 if (event.type === 'waiting') {
722 this.sourceBuffer.abort();
723 // tell the SWF where playback is continuing in the stitched timeline
724 this.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
725 } else if (event.type === 'timeupdate') {
726 return;
727 } else if (typeof offset !== 'number') {
728 //if the discontinuity is reached under normal conditions, ie not a seek,
729 //the buffer already contains data and does not need to be refilled,
730 return;
731 }
732 }
733
734 // transmux the segment data from MP2T to FLV 717 // transmux the segment data from MP2T to FLV
735 this.segmentParser_.parseSegmentBinaryData(bytes); 718 this.segmentParser_.parseSegmentBinaryData(bytes);
736 this.segmentParser_.flushTags(); 719 this.segmentParser_.flushTags();
...@@ -758,6 +741,12 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -758,6 +741,12 @@ videojs.Hls.prototype.drainBuffer = function(event) {
758 this.lastSeekedTime_ = null; 741 this.lastSeekedTime_ = null;
759 } 742 }
760 743
744 // when we're crossing a discontinuity, inject metadata to indicate
745 // that the decoder should be reset appropriately
746 if (segment.discontinuity && tags.length) {
747 this.el().vjs_discontinuity();
748 }
749
761 for (i = 0; i < tags.length; i++) { 750 for (i = 0; i < tags.length; i++) {
762 // queue up the bytes to be appended to the SourceBuffer 751 // queue up the bytes to be appended to the SourceBuffer
763 // the queue gives control back to the browser between tags 752 // the queue gives control back to the browser between tags
...@@ -776,11 +765,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -776,11 +765,19 @@ videojs.Hls.prototype.drainBuffer = function(event) {
776 } 765 }
777 }; 766 };
778 767
779 videojs.Hls.prototype.fetchKeys = function(playlist, index) { 768 /**
780 var i, key, tech, player, settings, view; 769 * Attempt to retrieve keys starting at a particular media
770 * segment. This method has no effect if segments are not yet
771 * available or a key request is already in progress.
772 *
773 * @param playlist {object} the media playlist to fetch keys for
774 * @param index {number} the media segment index to start from
775 */
776 videojs.Hls.prototype.fetchKeys_ = function() {
777 var i, key, tech, player, settings, segment, view, receiveKey;
781 778
782 // if there is a pending XHR or no segments, don't do anything 779 // if there is a pending XHR or no segments, don't do anything
783 if (keyXhr || !playlist.segments) { 780 if (keyXhr || !this.segmentBuffer_.length) {
784 return; 781 return;
785 } 782 }
786 783
...@@ -788,39 +785,55 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) { ...@@ -788,39 +785,55 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) {
788 player = this.player(); 785 player = this.player();
789 settings = player.options().hls || {}; 786 settings = player.options().hls || {};
790 787
791 // jshint -W083 788 /**
792 for (i = index; i < playlist.segments.length; i++) { 789 * Handle a key XHR response. This function needs to lookup the
793 key = playlist.segments[i].key; 790 */
794 if (key && !key.bytes && !keyFailed(key)) { 791 receiveKey = function(key) {
792 return function(error) {
793 keyXhr = null;
794
795 if (error || !this.response || this.response.byteLength !== 16) {
796 key.retries = key.retries || 0;
797 key.retries++;
798 if (!this.aborted) {
799 // try fetching again
800 tech.fetchKeys_();
801 }
802 return;
803 }
804
805 view = new DataView(this.response);
806 key.bytes = new Uint32Array([
807 view.getUint32(0),
808 view.getUint32(4),
809 view.getUint32(8),
810 view.getUint32(12)
811 ]);
812
813 // check to see if this allows us to make progress buffering now
814 tech.checkBuffer_();
815 };
816 };
817
818 for (i = 0; i < tech.segmentBuffer_.length; i++) {
819 segment = tech.segmentBuffer_[i].playlist.segments[tech.segmentBuffer_[i].mediaIndex];
820 key = segment.key;
821
822 // continue looking if this segment is unencrypted
823 if (!key) {
824 continue;
825 }
826
827 // request the key if the retry limit hasn't been reached
828 if (!key.bytes && !keyFailed(key)) {
795 keyXhr = videojs.Hls.xhr({ 829 keyXhr = videojs.Hls.xhr({
796 url: this.playlistUriToUrl(key.uri), 830 url: this.playlistUriToUrl(key.uri),
797 responseType: 'arraybuffer', 831 responseType: 'arraybuffer',
798 withCredentials: settings.withCredentials 832 withCredentials: settings.withCredentials
799 }, function(err, url) { 833 }, receiveKey(key));
800 keyXhr = null;
801
802 if (err || !this.response || this.response.byteLength !== 16) {
803 key.retries = key.retries || 0;
804 key.retries++;
805 if (!this.aborted) {
806 tech.fetchKeys(playlist, i);
807 }
808 return;
809 }
810
811 view = new DataView(this.response);
812 key.bytes = new Uint32Array([
813 view.getUint32(0),
814 view.getUint32(4),
815 view.getUint32(8),
816 view.getUint32(12)
817 ]);
818 tech.fetchKeys(playlist, i++, url);
819 });
820 break; 834 break;
821 } 835 }
822 } 836 }
823 // jshint +W083
824 }; 837 };
825 838
826 /** 839 /**
...@@ -925,9 +938,7 @@ videojs.Hls.getPlaylistTotalDuration = function(playlist) { ...@@ -925,9 +938,7 @@ videojs.Hls.getPlaylistTotalDuration = function(playlist) {
925 * playlist 938 * playlist
926 */ 939 */
927 videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { 940 videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
928 var i, 941 var translatedMediaIndex;
929 originalSegment,
930 translatedMediaIndex;
931 942
932 // no segments have been loaded from the original playlist 943 // no segments have been loaded from the original playlist
933 if (mediaIndex === 0) { 944 if (mediaIndex === 0) {
...@@ -939,15 +950,8 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { ...@@ -939,15 +950,8 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
939 return 0; 950 return 0;
940 } 951 }
941 952
942 // try to sync based on URI 953 // translate based on media sequence numbers. syncing up across
943 i = update.segments.length; 954 // bitrate switches should be happening here.
944 originalSegment = original.segments[mediaIndex - 1];
945 while (i--) {
946 if (originalSegment.uri === update.segments[i].uri) {
947 return i + 1;
948 }
949 }
950
951 translatedMediaIndex = (mediaIndex + (original.mediaSequence - update.mediaSequence)); 955 translatedMediaIndex = (mediaIndex + (original.mediaSequence - update.mediaSequence));
952 956
953 if (translatedMediaIndex >= update.segments.length || translatedMediaIndex < 0) { 957 if (translatedMediaIndex >= update.segments.length || translatedMediaIndex < 0) {
...@@ -955,7 +959,6 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { ...@@ -955,7 +959,6 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
955 return videojs.Hls.getMediaIndexForLive_(update) + 1; 959 return videojs.Hls.getMediaIndexForLive_(update) + 1;
956 } 960 }
957 961
958 // sync on media sequence
959 return translatedMediaIndex; 962 return translatedMediaIndex;
960 }; 963 };
961 964
......
...@@ -54,6 +54,7 @@ var ...@@ -54,6 +54,7 @@ var
54 tech.vjs_setProperty = function() {}; 54 tech.vjs_setProperty = function() {};
55 tech.vjs_src = function() {}; 55 tech.vjs_src = function() {};
56 tech.vjs_play = function() {}; 56 tech.vjs_play = function() {};
57 tech.vjs_discontinuity = function() {};
57 videojs.Flash.onReady(tech.id); 58 videojs.Flash.onReady(tech.id);
58 59
59 return player; 60 return player;
...@@ -86,7 +87,7 @@ var ...@@ -86,7 +87,7 @@ var
86 contentType = 'video/MP2T'; 87 contentType = 'video/MP2T';
87 } 88 }
88 89
89 request.response = new Uint8Array([1]).buffer; 90 request.response = new Uint8Array(16).buffer;
90 request.respond(200, 91 request.respond(200,
91 { 'Content-Type': contentType }, 92 { 'Content-Type': contentType },
92 window.manifests[manifestName]); 93 window.manifests[manifestName]);
...@@ -571,6 +572,23 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -571,6 +572,23 @@ test('calculates the bandwidth after downloading a segment', function() {
571 'saves segment request time: ' + player.hls.segmentXhrTime + 's'); 572 'saves segment request time: ' + player.hls.segmentXhrTime + 's');
572 }); 573 });
573 574
575 test('fires a progress event after downloading a segment', function() {
576 var progressCount = 0;
577
578 player.src({
579 src: 'manifest/media.m3u8',
580 type: 'application/vnd.apple.mpegurl'
581 });
582 openMediaSource(player);
583 standardXHRResponse(requests.shift());
584 player.on('progress', function() {
585 progressCount++;
586 });
587 standardXHRResponse(requests.shift());
588
589 equal(progressCount, 1, 'fired a progress event');
590 });
591
574 test('selects a playlist after segment downloads', function() { 592 test('selects a playlist after segment downloads', function() {
575 var calls = 0; 593 var calls = 0;
576 player.src({ 594 player.src({
...@@ -1221,6 +1239,7 @@ test('updates the media index when a playlist reloads', function() { ...@@ -1221,6 +1239,7 @@ test('updates the media index when a playlist reloads', function() {
1221 // reload the updated playlist 1239 // reload the updated playlist
1222 player.hls.playlists.media = function() { 1240 player.hls.playlists.media = function() {
1223 return { 1241 return {
1242 mediaSequence: 1,
1224 segments: [{ 1243 segments: [{
1225 uri: '1.ts' 1244 uri: '1.ts'
1226 }, { 1245 }, {
...@@ -1348,9 +1367,10 @@ test('does not break if the playlist has no segments', function() { ...@@ -1348,9 +1367,10 @@ test('does not break if the playlist has no segments', function() {
1348 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); 1367 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
1349 }); 1368 });
1350 1369
1351 test('waits until the buffer is empty before appending bytes at a discontinuity', function() { 1370 test('calls vjs_discontinuity() before appending bytes at a discontinuity', function() {
1352 var aborts = 0, setTime, currentTime, bufferEnd; 1371 var discontinuities = 0, tags = [], currentTime, bufferEnd;
1353 1372
1373 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1354 player.src({ 1374 player.src({
1355 src: 'discontinuity.m3u8', 1375 src: 'discontinuity.m3u8',
1356 type: 'application/vnd.apple.mpegurl' 1376 type: 'application/vnd.apple.mpegurl'
...@@ -1360,13 +1380,8 @@ test('waits until the buffer is empty before appending bytes at a discontinuity' ...@@ -1360,13 +1380,8 @@ test('waits until the buffer is empty before appending bytes at a discontinuity'
1360 player.buffered = function() { 1380 player.buffered = function() {
1361 return videojs.createTimeRange(0, bufferEnd); 1381 return videojs.createTimeRange(0, bufferEnd);
1362 }; 1382 };
1363 player.hls.sourceBuffer.abort = function() { 1383 player.el().querySelector('.vjs-tech').vjs_discontinuity = function() {
1364 aborts++; 1384 discontinuities++;
1365 };
1366 player.hls.el().vjs_setProperty = function(name, value) {
1367 if (name === 'currentTime') {
1368 return setTime = value;
1369 }
1370 }; 1385 };
1371 1386
1372 requests.pop().respond(200, null, 1387 requests.pop().respond(200, null,
...@@ -1382,15 +1397,11 @@ test('waits until the buffer is empty before appending bytes at a discontinuity' ...@@ -1382,15 +1397,11 @@ test('waits until the buffer is empty before appending bytes at a discontinuity'
1382 currentTime = 6; 1397 currentTime = 6;
1383 bufferEnd = 10; 1398 bufferEnd = 10;
1384 player.hls.checkBuffer_(); 1399 player.hls.checkBuffer_();
1385 strictEqual(aborts, 0, 'no aborts before the buffer empties'); 1400 strictEqual(discontinuities, 0, 'no discontinuities before the segment is received');
1386 1401
1402 tags.push({});
1387 standardXHRResponse(requests.pop()); 1403 standardXHRResponse(requests.pop());
1388 strictEqual(aborts, 0, 'no aborts before the buffer empties'); 1404 strictEqual(discontinuities, 1, 'signals a discontinuity');
1389
1390 // pretend the buffer has emptied
1391 player.trigger('waiting');
1392 strictEqual(aborts, 1, 'aborted before appending the new segment');
1393 strictEqual(setTime, 10, 'updated the time after crossing the discontinuity');
1394 }); 1405 });
1395 1406
1396 test('clears the segment buffer on seek', function() { 1407 test('clears the segment buffer on seek', function() {
...@@ -1781,40 +1792,23 @@ test('drainBuffer will not proceed with empty source buffer', function() { ...@@ -1781,40 +1792,23 @@ test('drainBuffer will not proceed with empty source buffer', function() {
1781 player.hls.playlists.media = oldMedia; 1792 player.hls.playlists.media = oldMedia;
1782 }); 1793 });
1783 1794
1784 test('calling fetchKeys() when a new playlist is loaded will create an XHR', function() { 1795 test('keys are requested when an encrypted segment is loaded', function() {
1785 player.src({ 1796 player.src({
1786 src: 'https://example.com/encrypted-media.m3u8', 1797 src: 'https://example.com/encrypted.m3u8',
1787 type: 'application/vnd.apple.mpegurl' 1798 type: 'application/vnd.apple.mpegurl'
1788 }); 1799 });
1789 openMediaSource(player); 1800 openMediaSource(player);
1790 1801
1791 var oldMedia = player.hls.playlists.media; 1802 standardXHRResponse(requests.shift()); // playlist
1792 player.hls.playlists.media = function() { 1803 standardXHRResponse(requests.shift()); // first segment
1793 return {
1794 segments: [{
1795 key: {
1796 'method': 'AES-128',
1797 'uri': 'https://priv.example.com/key.php?r=52'
1798 },
1799 uri: 'http://media.example.com/fileSequence52-A.ts'
1800 }, {
1801 key: {
1802 'method': 'AES-128',
1803 'uri': 'https://priv.example.com/key.php?r=53'
1804 },
1805 uri: 'http://media.example.com/fileSequence53-B.ts'
1806 }]
1807 };
1808 };
1809
1810 player.hls.playlists.trigger('loadedplaylist');
1811 strictEqual(requests.length, 2, 'a key XHR is created');
1812 strictEqual(requests[1].url, player.hls.playlists.media().segments[0].key.uri, 'a key XHR is created with correct uri');
1813 1804
1814 player.hls.playlists.media = oldMedia; 1805 strictEqual(requests.length, 1, 'a key XHR is created');
1806 strictEqual(requests[0].url,
1807 player.hls.playlists.media().segments[0].key.uri,
1808 'a key XHR is created with correct uri');
1815 }); 1809 });
1816 1810
1817 test('fetchKeys() resolves URLs relative to the master playlist', function() { 1811 test('keys are resolved relative to the master playlist', function() {
1818 player.src({ 1812 player.src({
1819 src: 'video/master-encrypted.m3u8', 1813 src: 'video/master-encrypted.m3u8',
1820 type: 'application/vnd.apple.mpegurl' 1814 type: 'application/vnd.apple.mpegurl'
...@@ -1833,12 +1827,13 @@ test('fetchKeys() resolves URLs relative to the master playlist', function() { ...@@ -1833,12 +1827,13 @@ test('fetchKeys() resolves URLs relative to the master playlist', function() {
1833 'http://media.example.com/fileSequence1.ts\n' + 1827 'http://media.example.com/fileSequence1.ts\n' +
1834 '#EXT-X-ENDLIST\n'); 1828 '#EXT-X-ENDLIST\n');
1835 1829
1836 equal(requests.length, 2, 'requested two URLs'); 1830 standardXHRResponse(requests.shift());
1831 equal(requests.length, 1, 'requested the key');
1837 ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url), 1832 ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url),
1838 'resolves multiple relative paths'); 1833 'resolves multiple relative paths');
1839 }); 1834 });
1840 1835
1841 test('fetchKeys() resolves URLs relative to their containing playlist', function() { 1836 test('keys are resolved relative to their containing playlist', function() {
1842 player.src({ 1837 player.src({
1843 src: 'video/media-encrypted.m3u8', 1838 src: 'video/media-encrypted.m3u8',
1844 type: 'application/vnd.apple.mpegurl' 1839 type: 'application/vnd.apple.mpegurl'
...@@ -1851,197 +1846,100 @@ test('fetchKeys() resolves URLs relative to their containing playlist', function ...@@ -1851,197 +1846,100 @@ test('fetchKeys() resolves URLs relative to their containing playlist', function
1851 '#EXTINF:2.833,\n' + 1846 '#EXTINF:2.833,\n' +
1852 'http://media.example.com/fileSequence1.ts\n' + 1847 'http://media.example.com/fileSequence1.ts\n' +
1853 '#EXT-X-ENDLIST\n'); 1848 '#EXT-X-ENDLIST\n');
1854 equal(requests.length, 2, 'requested two URLs'); 1849 standardXHRResponse(requests.shift());
1850 equal(requests.length, 1, 'requested a key');
1855 ok((/video\/keys\/key\.php$/).test(requests[0].url), 1851 ok((/video\/keys\/key\.php$/).test(requests[0].url),
1856 'resolves multiple relative paths'); 1852 'resolves multiple relative paths');
1857 }); 1853 });
1858 1854
1859 test('a new keys XHR is created when a previous key XHR finishes', function() { 1855 test('a new key XHR is created when a the segment is received', function() {
1860 player.src({ 1856 player.src({
1861 src: 'https://example.com/encrypted-media.m3u8', 1857 src: 'https://example.com/encrypted-media.m3u8',
1862 type: 'application/vnd.apple.mpegurl' 1858 type: 'application/vnd.apple.mpegurl'
1863 }); 1859 });
1864 openMediaSource(player); 1860 openMediaSource(player);
1865 1861
1866 var oldMedia = player.hls.playlists.media; 1862 requests.shift().respond(200, null,
1867 player.hls.playlists.media = function() { 1863 '#EXTM3U\n' +
1868 return { 1864 '#EXT-X-TARGETDURATION:15\n' +
1869 segments: [{ 1865 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
1870 key: { 1866 '#EXTINF:2.833,\n' +
1871 'method': 'AES-128', 1867 'http://media.example.com/fileSequence1.ts\n' +
1872 'uri': 'https://priv.example.com/key.php?r=52' 1868 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
1873 }, 1869 '#EXTINF:2.833,\n' +
1874 uri: 'http://media.example.com/fileSequence52-A.ts' 1870 'http://media.example.com/fileSequence2.ts\n' +
1875 }, { 1871 '#EXT-X-ENDLIST\n');
1876 key: { 1872 standardXHRResponse(requests.shift()); // segment 1
1877 'method': 'AES-128', 1873 standardXHRResponse(requests.shift()); // key 1
1878 'uri': 'https://priv.example.com/key.php?r=53' 1874 standardXHRResponse(requests.shift()); // segment 2
1879 },
1880 uri: 'http://media.example.com/fileSequence53-B.ts'
1881 }]
1882 };
1883 };
1884 // we're inject the media playlist, so drop the request
1885 requests.shift();
1886 1875
1887 player.hls.playlists.trigger('loadedplaylist');
1888 // key response
1889 requests[0].response = new Uint32Array([0, 0, 0, 0]).buffer;
1890 requests.shift().respond(200, null, '');
1891 strictEqual(requests.length, 1, 'a key XHR is created'); 1876 strictEqual(requests.length, 1, 'a key XHR is created');
1892 strictEqual(requests[0].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri'); 1877 strictEqual(requests[0].url,
1893 1878 'https://example.com/' +
1894 player.hls.playlists.media = oldMedia; 1879 player.hls.playlists.media().segments[1].key.uri,
1895 }); 1880 'a key XHR is created with the correct uri');
1896
1897 test('calling fetchKeys() when a seek happens will create an XHR', function() {
1898 player.src({
1899 src: 'https://example.com/encrypted-media.m3u8',
1900 type: 'application/vnd.apple.mpegurl'
1901 });
1902 openMediaSource(player);
1903
1904 var oldMedia = player.hls.playlists.media;
1905 player.hls.playlists.media = function() {
1906 return {
1907 segments: [{
1908 duration: 10,
1909 key: {
1910 'method': 'AES-128',
1911 'uri': 'https://priv.example.com/key.php?r=52'
1912 },
1913 uri: 'http://media.example.com/fileSequence52-A.ts'
1914 }, {
1915 duration: 10,
1916 key: {
1917 'method': 'AES-128',
1918 'uri': 'https://priv.example.com/key.php?r=53'
1919 },
1920 uri: 'http://media.example.com/fileSequence53-B.ts'
1921 }]
1922 };
1923 };
1924
1925 player.hls.fetchKeys(player.hls.playlists.media(), 0);
1926 player.currentTime(11);
1927 ok(requests[1].aborted, 'the key XHR should be aborted');
1928 equal(requests.length, 3, 'we should get a new key XHR');
1929 equal(requests[2].url, player.hls.playlists.media().segments[1].key.uri, 'urls should match');
1930
1931 player.hls.playlists.media = oldMedia;
1932 }); 1881 });
1933 1882
1934 test('calling fetchKeys() when a key XHR is in progress will *not* create an XHR', function() { 1883 test('seeking should abort an outstanding key request and create a new one', function() {
1935 player.src({ 1884 player.src({
1936 src: 'https://example.com/encrypted-media.m3u8', 1885 src: 'https://example.com/encrypted.m3u8',
1937 type: 'application/vnd.apple.mpegurl' 1886 type: 'application/vnd.apple.mpegurl'
1938 }); 1887 });
1939 openMediaSource(player); 1888 openMediaSource(player);
1940 1889
1941 var oldMedia = player.hls.playlists.media;
1942 player.hls.playlists.media = function() {
1943 return {
1944 segments: [{
1945 key: {
1946 'method': 'AES-128',
1947 'uri': 'https://priv.example.com/key.php?r=52'
1948 },
1949 uri: 'http://media.example.com/fileSequence52-A.ts'
1950 }, {
1951 key: {
1952 'method': 'AES-128',
1953 'uri': 'https://priv.example.com/key.php?r=53'
1954 },
1955 uri: 'http://media.example.com/fileSequence53-B.ts'
1956 }]
1957 };
1958 };
1959
1960 strictEqual(requests.length, 1, 'no key XHR created for the player');
1961 player.hls.playlists.trigger('loadedplaylist');
1962 player.hls.fetchKeys(player.hls.playlists.media(), 0);
1963 strictEqual(requests.length, 2, 'only the original XHR is available');
1964
1965 player.hls.playlists.media = oldMedia;
1966 });
1967 1890
1968 test('calling fetchKeys() when all keys are fetched, will *not* create an XHR', function() { 1891 requests.shift().respond(200, null,
1969 player.src({ 1892 '#EXTM3U\n' +
1970 src: 'https://example.com/encrypted-media.m3u8', 1893 '#EXT-X-TARGETDURATION:15\n' +
1971 type: 'application/vnd.apple.mpegurl' 1894 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
1972 }); 1895 '#EXTINF:9,\n' +
1973 openMediaSource(player); 1896 'http://media.example.com/fileSequence1.ts\n' +
1974 1897 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
1975 var oldMedia = player.hls.playlists.media; 1898 '#EXTINF:9,\n' +
1976 player.hls.playlists.media = function() { 1899 'http://media.example.com/fileSequence2.ts\n' +
1977 return { 1900 '#EXT-X-ENDLIST\n');
1978 segments: [{ 1901 standardXHRResponse(requests.shift()); // segment 1
1979 key: {
1980 'method': 'AES-128',
1981 'uri': 'https://priv.example.com/key.php?r=52',
1982 bytes: new Uint8Array([1])
1983 },
1984 uri: 'http://media.example.com/fileSequence52-A.ts'
1985 }, {
1986 key: {
1987 'method': 'AES-128',
1988 'uri': 'https://priv.example.com/key.php?r=53',
1989 bytes: new Uint8Array([1])
1990 },
1991 uri: 'http://media.example.com/fileSequence53-B.ts'
1992 }]
1993 };
1994 };
1995 1902
1996 player.hls.fetchKeys(player.hls.playlists.media(), 0); 1903 player.currentTime(11);
1997 strictEqual(requests.length, 1, 'no XHR for keys created since they were all downloaded'); 1904 ok(requests[0].aborted, 'the key XHR should be aborted');
1905 requests.shift(); // aborted key 1
1998 1906
1999 player.hls.playlists.media = oldMedia; 1907 equal(requests.length, 1, 'requested the new segment');
1908 standardXHRResponse(requests.shift()); // segment 2
1909 equal(requests.length, 1, 'requested the new key');
1910 equal(requests[0].url,
1911 'https://example.com/' +
1912 player.hls.playlists.media().segments[1].key.uri,
1913 'urls should match');
2000 }); 1914 });
2001 1915
2002 test('retries key requests once upon failure', function() { 1916 test('retries key requests once upon failure', function() {
2003 player.src({ 1917 player.src({
2004 src: 'https://example.com/encrypted-media.m3u8', 1918 src: 'https://example.com/encrypted.m3u8',
2005 type: 'application/vnd.apple.mpegurl' 1919 type: 'application/vnd.apple.mpegurl'
2006 }); 1920 });
2007 openMediaSource(player); 1921 openMediaSource(player);
2008 1922
2009 var oldMedia = player.hls.playlists.media; 1923 requests.shift().respond(200, null,
2010 player.hls.playlists.media = function() { 1924 '#EXTM3U\n' +
2011 return { 1925 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
2012 segments: [{ 1926 '#EXTINF:2.833,\n' +
2013 key: { 1927 'http://media.example.com/fileSequence52-A.ts\n' +
2014 'method': 'AES-128', 1928 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
2015 'uri': 'https://priv.example.com/key.php?r=52' 1929 '#EXTINF:15.0,\n' +
2016 }, 1930 'http://media.example.com/fileSequence53-A.ts\n');
2017 uri: 'http://media.example.com/fileSequence52-A.ts' 1931 standardXHRResponse(requests.shift()); // segment
2018 }, { 1932 requests[0].respond(404);
2019 key: { 1933 equal(requests.length, 2, 'create a new XHR for the same key');
2020 'method': 'AES-128', 1934 equal(requests[1].url, requests[0].url, 'should be the same key');
2021 'uri': 'https://priv.example.com/key.php?r=53'
2022 },
2023 uri: 'http://media.example.com/fileSequence53-B.ts'
2024 }]
2025 };
2026 };
2027
2028 player.hls.fetchKeys(player.hls.playlists.media(), 0);
2029 1935
2030 requests[1].respond(404); 1936 requests[1].respond(404);
2031 equal(requests.length, 3, 'create a new XHR for the same key'); 1937 equal(requests.length, 2, 'gives up after one retry');
2032 equal(requests[2].url, requests[1].url, 'should be the same key');
2033
2034 requests[2].respond(404);
2035 equal(requests.length, 4, 'create a new XHR for the same key');
2036 notEqual(requests[3].url, requests[2].url, 'should be the same key');
2037 equal(requests[3].url, player.hls.playlists.media().segments[1].key.uri);
2038
2039 player.hls.playlists.media = oldMedia;
2040 }); 1938 });
2041 1939
2042 test('skip segments if key requests fail more than once', function() { 1940 test('skip segments if key requests fail more than once', function() {
2043 var bytes = [], 1941 var bytes = [],
2044 tags = [{ pats: 0, bytes: 0 }]; 1942 tags = [{ pts: 0, bytes: 0 }];
2045 1943
2046 videojs.Hls.SegmentParser = mockSegmentParser(tags); 1944 videojs.Hls.SegmentParser = mockSegmentParser(tags);
2047 window.videojs.SourceBuffer = function() { 1945 window.videojs.SourceBuffer = function() {
...@@ -2057,7 +1955,7 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2057,7 +1955,7 @@ test('skip segments if key requests fail more than once', function() {
2057 }); 1955 });
2058 openMediaSource(player); 1956 openMediaSource(player);
2059 1957
2060 requests.pop().respond(200, null, 1958 requests.shift().respond(200, null,
2061 '#EXTM3U\n' + 1959 '#EXTM3U\n' +
2062 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' + 1960 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
2063 '#EXTINF:2.833,\n' + 1961 '#EXTINF:2.833,\n' +
...@@ -2065,32 +1963,19 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2065,32 +1963,19 @@ test('skip segments if key requests fail more than once', function() {
2065 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' + 1963 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
2066 '#EXTINF:15.0,\n' + 1964 '#EXTINF:15.0,\n' +
2067 'http://media.example.com/fileSequence53-A.ts\n'); 1965 'http://media.example.com/fileSequence53-A.ts\n');
1966 standardXHRResponse(requests.shift()); // segment 1
1967 requests.shift().respond(404); // fail key
1968 requests.shift().respond(404); // fail key, again
2068 1969
2069 player.hls.playlists.trigger('loadedplaylist'); 1970 tags.length = 0;
2070 1971 tags.push({pts: 0, bytes: 1});
2071 player.hls.checkBuffer_(); 1972 player.hls.checkBuffer_();
2072 1973 standardXHRResponse(requests.shift()); // segment 2
2073 // respond to ts segment 1974 equal(bytes.length, 1, 'bytes from the ts segments should not be added');
2074 standardXHRResponse(requests.pop());
2075 // fail key
2076 requests.pop().respond(404);
2077 // fail key, again
2078 requests.pop().respond(404);
2079 1975
2080 // key for second segment 1976 // key for second segment
2081 requests[0].response = new Uint32Array([0,0,0,0]).buffer; 1977 requests[0].response = new Uint32Array([0,0,0,0]).buffer;
2082 requests[0].respond(200, null, ''); 1978 requests.shift().respond(200, null, '');
2083 requests.shift();
2084
2085 equal(bytes.length, 1, 'bytes from the ts segments should not be added');
2086
2087 player.hls.checkBuffer_();
2088
2089 tags.length = 0;
2090 tags.push({pts: 0, bytes: 1});
2091
2092 // second segment
2093 standardXHRResponse(requests.pop());
2094 1979
2095 equal(bytes.length, 2, 'bytes from the second ts segment should be added'); 1980 equal(bytes.length, 2, 'bytes from the second ts segment should be added');
2096 equal(bytes[1], 1, 'the bytes from the second segment are added and not the first'); 1981 equal(bytes[1], 1, 'the bytes from the second segment are added and not the first');
...@@ -2120,10 +2005,10 @@ test('the key is supplied to the decrypter in the correct format', function() { ...@@ -2120,10 +2005,10 @@ test('the key is supplied to the decrypter in the correct format', function() {
2120 return new Uint8Array([0]); 2005 return new Uint8Array([0]);
2121 }; 2006 };
2122 2007
2008 standardXHRResponse(requests.shift()); // segment
2123 requests[0].response = new Uint32Array([0,1,2,3]).buffer; 2009 requests[0].response = new Uint32Array([0,1,2,3]).buffer;
2124 requests[0].respond(200, null, ''); 2010 requests[0].respond(200, null, '');
2125 requests.shift(); 2011 requests.shift(); // key
2126 standardXHRResponse(requests.pop());
2127 2012
2128 equal(keys.length, 1, 'only one call to decrypt was made'); 2013 equal(keys.length, 1, 'only one call to decrypt was made');
2129 deepEqual(keys[0], 2014 deepEqual(keys[0],
...@@ -2188,23 +2073,26 @@ test('switching playlists with an outstanding key request does not stall playbac ...@@ -2188,23 +2073,26 @@ test('switching playlists with an outstanding key request does not stall playbac
2188 player.hls.playlists.media = function() { 2073 player.hls.playlists.media = function() {
2189 return player.hls.playlists.master.playlists[0]; 2074 return player.hls.playlists.master.playlists[0];
2190 }; 2075 };
2191 // don't respond to the initial key request
2192 requests.shift();
2193 // first segment of the original media playlist 2076 // first segment of the original media playlist
2194 standardXHRResponse(requests.shift()); 2077 standardXHRResponse(requests.shift());
2078 // don't respond to the initial key request
2079 requests.shift();
2195 2080
2196 // "switch" media 2081 // "switch" media
2197 player.hls.playlists.trigger('mediachange'); 2082 player.hls.playlists.trigger('mediachange');
2198 2083
2199 player.trigger('timeupdate'); 2084 player.hls.checkBuffer_();
2200 2085
2201 ok(requests.length, 'made a request'); 2086 ok(requests.length, 'made a request');
2202 equal(requests[0].url, 2087 equal(requests[0].url,
2088 'http://media.example.com/fileSequence52-B.ts',
2089 'requested the segment');
2090 equal(requests[1].url,
2203 'https://priv.example.com/key.php?r=52', 2091 'https://priv.example.com/key.php?r=52',
2204 'requested the segment and key'); 2092 'requested the key');
2205 }); 2093 });
2206 2094
2207 test('resovles relative key URLs against the playlist', function() { 2095 test('resolves relative key URLs against the playlist', function() {
2208 player.src({ 2096 player.src({
2209 src: 'https://example.com/media.m3u8', 2097 src: 'https://example.com/media.m3u8',
2210 type: 'application/vnd.apple.mpegurl' 2098 type: 'application/vnd.apple.mpegurl'
...@@ -2217,6 +2105,8 @@ test('resovles relative key URLs against the playlist', function() { ...@@ -2217,6 +2105,8 @@ test('resovles relative key URLs against the playlist', function() {
2217 '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' + 2105 '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' +
2218 '#EXTINF:2.833,\n' + 2106 '#EXTINF:2.833,\n' +
2219 'http://media.example.com/fileSequence52-A.ts\n'); 2107 'http://media.example.com/fileSequence52-A.ts\n');
2108 standardXHRResponse(requests.shift()); // segment
2109
2220 equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL'); 2110 equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL');
2221 }); 2111 });
2222 2112
...@@ -2243,11 +2133,11 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2243,11 +2133,11 @@ test('treats invalid keys as a key request failure', function() {
2243 '#EXT-X-KEY:METHOD=NONE\n' + 2133 '#EXT-X-KEY:METHOD=NONE\n' +
2244 '#EXTINF:15.0,\n' + 2134 '#EXTINF:15.0,\n' +
2245 'http://media.example.com/fileSequence52-B.ts\n'); 2135 'http://media.example.com/fileSequence52-B.ts\n');
2136 // segment request
2137 standardXHRResponse(requests.shift());
2246 // keys should be 16 bytes long 2138 // keys should be 16 bytes long
2247 requests[0].response = new Uint8Array(1).buffer; 2139 requests[0].response = new Uint8Array(1).buffer;
2248 requests.shift().respond(200, null, ''); 2140 requests.shift().respond(200, null, '');
2249 // segment request
2250 standardXHRResponse(requests.shift());
2251 2141
2252 equal(requests[0].url, 'https://priv.example.com/key.php?r=52', 'retries the key'); 2142 equal(requests[0].url, 'https://priv.example.com/key.php?r=52', 'retries the key');
2253 2143
......