Fix seeking in live streams. Closes #308.
Showing
5 changed files
with
168 additions
and
38 deletions
... | @@ -362,5 +362,55 @@ | ... | @@ -362,5 +362,55 @@ |
362 | this.media_ = this.master.playlists[update.uri]; | 362 | this.media_ = this.master.playlists[update.uri]; |
363 | }; | 363 | }; |
364 | 364 | ||
365 | /** | ||
366 | * Determine the index of the segment that contains a specified | ||
367 | * playback position in the current media playlist. Early versions | ||
368 | * of the HLS specification require segment durations to be rounded | ||
369 | * to the nearest integer which means it may not be possible to | ||
370 | * determine the correct segment for a playback position if that | ||
371 | * position is within .5 seconds of the segment duration. This | ||
372 | * function will always return the lower of the two possible indices | ||
373 | * in those cases. | ||
374 | * | ||
375 | * @param time {number} The number of seconds since the earliest | ||
376 | * possible position to determine the containing segment for | ||
377 | * @returns {number} The number of the media segment that contains | ||
378 | * that time position. If the specified playback position is outside | ||
379 | * the time range of the current set of media segments, the return | ||
380 | * value will be clamped to the index of the segment containing the | ||
381 | * closest playback position that is currently available. | ||
382 | */ | ||
383 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { | ||
384 | var i; | ||
385 | |||
386 | if (!this.media_) { | ||
387 | return 0; | ||
388 | } | ||
389 | |||
390 | // when the requested position is earlier than the current set of | ||
391 | // segments, return the earliest segment index | ||
392 | time -= this.expiredPreDiscontinuity_ + this.expiredPostDiscontinuity_; | ||
393 | if (time < 0) { | ||
394 | return 0; | ||
395 | } | ||
396 | |||
397 | for (i = 0; i < this.media_.segments.length; i++) { | ||
398 | time -= Playlist.duration(this.media_, | ||
399 | this.media_.mediaSequence + i, | ||
400 | this.media_.mediaSequence + i + 1); | ||
401 | |||
402 | // HLS version 3 and lower round segment durations to the | ||
403 | // nearest decimal integer. When the correct media index is | ||
404 | // ambiguous, prefer the lower one. | ||
405 | if (time <= 0) { | ||
406 | return i; | ||
407 | } | ||
408 | } | ||
409 | |||
410 | // the playback position is outside the range of available | ||
411 | // segments so return the last one | ||
412 | return this.media_.segments.length - 1; | ||
413 | }; | ||
414 | |||
365 | videojs.Hls.PlaylistLoader = PlaylistLoader; | 415 | videojs.Hls.PlaylistLoader = PlaylistLoader; |
366 | })(window, window.videojs); | 416 | })(window, window.videojs); | ... | ... |
... | @@ -99,6 +99,10 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -99,6 +99,10 @@ videojs.Hls.prototype.src = function(src) { |
99 | this.playlists.dispose(); | 99 | this.playlists.dispose(); |
100 | } | 100 | } |
101 | 101 | ||
102 | // The index of the next segment to be downloaded in the current | ||
103 | // media playlist. When the current media playlist is live with | ||
104 | // expiring segments, it may be a different value from the media | ||
105 | // sequence number for a segment. | ||
102 | this.mediaIndex = 0; | 106 | this.mediaIndex = 0; |
103 | 107 | ||
104 | this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials); | 108 | this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials); |
... | @@ -313,7 +317,6 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ... | @@ -313,7 +317,6 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { |
313 | * ended. | 317 | * ended. |
314 | */ | 318 | */ |
315 | videojs.Hls.prototype.play = function() { | 319 | videojs.Hls.prototype.play = function() { |
316 | var media; | ||
317 | if (this.ended()) { | 320 | if (this.ended()) { |
318 | this.mediaIndex = 0; | 321 | this.mediaIndex = 0; |
319 | } | 322 | } |
... | @@ -323,9 +326,7 @@ videojs.Hls.prototype.play = function() { | ... | @@ -323,9 +326,7 @@ videojs.Hls.prototype.play = function() { |
323 | if (this.duration() === Infinity && | 326 | if (this.duration() === Infinity && |
324 | this.playlists.media() && | 327 | this.playlists.media() && |
325 | !this.player().hasClass('vjs-has-started')) { | 328 | !this.player().hasClass('vjs-has-started')) { |
326 | media = this.playlists.media(); | 329 | this.setCurrentTime(this.seekable().end(0)); |
327 | this.mediaIndex = videojs.Hls.getMediaIndexForLive_(media); | ||
328 | this.setCurrentTime(videojs.Hls.Playlist.seekable(media).end(0)); | ||
329 | } | 330 | } |
330 | 331 | ||
331 | // delegate back to the Flash implementation | 332 | // delegate back to the Flash implementation |
... | @@ -360,7 +361,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -360,7 +361,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
360 | this.lastSeekedTime_ = currentTime; | 361 | this.lastSeekedTime_ = currentTime; |
361 | 362 | ||
362 | // determine the requested segment | 363 | // determine the requested segment |
363 | this.mediaIndex = videojs.Hls.getMediaIndexByTime(this.playlists.media(), currentTime); | 364 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); |
364 | 365 | ||
365 | // abort any segments still being decoded | 366 | // abort any segments still being decoded |
366 | this.sourceBuffer.abort(); | 367 | this.sourceBuffer.abort(); |
... | @@ -641,7 +642,8 @@ videojs.Hls.prototype.fillBuffer = function(offset) { | ... | @@ -641,7 +642,8 @@ videojs.Hls.prototype.fillBuffer = function(offset) { |
641 | // being buffering so we don't preload data that will never be | 642 | // being buffering so we don't preload data that will never be |
642 | // played | 643 | // played |
643 | if (!this.playlists.media().endList && | 644 | if (!this.playlists.media().endList && |
644 | !this.player().hasClass('vjs-has-started')) { | 645 | !this.player().hasClass('vjs-has-started') && |
646 | offset === undefined) { | ||
645 | return; | 647 | return; |
646 | } | 648 | } |
647 | 649 | ||
... | @@ -1103,33 +1105,14 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { | ... | @@ -1103,33 +1105,14 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { |
1103 | }; | 1105 | }; |
1104 | 1106 | ||
1105 | /** | 1107 | /** |
1106 | * Determine the media index in one playlist by a time in seconds. This | 1108 | * Deprecated. |
1107 | * function iterates through the segments of a playlist and creates TimeRange | ||
1108 | * objects for each and then returns the most appropriate segment index by | ||
1109 | * checking the time value versus each range. | ||
1110 | * | 1109 | * |
1111 | * @param playlist {object} The playlist of the segments being searched. | 1110 | * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead |
1112 | * @param time {number} The time in seconds of what segment you want. | ||
1113 | * @returns {number} The media index, or -1 if none appropriate. | ||
1114 | */ | 1111 | */ |
1115 | videojs.Hls.getMediaIndexByTime = function(playlist, time) { | 1112 | videojs.Hls.getMediaIndexByTime = function() { |
1116 | var index, counter, timeRanges, currentSegmentRange; | 1113 | videojs.log.warn('getMediaIndexByTime is deprecated. ' + |
1117 | 1114 | 'Use PlaylistLoader.getMediaIndexForTime_ instead.'); | |
1118 | timeRanges = []; | 1115 | return 0; |
1119 | for (index = 0; index < playlist.segments.length; index++) { | ||
1120 | currentSegmentRange = {}; | ||
1121 | currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end; | ||
1122 | currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration; | ||
1123 | timeRanges.push(currentSegmentRange); | ||
1124 | } | ||
1125 | |||
1126 | for (counter = 0; counter < timeRanges.length; counter++) { | ||
1127 | if (time >= timeRanges[counter].start && time < timeRanges[counter].end) { | ||
1128 | return counter; | ||
1129 | } | ||
1130 | } | ||
1131 | |||
1132 | return -1; | ||
1133 | }; | 1116 | }; |
1134 | 1117 | ||
1135 | /** | 1118 | /** | ... | ... |
... | @@ -700,6 +700,69 @@ | ... | @@ -700,6 +700,69 @@ |
700 | strictEqual(mediaChanges, 2, 'ignored a no-op media change'); | 700 | strictEqual(mediaChanges, 2, 'ignored a no-op media change'); |
701 | }); | 701 | }); |
702 | 702 | ||
703 | test('can get media index by playback position for non-live videos', function() { | ||
704 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
705 | requests.shift().respond(200, null, | ||
706 | '#EXTM3U\n' + | ||
707 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
708 | '#EXTINF:4,\n' + | ||
709 | '0.ts\n' + | ||
710 | '#EXTINF:5,\n' + | ||
711 | '1.ts\n' + | ||
712 | '#EXTINF:6,\n' + | ||
713 | '2.ts\n' + | ||
714 | '#EXT-X-ENDLIST\n'); | ||
715 | |||
716 | equal(loader.getMediaIndexForTime_(-1), | ||
717 | 0, | ||
718 | 'the index is never less than zero'); | ||
719 | equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero'); | ||
720 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | ||
721 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | ||
722 | equal(loader.getMediaIndexForTime_(22), | ||
723 | 2, | ||
724 | 'the index is never greater than the length'); | ||
725 | }); | ||
726 | |||
727 | test('returns the lower index when calculating for a segment boundary', function() { | ||
728 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
729 | requests.shift().respond(200, null, | ||
730 | '#EXTM3U\n' + | ||
731 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
732 | '#EXTINF:4,\n' + | ||
733 | '0.ts\n' + | ||
734 | '#EXTINF:5,\n' + | ||
735 | '1.ts\n' + | ||
736 | '#EXT-X-ENDLIST\n'); | ||
737 | equal(loader.getMediaIndexForTime_(4), 0, 'rounds down exact matches'); | ||
738 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | ||
739 | // FIXME: the test below should pass for HLSv3 | ||
740 | //equal(loader.getMediaIndexForTime_(4.2), 0, 'rounds down'); | ||
741 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | ||
742 | }); | ||
743 | |||
744 | test('accounts for expired time when calculating media index', function() { | ||
745 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
746 | requests.shift().respond(200, null, | ||
747 | '#EXTM3U\n' + | ||
748 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | ||
749 | '#EXTINF:4,\n' + | ||
750 | '1001.ts\n' + | ||
751 | '#EXTINF:5,\n' + | ||
752 | '1002.ts\n'); | ||
753 | loader.expiredPreDiscontinuity_ = 50; | ||
754 | loader.expiredPostDiscontinuity_ = 100; | ||
755 | |||
756 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); | ||
757 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); | ||
758 | equal(loader.getMediaIndexForTime_(75), 0, 'expired content returns zero'); | ||
759 | equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position'); | ||
760 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | ||
761 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | ||
762 | equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment'); | ||
763 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | ||
764 | }); | ||
765 | |||
703 | test('does not misintrepret playlists missing newlines at the end', function() { | 766 | test('does not misintrepret playlists missing newlines at the end', function() { |
704 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 767 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); |
705 | requests.shift().respond(200, null, | 768 | requests.shift().respond(200, null, | ... | ... |
... | @@ -1663,19 +1663,33 @@ test('updates the media index when a playlist reloads', function() { | ... | @@ -1663,19 +1663,33 @@ test('updates the media index when a playlist reloads', function() { |
1663 | test('live playlist starts three target durations before live', function() { | 1663 | test('live playlist starts three target durations before live', function() { |
1664 | var mediaPlaylist; | 1664 | var mediaPlaylist; |
1665 | player.src({ | 1665 | player.src({ |
1666 | src: 'http://example.com/manifest/liveStart30sBefore.m3u8', | 1666 | src: 'live.m3u8', |
1667 | type: 'application/vnd.apple.mpegurl' | 1667 | type: 'application/vnd.apple.mpegurl' |
1668 | }); | 1668 | }); |
1669 | openMediaSource(player); | 1669 | openMediaSource(player); |
1670 | standardXHRResponse(requests.shift()); | 1670 | requests.shift().respond(200, null, |
1671 | '#EXTM3U\n' + | ||
1672 | '#EXT-X-MEDIA-SEQUENCE:101\n' + | ||
1673 | '#EXTINF:10,\n' + | ||
1674 | '0.ts\n' + | ||
1675 | '#EXTINF:10,\n' + | ||
1676 | '1.ts\n' + | ||
1677 | '#EXTINF:10,\n' + | ||
1678 | '2.ts\n' + | ||
1679 | '#EXTINF:10,\n' + | ||
1680 | '3.ts\n' + | ||
1681 | '#EXTINF:10,\n' + | ||
1682 | '4.ts\n'); | ||
1671 | 1683 | ||
1672 | equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering'); | 1684 | equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering'); |
1673 | equal(requests.length, 0, 'no outstanding segment request'); | 1685 | equal(requests.length, 0, 'no outstanding segment request'); |
1674 | 1686 | ||
1675 | player.play(); | 1687 | player.play(); |
1676 | mediaPlaylist = player.hls.playlists.media(); | 1688 | mediaPlaylist = player.hls.playlists.media(); |
1677 | equal(player.hls.mediaIndex, 6, 'mediaIndex is updated at play'); | 1689 | equal(player.hls.mediaIndex, 1, 'mediaIndex is updated at play'); |
1678 | equal(player.currentTime(), videojs.Hls.Playlist.seekable(mediaPlaylist).end(0)); | 1690 | equal(player.currentTime(), player.seekable().end(0)); |
1691 | |||
1692 | equal(requests.length, 1, 'begins buffering'); | ||
1679 | }); | 1693 | }); |
1680 | 1694 | ||
1681 | test('does not reset live currentTime if mediaIndex is one beyond the last available segment', function() { | 1695 | test('does not reset live currentTime if mediaIndex is one beyond the last available segment', function() { |
... | @@ -1728,6 +1742,24 @@ test('mediaIndex is zero before the first segment loads', function() { | ... | @@ -1728,6 +1742,24 @@ test('mediaIndex is zero before the first segment loads', function() { |
1728 | strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero'); | 1742 | strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero'); |
1729 | }); | 1743 | }); |
1730 | 1744 | ||
1745 | test('mediaIndex returns correctly at playlist boundaries', function() { | ||
1746 | player.src({ | ||
1747 | src: 'http://example.com/master.m3u8', | ||
1748 | type: 'application/vnd.apple.mpegurl' | ||
1749 | }); | ||
1750 | |||
1751 | openMediaSource(player); | ||
1752 | standardXHRResponse(requests.shift()); // master | ||
1753 | standardXHRResponse(requests.shift()); // media | ||
1754 | |||
1755 | strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero at first segment'); | ||
1756 | |||
1757 | // seek to end | ||
1758 | player.currentTime(40); | ||
1759 | |||
1760 | strictEqual(player.hls.mediaIndex, 3, 'mediaIndex is 3 at last segment'); | ||
1761 | }); | ||
1762 | |||
1731 | test('reloads out-of-date live playlists when switching variants', function() { | 1763 | test('reloads out-of-date live playlists when switching variants', function() { |
1732 | player.src({ | 1764 | player.src({ |
1733 | src: 'http://example.com/master.m3u8', | 1765 | src: 'http://example.com/master.m3u8', |
... | @@ -1919,18 +1951,19 @@ test('continues playing after seek to discontinuity', function() { | ... | @@ -1919,18 +1951,19 @@ test('continues playing after seek to discontinuity', function() { |
1919 | '#EXTINF:10,0\n' + | 1951 | '#EXTINF:10,0\n' + |
1920 | '2.ts\n' + | 1952 | '2.ts\n' + |
1921 | '#EXT-X-ENDLIST\n'); | 1953 | '#EXT-X-ENDLIST\n'); |
1922 | standardXHRResponse(requests.pop()); | 1954 | standardXHRResponse(requests.pop()); // 1.ts |
1923 | 1955 | ||
1924 | currentTime = 1; | 1956 | currentTime = 1; |
1925 | bufferEnd = 10; | 1957 | bufferEnd = 10; |
1926 | player.hls.checkBuffer_(); | 1958 | player.hls.checkBuffer_(); |
1927 | 1959 | ||
1928 | standardXHRResponse(requests.pop()); | 1960 | standardXHRResponse(requests.pop()); // 2.ts |
1929 | 1961 | ||
1930 | // seek to the discontinuity | 1962 | // seek to the discontinuity |
1931 | player.currentTime(10); | 1963 | player.currentTime(10); |
1932 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | 1964 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); |
1933 | standardXHRResponse(requests.pop()); | 1965 | tags.push({ pts: 11 * 1000, bytes: new Uint8Array(1) }); |
1966 | standardXHRResponse(requests.pop()); // 1.ts, again | ||
1934 | strictEqual(aborts, 1, 'aborted once for the seek'); | 1967 | strictEqual(aborts, 1, 'aborted once for the seek'); |
1935 | 1968 | ||
1936 | // the source buffer empties. is 2.ts still in the segment buffer? | 1969 | // the source buffer empties. is 2.ts still in the segment buffer? | ... | ... |
-
Please register or sign in to post a comment