72f9507d by David LaPalomento

Account for expired segments when seeking

Deprecate getMediaIndexByTime and replace it with a PlaylistLoader.getMediaIndexForTime_ that considers expired content in live playlists. Fix an issues that would allow the return value to be less than zero or greater than the index of the last available media segment. Currently, this code does not take into account rounding of segment durations in HLS v3.
1 parent d9afdb77
...@@ -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);
...@@ -360,7 +364,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -360,7 +364,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
360 this.lastSeekedTime_ = currentTime; 364 this.lastSeekedTime_ = currentTime;
361 365
362 // determine the requested segment 366 // determine the requested segment
363 this.mediaIndex = videojs.Hls.getMediaIndexByTime(this.playlists.media(), currentTime); 367 this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime);
364 368
365 // abort any segments still being decoded 369 // abort any segments still being decoded
366 this.sourceBuffer.abort(); 370 this.sourceBuffer.abort();
...@@ -1103,41 +1107,14 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { ...@@ -1103,41 +1107,14 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
1103 }; 1107 };
1104 1108
1105 /** 1109 /**
1106 * Determine the media index in one playlist by a time in seconds. This 1110 * 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 * 1111 *
1111 * @param playlist {object} The playlist of the segments being searched. 1112 * @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 */ 1113 */
1115 videojs.Hls.getMediaIndexByTime = function(playlist, time) { 1114 videojs.Hls.getMediaIndexByTime = function() {
1116 var index, counter, timeRanges, currentSegmentRange; 1115 videojs.log.warn('getMediaIndexByTime is deprecated. ' +
1117 1116 'Use PlaylistLoader.getMediaIndexForTime_ instead.');
1118 if (time === 0) { 1117 return 0;
1119 return 0;
1120 }
1121
1122 timeRanges = [];
1123 for (index = 0; index < playlist.segments.length; index++) {
1124 currentSegmentRange = {};
1125 currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end;
1126 currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration;
1127 timeRanges.push(currentSegmentRange);
1128 }
1129
1130 if (time >= timeRanges[timeRanges.length - 1].end) {
1131 return (playlist.segments.length - 1);
1132 }
1133
1134 for (counter = 0; counter < timeRanges.length; counter++) {
1135 if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
1136 return counter;
1137 }
1138 }
1139
1140 return -1;
1141 }; 1118 };
1142 1119
1143 /** 1120 /**
......
...@@ -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,
......
...@@ -1937,18 +1937,19 @@ test('continues playing after seek to discontinuity', function() { ...@@ -1937,18 +1937,19 @@ test('continues playing after seek to discontinuity', function() {
1937 '#EXTINF:10,0\n' + 1937 '#EXTINF:10,0\n' +
1938 '2.ts\n' + 1938 '2.ts\n' +
1939 '#EXT-X-ENDLIST\n'); 1939 '#EXT-X-ENDLIST\n');
1940 standardXHRResponse(requests.pop()); 1940 standardXHRResponse(requests.pop()); // 1.ts
1941 1941
1942 currentTime = 1; 1942 currentTime = 1;
1943 bufferEnd = 10; 1943 bufferEnd = 10;
1944 player.hls.checkBuffer_(); 1944 player.hls.checkBuffer_();
1945 1945
1946 standardXHRResponse(requests.pop()); 1946 standardXHRResponse(requests.pop()); // 2.ts
1947 1947
1948 // seek to the discontinuity 1948 // seek to the discontinuity
1949 player.currentTime(10); 1949 player.currentTime(10);
1950 tags.push({ pts: 0, bytes: new Uint8Array(1) }); 1950 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1951 standardXHRResponse(requests.pop()); 1951 tags.push({ pts: 11 * 1000, bytes: new Uint8Array(1) });
1952 standardXHRResponse(requests.pop()); // 1.ts, again
1952 strictEqual(aborts, 1, 'aborted once for the seek'); 1953 strictEqual(aborts, 1, 'aborted once for the seek');
1953 1954
1954 // the source buffer empties. is 2.ts still in the segment buffer? 1955 // the source buffer empties. is 2.ts still in the segment buffer?
......