4624510d by David LaPalomento

Merge pull request #246 from videojs/live-hlse-fixes

Fix discontinuities
2 parents 021896e3 4222a1f8
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
70 type="application/x-mpegURL"> 70 type="application/x-mpegURL">
71 </video> 71 </video>
72 <script> 72 <script>
73 videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf'; 73 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
74 // initialize the player 74 // initialize the player
75 var player = videojs('video'); 75 var player = videojs('video');
76 </script> 76 </script>
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
43 }, 43 },
44 "dependencies": { 44 "dependencies": {
45 "pkcs7": "^0.2.2", 45 "pkcs7": "^0.2.2",
46 "videojs-contrib-media-sources": "^0.3.0" 46 "videojs-contrib-media-sources": "^0.3.0",
47 "videojs-swf": "^4.6.0"
47 } 48 }
48 } 49 }
......
...@@ -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
......