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 @@
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);
......@@ -360,7 +364,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();
......@@ -1103,41 +1107,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;
if (time === 0) {
videojs.Hls.getMediaIndexByTime = function() {
videojs.log.warn('getMediaIndexByTime is deprecated. ' +
'Use PlaylistLoader.getMediaIndexForTime_ instead.');
return 0;
}
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);
}
if (time >= timeRanges[timeRanges.length - 1].end) {
return (playlist.segments.length - 1);
}
for (counter = 0; counter < timeRanges.length; counter++) {
if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
return counter;
}
}
return -1;
};
/**
......
......@@ -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,
......
......@@ -1937,18 +1937,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?
......