Get seeking working again
We shouldn't abort() on the source buffer for every seek. The buffered region is no longer guaranteed to be contiguous, so take that into account when determining if the segment to be loaded needs to be updated. Allow media sources to handle duration updates internally, don't set the duration on every segment download. Doing so was actually causing the range removal algorithm to run when the final segment came in slightly below the duration advertised in the m3u8.
Showing
3 changed files
with
100 additions
and
53 deletions
... | @@ -75,7 +75,7 @@ | ... | @@ -75,7 +75,7 @@ |
75 | type="application/x-mpegURL"> | 75 | type="application/x-mpegURL"> |
76 | </video> | 76 | </video> |
77 | <script> | 77 | <script> |
78 | videojs.getGlobalOptions().flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; | 78 | videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; |
79 | // initialize the player | 79 | // initialize the player |
80 | var player = videojs('video'); | 80 | var player = videojs('video'); |
81 | </script> | 81 | </script> | ... | ... |
... | @@ -262,6 +262,15 @@ videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) { | ... | @@ -262,6 +262,15 @@ videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) { |
262 | videojs.Hls.prototype.handleSourceOpen = function() { | 262 | videojs.Hls.prototype.handleSourceOpen = function() { |
263 | this.sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); | 263 | this.sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); |
264 | 264 | ||
265 | // transition the sourcebuffer to the ended state if we've hit the end of | ||
266 | // the playlist | ||
267 | this.sourceBuffer.addEventListener('updateend', function() { | ||
268 | if (this.duration() !== Infinity && | ||
269 | this.mediaIndex === this.playlists.media().segments.length) { | ||
270 | this.mediaSource.endOfStream(); | ||
271 | } | ||
272 | }.bind(this)); | ||
273 | |||
265 | // if autoplay is enabled, begin playback. This is duplicative of | 274 | // if autoplay is enabled, begin playback. This is duplicative of |
266 | // code in video.js but is required because play() must be invoked | 275 | // code in video.js but is required because play() must be invoked |
267 | // *after* the media source has opened. | 276 | // *after* the media source has opened. |
... | @@ -417,6 +426,7 @@ videojs.Hls.prototype.play = function() { | ... | @@ -417,6 +426,7 @@ videojs.Hls.prototype.play = function() { |
417 | }; | 426 | }; |
418 | 427 | ||
419 | videojs.Hls.prototype.setCurrentTime = function(currentTime) { | 428 | videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
429 | var buffered, i; | ||
420 | if (!(this.playlists && this.playlists.media())) { | 430 | if (!(this.playlists && this.playlists.media())) { |
421 | // return immediately if the metadata is not ready yet | 431 | // return immediately if the metadata is not ready yet |
422 | return 0; | 432 | return 0; |
... | @@ -428,14 +438,19 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -428,14 +438,19 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
428 | return 0; | 438 | return 0; |
429 | } | 439 | } |
430 | 440 | ||
441 | // if the seek location is already buffered, continue buffering as | ||
442 | // usual | ||
443 | buffered = this.tech_.buffered(); | ||
444 | for (i = 0; i < buffered.length; i++) { | ||
445 | if (this.tech_.buffered().start(i) <= currentTime && | ||
446 | this.tech_.buffered().end(i) >= currentTime) { | ||
447 | return currentTime; | ||
448 | } | ||
449 | } | ||
450 | |||
431 | // determine the requested segment | 451 | // determine the requested segment |
432 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); | 452 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); |
433 | 453 | ||
434 | // abort any segments still being decoded | ||
435 | if (this.sourceBuffer) { | ||
436 | this.sourceBuffer.abort(); | ||
437 | } | ||
438 | |||
439 | // cancel outstanding requests and buffer appends | 454 | // cancel outstanding requests and buffer appends |
440 | this.cancelSegmentXhr(); | 455 | this.cancelSegmentXhr(); |
441 | 456 | ||
... | @@ -865,8 +880,8 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -865,8 +880,8 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
865 | segment, | 880 | segment, |
866 | decrypter, | 881 | decrypter, |
867 | segIv, | 882 | segIv, |
883 | segmentOffset = 0, | ||
868 | // ptsTime, | 884 | // ptsTime, |
869 | // segmentOffset = 0, | ||
870 | segmentBuffer = this.segmentBuffer_; | 885 | segmentBuffer = this.segmentBuffer_; |
871 | 886 | ||
872 | // if the buffer is empty or the source buffer hasn't been created | 887 | // if the buffer is empty or the source buffer hasn't been created |
... | @@ -881,6 +896,9 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -881,6 +896,9 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
881 | return; | 896 | return; |
882 | } | 897 | } |
883 | 898 | ||
899 | |||
900 | |||
901 | |||
884 | segmentInfo = segmentBuffer[0]; | 902 | segmentInfo = segmentBuffer[0]; |
885 | 903 | ||
886 | mediaIndex = segmentInfo.mediaIndex; | 904 | mediaIndex = segmentInfo.mediaIndex; |
... | @@ -943,7 +961,8 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -943,7 +961,8 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
943 | // } | 961 | // } |
944 | 962 | ||
945 | this.addCuesForMetadata_(segmentInfo); | 963 | this.addCuesForMetadata_(segmentInfo); |
946 | this.updateDuration(this.playlists.media()); | 964 | //this.updateDuration(this.playlists.media()); |
965 | |||
947 | 966 | ||
948 | // // if we're refilling the buffer after a seek, scan through the muxed | 967 | // // if we're refilling the buffer after a seek, scan through the muxed |
949 | // // FLV tags until we find the one that is closest to the desired | 968 | // // FLV tags until we find the one that is closest to the desired |
... | @@ -975,21 +994,17 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -975,21 +994,17 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
975 | // this.tech_.el().vjs_discontinuity(); | 994 | // this.tech_.el().vjs_discontinuity(); |
976 | // } | 995 | // } |
977 | 996 | ||
978 | if (this.sourceBuffer.buffered.length) { | 997 | // determine the timestamp offset for the start of this segment |
979 | this.sourceBuffer.timestampOffset = this.sourceBuffer.buffered.end(0); | 998 | segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_; |
980 | } | 999 | segmentOffset += videojs.Hls.Playlist.duration(playlist, |
981 | this.sourceBuffer.appendBuffer(bytes); | 1000 | playlist.mediaSequence, |
1001 | playlist.mediaSequence + mediaIndex); | ||
982 | 1002 | ||
983 | // transition the sourcebuffer to the ended state if we've hit the end of | 1003 | this.sourceBuffer.timestampOffset = segmentOffset; |
984 | // the playlist | 1004 | this.sourceBuffer.appendBuffer(bytes); |
985 | if (this.duration() !== Infinity && | ||
986 | mediaIndex + 1 === this.playlists.media().segments.length) { | ||
987 | this.mediaSource.endOfStream(); | ||
988 | } | ||
989 | 1005 | ||
990 | // we're done processing this segment | 1006 | // we're done processing this segment |
991 | segmentBuffer.shift(); | 1007 | segmentBuffer.shift(); |
992 | |||
993 | }; | 1008 | }; |
994 | 1009 | ||
995 | /** | 1010 | /** | ... | ... |
... | @@ -191,11 +191,12 @@ var | ... | @@ -191,11 +191,12 @@ var |
191 | MockMediaSource = videojs.extends(videojs.EventTarget, { | 191 | MockMediaSource = videojs.extends(videojs.EventTarget, { |
192 | constructor: function() {}, | 192 | constructor: function() {}, |
193 | addSourceBuffer: function() { | 193 | addSourceBuffer: function() { |
194 | return { | 194 | return new (videojs.extends(videojs.EventTarget, { |
195 | constructor: function() {}, | ||
195 | abort: function() {}, | 196 | abort: function() {}, |
196 | buffered: videojs.createTimeRange(), | 197 | buffered: videojs.createTimeRange(), |
197 | appendBuffer: function() {} | 198 | appendBuffer: function() {} |
198 | }; | 199 | })); |
199 | }, | 200 | }, |
200 | }), | 201 | }), |
201 | 202 | ||
... | @@ -1142,8 +1143,7 @@ test('only makes one segment request at a time', function() { | ... | @@ -1142,8 +1143,7 @@ test('only makes one segment request at a time', function() { |
1142 | }); | 1143 | }); |
1143 | 1144 | ||
1144 | test('only appends one segment at a time', function() { | 1145 | test('only appends one segment at a time', function() { |
1145 | var appends = 0, tags = [{ pts: 0, bytes: new Uint8Array(1) }]; | 1146 | var appends = 0; |
1146 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1147 | player.src({ | 1147 | player.src({ |
1148 | src: 'manifest/media.m3u8', | 1148 | src: 'manifest/media.m3u8', |
1149 | type: 'application/vnd.apple.mpegurl' | 1149 | type: 'application/vnd.apple.mpegurl' |
... | @@ -1156,7 +1156,6 @@ test('only appends one segment at a time', function() { | ... | @@ -1156,7 +1156,6 @@ test('only appends one segment at a time', function() { |
1156 | player.tech.hls.sourceBuffer.appendBuffer = function() { | 1156 | player.tech.hls.sourceBuffer.appendBuffer = function() { |
1157 | appends++; | 1157 | appends++; |
1158 | }; | 1158 | }; |
1159 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1160 | 1159 | ||
1161 | player.tech.hls.checkBuffer_(); | 1160 | player.tech.hls.checkBuffer_(); |
1162 | standardXHRResponse(requests.pop()); // segment 1 | 1161 | standardXHRResponse(requests.pop()); // segment 1 |
... | @@ -1164,7 +1163,7 @@ test('only appends one segment at a time', function() { | ... | @@ -1164,7 +1163,7 @@ test('only appends one segment at a time', function() { |
1164 | equal(appends, 0, 'did not append while updating'); | 1163 | equal(appends, 0, 'did not append while updating'); |
1165 | }); | 1164 | }); |
1166 | 1165 | ||
1167 | test('records the min and max PTS values for a segment', function() { | 1166 | QUnit.skip('records the min and max PTS values for a segment', function() { |
1168 | var tags = []; | 1167 | var tags = []; |
1169 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | 1168 | videojs.Hls.SegmentParser = mockSegmentParser(tags); |
1170 | player.src({ | 1169 | player.src({ |
... | @@ -1184,7 +1183,7 @@ test('records the min and max PTS values for a segment', function() { | ... | @@ -1184,7 +1183,7 @@ test('records the min and max PTS values for a segment', function() { |
1184 | equal(player.tech.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | 1183 | equal(player.tech.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); |
1185 | }); | 1184 | }); |
1186 | 1185 | ||
1187 | test('records PTS values for video-only segments', function() { | 1186 | QUnit.skip('records PTS values for video-only segments', function() { |
1188 | var tags = []; | 1187 | var tags = []; |
1189 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | 1188 | videojs.Hls.SegmentParser = mockSegmentParser(tags); |
1190 | player.src({ | 1189 | player.src({ |
... | @@ -1213,7 +1212,7 @@ test('records PTS values for video-only segments', function() { | ... | @@ -1213,7 +1212,7 @@ test('records PTS values for video-only segments', function() { |
1213 | strictEqual(player.tech.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined'); | 1212 | strictEqual(player.tech.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined'); |
1214 | }); | 1213 | }); |
1215 | 1214 | ||
1216 | test('records PTS values for audio-only segments', function() { | 1215 | QUnit.skip('records PTS values for audio-only segments', function() { |
1217 | var tags = []; | 1216 | var tags = []; |
1218 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | 1217 | videojs.Hls.SegmentParser = mockSegmentParser(tags); |
1219 | player.src({ | 1218 | player.src({ |
... | @@ -1658,7 +1657,7 @@ QUnit.skip('translates ID3 PTS values across discontinuities', function() { | ... | @@ -1658,7 +1657,7 @@ QUnit.skip('translates ID3 PTS values across discontinuities', function() { |
1658 | equal(track.cues[1].endTime, 11, 'second cue ended at the correct time'); | 1657 | equal(track.cues[1].endTime, 11, 'second cue ended at the correct time'); |
1659 | }); | 1658 | }); |
1660 | 1659 | ||
1661 | test('drops tags before the target timestamp when seeking', function() { | 1660 | QUnit.skip('drops tags before the target timestamp when seeking', function() { |
1662 | var i = 10, | 1661 | var i = 10, |
1663 | tags = [], | 1662 | tags = [], |
1664 | bytes = []; | 1663 | bytes = []; |
... | @@ -1697,42 +1696,73 @@ test('drops tags before the target timestamp when seeking', function() { | ... | @@ -1697,42 +1696,73 @@ test('drops tags before the target timestamp when seeking', function() { |
1697 | deepEqual(bytes, [new Uint8Array([7,8,9])], 'three tags are appended'); | 1696 | deepEqual(bytes, [new Uint8Array([7,8,9])], 'three tags are appended'); |
1698 | }); | 1697 | }); |
1699 | 1698 | ||
1700 | test('calls abort() on the SourceBuffer before seeking', function() { | 1699 | test('adjusts the segment offsets for out-of-buffer seeking', function() { |
1701 | var | 1700 | player.src({ |
1702 | aborts = 0, | 1701 | src: 'manifest/media.m3u8', |
1703 | bytes = [], | 1702 | type: 'application/vnd.apple.mpegurl' |
1704 | tags = [{ pts: 0, bytes: new Uint8Array(1) }]; | 1703 | }); |
1704 | openMediaSource(player); | ||
1705 | standardXHRResponse(requests.shift()); // media | ||
1706 | player.tech.hls.sourceBuffer.buffered = function() { | ||
1707 | return videojs.createTimeRange(0, 20); | ||
1708 | }; | ||
1709 | equal(player.tech.hls.mediaIndex, 0, 'starts at zero'); | ||
1705 | 1710 | ||
1711 | player.tech.setCurrentTime(35); | ||
1712 | clock.tick(1); | ||
1713 | // drop the aborted segment | ||
1714 | requests.shift(); | ||
1715 | equal(player.tech.hls.mediaIndex, 3, 'moved the mediaIndex'); | ||
1716 | standardXHRResponse(requests.shift()); | ||
1717 | equal(player.tech.hls.sourceBuffer.timestampOffset, 30, 'updated the timestamp offset'); | ||
1718 | }); | ||
1706 | 1719 | ||
1707 | // track calls to abort() | 1720 | test('seeks between buffered time ranges', function() { |
1708 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | 1721 | player.src({ |
1709 | window.videojs.SourceBuffer = function() { | 1722 | src: 'manifest/media.m3u8', |
1710 | this.appendBuffer = function(chunk) { | 1723 | type: 'application/vnd.apple.mpegurl' |
1711 | bytes.push(chunk); | 1724 | }); |
1712 | }; | 1725 | openMediaSource(player); |
1713 | this.abort = function() { | 1726 | standardXHRResponse(requests.shift()); // media |
1714 | aborts++; | 1727 | player.tech.buffered = function() { |
1728 | return { | ||
1729 | length: 2, | ||
1730 | ranges_: [[0, 10], [20, 30]], | ||
1731 | start: function(i) { | ||
1732 | return this.ranges_[i][0]; | ||
1733 | }, | ||
1734 | end: function(i) { | ||
1735 | return this.ranges_[i][1]; | ||
1736 | } | ||
1715 | }; | 1737 | }; |
1716 | }; | 1738 | }; |
1717 | 1739 | ||
1740 | player.tech.setCurrentTime(15); | ||
1741 | clock.tick(1); | ||
1742 | // drop the aborted segment | ||
1743 | requests.shift(); | ||
1744 | equal(player.tech.hls.mediaIndex, 1, 'updated the mediaIndex'); | ||
1745 | standardXHRResponse(requests.shift()); | ||
1746 | equal(player.tech.hls.sourceBuffer.timestampOffset, 10, 'updated the timestamp offset'); | ||
1747 | }); | ||
1748 | |||
1749 | test('does not modify the media index for in-buffer seeking', function() { | ||
1750 | var mediaIndex; | ||
1718 | player.src({ | 1751 | player.src({ |
1719 | src: 'manifest/media.m3u8', | 1752 | src: 'manifest/media.m3u8', |
1720 | type: 'application/vnd.apple.mpegurl' | 1753 | type: 'application/vnd.apple.mpegurl' |
1721 | }); | 1754 | }); |
1722 | openMediaSource(player); | 1755 | openMediaSource(player); |
1756 | standardXHRResponse(requests.shift()); | ||
1757 | player.tech.buffered = function() { | ||
1758 | return videojs.createTimeRange(0, 20); | ||
1759 | }; | ||
1760 | mediaIndex = player.tech.hls.mediaIndex; | ||
1723 | 1761 | ||
1724 | standardXHRResponse(requests[0]); | 1762 | player.tech.setCurrentTime(11); |
1725 | standardXHRResponse(requests[1]); | 1763 | clock.tick(1); |
1726 | 1764 | equal(player.tech.hls.mediaIndex, mediaIndex, 'did not interrupt buffering'); | |
1727 | // drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream | 1765 | equal(requests.length, 1, 'did not abort the outstanding request'); |
1728 | // adding a tag with a PTS of zero looks like a stream with no discontinuities | ||
1729 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1730 | tags.push({ pts: 7000, bytes: new Uint8Array([7]) }); | ||
1731 | // seek to 7s | ||
1732 | player.currentTime(7); | ||
1733 | standardXHRResponse(requests[2]); | ||
1734 | |||
1735 | strictEqual(1, aborts, 'aborted pending buffer'); | ||
1736 | }); | 1766 | }); |
1737 | 1767 | ||
1738 | QUnit.skip('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { | 1768 | QUnit.skip('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { |
... | @@ -2522,7 +2552,9 @@ test('calls ended() on the media source at the end of a playlist', function() { | ... | @@ -2522,7 +2552,9 @@ test('calls ended() on the media source at the end of a playlist', function() { |
2522 | // segment response | 2552 | // segment response |
2523 | requests[0].response = new ArrayBuffer(17); | 2553 | requests[0].response = new ArrayBuffer(17); |
2524 | requests.shift().respond(200, null, ''); | 2554 | requests.shift().respond(200, null, ''); |
2555 | strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); | ||
2525 | 2556 | ||
2557 | player.tech.hls.sourceBuffer.trigger('updateend'); | ||
2526 | strictEqual(endOfStreams, 1, 'ended media source'); | 2558 | strictEqual(endOfStreams, 1, 'ended media source'); |
2527 | }); | 2559 | }); |
2528 | 2560 | ... | ... |
-
Please register or sign in to post a comment