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
......