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.
Showing
2 changed files
with
55 additions
and
52 deletions
... | @@ -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,22 +785,19 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) { | ... | @@ -788,22 +785,19 @@ 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) { |
795 | keyXhr = videojs.Hls.xhr({ | 792 | return function(error) { |
796 | url: this.playlistUriToUrl(key.uri), | ||
797 | responseType: 'arraybuffer', | ||
798 | withCredentials: settings.withCredentials | ||
799 | }, function(err, url) { | ||
800 | keyXhr = null; | 793 | keyXhr = null; |
801 | 794 | ||
802 | if (err || !this.response || this.response.byteLength !== 16) { | 795 | if (error || !this.response || this.response.byteLength !== 16) { |
803 | key.retries = key.retries || 0; | 796 | key.retries = key.retries || 0; |
804 | key.retries++; | 797 | key.retries++; |
805 | if (!this.aborted) { | 798 | if (!this.aborted) { |
806 | tech.fetchKeys(playlist, i); | 799 | // try fetching again |
800 | tech.fetchKeys_(); | ||
807 | } | 801 | } |
808 | return; | 802 | return; |
809 | } | 803 | } |
... | @@ -815,12 +809,31 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) { | ... | @@ -815,12 +809,31 @@ videojs.Hls.prototype.fetchKeys = function(playlist, index) { |
815 | view.getUint32(8), | 809 | view.getUint32(8), |
816 | view.getUint32(12) | 810 | view.getUint32(12) |
817 | ]); | 811 | ]); |
818 | tech.fetchKeys(playlist, i++, url); | 812 | |
819 | }); | 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)) { | ||
829 | keyXhr = videojs.Hls.xhr({ | ||
830 | url: this.playlistUriToUrl(key.uri), | ||
831 | responseType: 'arraybuffer', | ||
832 | withCredentials: settings.withCredentials | ||
833 | }, receiveKey(key)); | ||
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 | ... | ... |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment