dcce9f45 by David LaPalomento

Fix seeking in live streams. Closes #308.

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