dcce9f45 by David LaPalomento

Fix seeking in live streams. Closes #308.

2 parents c6c973f6 6b05241f
...@@ -3,6 +3,7 @@ CHANGELOG ...@@ -3,6 +3,7 @@ CHANGELOG
3 3
4 ## HEAD (Unreleased) 4 ## HEAD (Unreleased)
5 5
6 * @dmlap fix seeking in live streams ([view](https://github.com/videojs/videojs-contrib-hls/pull/308))
6 7
7 -------------------- 8 --------------------
8 9
......
...@@ -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?
......