diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1dc324..a828c0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ CHANGELOG ## HEAD (Unreleased) +* @dmlap fix seeking in live streams ([view](https://github.com/videojs/videojs-contrib-hls/pull/308)) -------------------- diff --git a/src/playlist-loader.js b/src/playlist-loader.js index 0d6ae109..338783a9 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -362,5 +362,55 @@ this.media_ = this.master.playlists[update.uri]; }; + /** + * Determine the index of the segment that contains a specified + * playback position in the current media playlist. Early versions + * of the HLS specification require segment durations to be rounded + * to the nearest integer which means it may not be possible to + * determine the correct segment for a playback position if that + * position is within .5 seconds of the segment duration. This + * function will always return the lower of the two possible indices + * in those cases. + * + * @param time {number} The number of seconds since the earliest + * possible position to determine the containing segment for + * @returns {number} The number of the media segment that contains + * that time position. If the specified playback position is outside + * the time range of the current set of media segments, the return + * value will be clamped to the index of the segment containing the + * closest playback position that is currently available. + */ + PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { + var i; + + if (!this.media_) { + return 0; + } + + // when the requested position is earlier than the current set of + // segments, return the earliest segment index + time -= this.expiredPreDiscontinuity_ + this.expiredPostDiscontinuity_; + if (time < 0) { + return 0; + } + + for (i = 0; i < this.media_.segments.length; i++) { + time -= Playlist.duration(this.media_, + this.media_.mediaSequence + i, + this.media_.mediaSequence + i + 1); + + // HLS version 3 and lower round segment durations to the + // nearest decimal integer. When the correct media index is + // ambiguous, prefer the lower one. + if (time <= 0) { + return i; + } + } + + // the playback position is outside the range of available + // segments so return the last one + return this.media_.segments.length - 1; + }; + videojs.Hls.PlaylistLoader = PlaylistLoader; })(window, window.videojs); diff --git a/src/videojs-hls.js b/src/videojs-hls.js index 78438fdf..21a97f2f 100644 --- a/src/videojs-hls.js +++ b/src/videojs-hls.js @@ -99,6 +99,10 @@ videojs.Hls.prototype.src = function(src) { this.playlists.dispose(); } + // The index of the next segment to be downloaded in the current + // media playlist. When the current media playlist is live with + // expiring segments, it may be a different value from the media + // sequence number for a segment. this.mediaIndex = 0; this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials); @@ -313,7 +317,6 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { * ended. */ videojs.Hls.prototype.play = function() { - var media; if (this.ended()) { this.mediaIndex = 0; } @@ -323,9 +326,7 @@ videojs.Hls.prototype.play = function() { if (this.duration() === Infinity && this.playlists.media() && !this.player().hasClass('vjs-has-started')) { - media = this.playlists.media(); - this.mediaIndex = videojs.Hls.getMediaIndexForLive_(media); - this.setCurrentTime(videojs.Hls.Playlist.seekable(media).end(0)); + this.setCurrentTime(this.seekable().end(0)); } // delegate back to the Flash implementation @@ -360,7 +361,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { this.lastSeekedTime_ = currentTime; // determine the requested segment - this.mediaIndex = videojs.Hls.getMediaIndexByTime(this.playlists.media(), currentTime); + this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); // abort any segments still being decoded this.sourceBuffer.abort(); @@ -641,7 +642,8 @@ videojs.Hls.prototype.fillBuffer = function(offset) { // being buffering so we don't preload data that will never be // played if (!this.playlists.media().endList && - !this.player().hasClass('vjs-has-started')) { + !this.player().hasClass('vjs-has-started') && + offset === undefined) { return; } @@ -1103,33 +1105,14 @@ videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { }; /** - * Determine the media index in one playlist by a time in seconds. This - * function iterates through the segments of a playlist and creates TimeRange - * objects for each and then returns the most appropriate segment index by - * checking the time value versus each range. + * Deprecated. * - * @param playlist {object} The playlist of the segments being searched. - * @param time {number} The time in seconds of what segment you want. - * @returns {number} The media index, or -1 if none appropriate. + * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead */ -videojs.Hls.getMediaIndexByTime = function(playlist, time) { - var index, counter, timeRanges, currentSegmentRange; - - timeRanges = []; - for (index = 0; index < playlist.segments.length; index++) { - currentSegmentRange = {}; - currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end; - currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration; - timeRanges.push(currentSegmentRange); - } - - for (counter = 0; counter < timeRanges.length; counter++) { - if (time >= timeRanges[counter].start && time < timeRanges[counter].end) { - return counter; - } - } - - return -1; +videojs.Hls.getMediaIndexByTime = function() { + videojs.log.warn('getMediaIndexByTime is deprecated. ' + + 'Use PlaylistLoader.getMediaIndexForTime_ instead.'); + return 0; }; /** diff --git a/test/playlist-loader_test.js b/test/playlist-loader_test.js index 619683d0..a8461df8 100644 --- a/test/playlist-loader_test.js +++ b/test/playlist-loader_test.js @@ -700,6 +700,69 @@ strictEqual(mediaChanges, 2, 'ignored a no-op media change'); }); + test('can get media index by playback position for non-live videos', function() { + var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); + requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXTINF:4,\n' + + '0.ts\n' + + '#EXTINF:5,\n' + + '1.ts\n' + + '#EXTINF:6,\n' + + '2.ts\n' + + '#EXT-X-ENDLIST\n'); + + equal(loader.getMediaIndexForTime_(-1), + 0, + 'the index is never less than zero'); + equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero'); + equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); + equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); + equal(loader.getMediaIndexForTime_(22), + 2, + 'the index is never greater than the length'); + }); + + test('returns the lower index when calculating for a segment boundary', function() { + var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); + requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXTINF:4,\n' + + '0.ts\n' + + '#EXTINF:5,\n' + + '1.ts\n' + + '#EXT-X-ENDLIST\n'); + equal(loader.getMediaIndexForTime_(4), 0, 'rounds down exact matches'); + equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); + // FIXME: the test below should pass for HLSv3 + //equal(loader.getMediaIndexForTime_(4.2), 0, 'rounds down'); + equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); + }); + + test('accounts for expired time when calculating media index', function() { + var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); + requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:1001\n' + + '#EXTINF:4,\n' + + '1001.ts\n' + + '#EXTINF:5,\n' + + '1002.ts\n'); + loader.expiredPreDiscontinuity_ = 50; + loader.expiredPostDiscontinuity_ = 100; + + equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); + equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); + equal(loader.getMediaIndexForTime_(75), 0, 'expired content returns zero'); + equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position'); + equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); + equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); + equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment'); + equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); + }); + test('does not misintrepret playlists missing newlines at the end', function() { var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); requests.shift().respond(200, null, diff --git a/test/videojs-hls_test.js b/test/videojs-hls_test.js index 5fae5449..a583d00d 100644 --- a/test/videojs-hls_test.js +++ b/test/videojs-hls_test.js @@ -1663,19 +1663,33 @@ test('updates the media index when a playlist reloads', function() { test('live playlist starts three target durations before live', function() { var mediaPlaylist; player.src({ - src: 'http://example.com/manifest/liveStart30sBefore.m3u8', + src: 'live.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); - standardXHRResponse(requests.shift()); + requests.shift().respond(200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:101\n' + + '#EXTINF:10,\n' + + '0.ts\n' + + '#EXTINF:10,\n' + + '1.ts\n' + + '#EXTINF:10,\n' + + '2.ts\n' + + '#EXTINF:10,\n' + + '3.ts\n' + + '#EXTINF:10,\n' + + '4.ts\n'); equal(player.hls.mediaIndex, 0, 'waits for the first play to start buffering'); equal(requests.length, 0, 'no outstanding segment request'); player.play(); mediaPlaylist = player.hls.playlists.media(); - equal(player.hls.mediaIndex, 6, 'mediaIndex is updated at play'); - equal(player.currentTime(), videojs.Hls.Playlist.seekable(mediaPlaylist).end(0)); + equal(player.hls.mediaIndex, 1, 'mediaIndex is updated at play'); + equal(player.currentTime(), player.seekable().end(0)); + + equal(requests.length, 1, 'begins buffering'); }); 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() { strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero'); }); +test('mediaIndex returns correctly at playlist boundaries', function() { + player.src({ + src: 'http://example.com/master.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + openMediaSource(player); + standardXHRResponse(requests.shift()); // master + standardXHRResponse(requests.shift()); // media + + strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero at first segment'); + + // seek to end + player.currentTime(40); + + strictEqual(player.hls.mediaIndex, 3, 'mediaIndex is 3 at last segment'); +}); + test('reloads out-of-date live playlists when switching variants', function() { player.src({ src: 'http://example.com/master.m3u8', @@ -1919,18 +1951,19 @@ test('continues playing after seek to discontinuity', function() { '#EXTINF:10,0\n' + '2.ts\n' + '#EXT-X-ENDLIST\n'); - standardXHRResponse(requests.pop()); + standardXHRResponse(requests.pop()); // 1.ts currentTime = 1; bufferEnd = 10; player.hls.checkBuffer_(); - standardXHRResponse(requests.pop()); + standardXHRResponse(requests.pop()); // 2.ts // seek to the discontinuity player.currentTime(10); tags.push({ pts: 0, bytes: new Uint8Array(1) }); - standardXHRResponse(requests.pop()); + tags.push({ pts: 11 * 1000, bytes: new Uint8Array(1) }); + standardXHRResponse(requests.pop()); // 1.ts, again strictEqual(aborts, 1, 'aborted once for the seek'); // the source buffer empties. is 2.ts still in the segment buffer?