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.
Showing
4 changed files
with
128 additions
and
37 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); |
... | @@ -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? | ... | ... |
-
Please register or sign in to post a comment