bce88a18 by jrivera Committed by David LaPalomento

buffer at the current time range end instead of incrementing a variable. closes #423

1 parent c7d5b626
......@@ -2,7 +2,7 @@ CHANGELOG
=========
## HEAD (Unreleased)
_(none)_
* buffer at the current time range end instead of incrementing a variable. ([view](https://github.com/videojs/videojs-contrib-hls/pull/423))
--------------------
......
......@@ -44,7 +44,7 @@
"karma-sauce-launcher": "~0.1.8",
"qunitjs": "^1.18.0",
"sinon": "1.10.2",
"video.js": "^5.0.0-rc.96"
"video.js": "^5.1.0"
},
"dependencies": {
"pkcs7": "^0.2.2",
......
......@@ -367,7 +367,17 @@
* closest playback position that is currently available.
*/
PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) {
var i, j, segment, targetDuration;
var
i,
segment,
originalTime = time,
targetDuration = this.media_.targetDuration || 10,
numSegments = this.media_.segments.length,
lastSegment = numSegments - 1,
startIndex,
endIndex,
knownStart,
knownEnd;
if (!this.media_) {
return 0;
......@@ -379,57 +389,105 @@
return 0;
}
// 1) Walk backward until we find the latest segment with timeline
// 1) Walk backward until we find the first segment with timeline
// information that is earlier than `time`
targetDuration = this.media_.targetDuration || 10;
i = this.media_.segments.length;
while (i--) {
for (i = lastSegment; i >= 0; i--) {
segment = this.media_.segments[i];
if (segment.end !== undefined && segment.end <= time) {
time -= segment.end;
startIndex = i + 1;
knownStart = segment.end;
if (startIndex >= numSegments) {
// The last segment claims to end *before* the time we are
// searching for so just return it
return numSegments;
}
break;
}
if (segment.start !== undefined && segment.start < time) {
if (segment.start !== undefined && segment.start <= time) {
if (segment.end !== undefined && segment.end > time) {
// we've found the target segment exactly
return i;
}
startIndex = i;
knownStart = segment.start;
break;
}
}
// 2) Walk forward until we find the first segment with timeline
// information that is greater than `time`
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
if (segment.start !== undefined && segment.start > time) {
endIndex = i - 1;
knownEnd = segment.start;
if (endIndex < 0) {
// The first segment claims to start *after* the time we are
// searching for so just return it
return -1;
}
break;
}
if (segment.end !== undefined && segment.end > time) {
endIndex = i;
knownEnd = segment.end;
break;
}
}
time -= segment.start;
if (startIndex !== undefined) {
// We have a known-start point that is before our desired time so
// walk from that point forwards
time = time - knownStart;
for (i = startIndex; i < (endIndex || numSegments); i++) {
segment = this.media_.segments[i];
time -= segment.duration || targetDuration;
if (time < 0) {
// the segment with start information is also our best guess
// for the momment
return i;
}
break;
}
if (i === endIndex) {
// We haven't found a segment but we did hit a known end point
// so fallback to "Algorithm Jon" - try to interpolate the segment
// index based on the known span of the timeline we are dealing with
// and the number of segments inside that span
return startIndex + Math.floor(
((originalTime - knownStart) / (knownEnd - knownStart)) *
(endIndex - startIndex));
}
i++;
// 2) Walk forward, testing each segment to see if `time` falls within it
for (j = i; j < this.media_.segments.length; j++) {
segment = this.media_.segments[j];
// We _still_ haven't found a segment so load the last one
return lastSegment;
} else if (endIndex !== undefined) {
// We _only_ have a known-end point that is after our desired time so
// walk from that point backwards
time = knownEnd - time;
for (i = endIndex; i >= 0; i--) {
segment = this.media_.segments[i];
time -= segment.duration || targetDuration;
if (time < 0) {
return j;
return i;
}
// 2a) If we discover a segment that has timeline information
// before finding the result segment, the playlist information
// must have been inaccurate. Start a binary search for the
// segment which contains `time`. If the guess turns out to be
// incorrect, we'll have more info to work with next time.
if (segment.start !== undefined || segment.end !== undefined) {
return Math.floor((j - i) * 0.5);
}
// We haven't found a segment so load the first one
return 0;
} else {
// We known nothing so use "Algorithm A" - walk from the front
// of the playlist naively subtracking durations until we find
// a segment that contains time and return it
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
time -= segment.duration || targetDuration;
if (time < 0) {
return i;
}
}
// the playback position is outside the range of available
// segments so return the length
return this.media_.segments.length;
// We are out of possible candidates so load the last one...
// The last one is the least likely to overlap a buffer and therefore
// the one most likely to tell us something about the timeline
return lastSegment;
}
};
videojs.Hls.PlaylistLoader = PlaylistLoader;
......
......@@ -304,13 +304,26 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', function() {
var segmentInfo = this.pendingSegment_, segment, currentBuffered, timelineUpdates;
var
segmentInfo = this.pendingSegment_,
segment,
playlist,
currentMediaIndex,
currentBuffered,
timelineUpdates;
// stop here if the update errored or was aborted
if (!segmentInfo) {
return;
}
this.pendingSegment_ = null;
// if we've buffered to the end of the video, let the MediaSource know
currentBuffered = this.findCurrentBuffered_();
if (currentBuffered.length && this.duration() === currentBuffered.end(0)) {
if (currentBuffered.length &&
this.duration() === currentBuffered.end(0) &&
this.mediaSource.readyState === 'open') {
this.mediaSource.endOfStream();
}
......@@ -319,26 +332,46 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
return;
}
// if we switched renditions don't try to add segment timeline
// information to the playlist
if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
return this.fillBuffer();
}
playlist = this.playlists.media();
currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
// annotate the segment with any start and end time information
// added by the media processing
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
segment = playlist.segments[currentMediaIndex];
timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered,
this.tech_.buffered());
timelineUpdates.forEach(function(update) {
timelineUpdates.forEach(function (update) {
if (segment) {
if (update.start !== undefined) {
segment.start = update.start;
}
if (update.end !== undefined) {
segment.end = update.end;
}
}
});
if (timelineUpdates.length) {
this.updateDuration(segmentInfo.playlist);
this.updateDuration(playlist);
// check if it's time to download the next segment
this.fillBuffer();
return;
}
// check if it's time to download the next segment
this.checkBuffer_();
// the last segment append must have been entirely in the
// already buffered time ranges. just buffer forward until we
// find a segment that adds to the buffered time ranges and
// improves subsequent media index calculations.
this.fillBuffer(currentMediaIndex + 1);
return;
}.bind(this));
};
......@@ -426,11 +459,8 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
this.cancelKeyXhr();
}
// clear out the segment being processed
this.pendingSegment_ = null;
// begin filling the buffer at the new position
this.fillBuffer(currentTime);
this.fillBuffer(this.playlists.getMediaIndexForTime_(currentTime));
};
videojs.Hls.prototype.duration = function() {
......@@ -465,15 +495,25 @@ videojs.Hls.prototype.updateDuration = function(playlist) {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
this.mediaSource.removeEventListener('sourceopen', setDuration);
}.bind(this);
}.bind(this),
seekable = this.seekable();
// TODO: Move to videojs-contrib-media-sources
if (seekable.length && newDuration === Infinity) {
if (isNaN(oldDuration)) {
oldDuration = 0;
}
newDuration = Math.max(oldDuration,
seekable.end(0) + playlist.targetDuration * 3);
}
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
if (this.mediaSource.readyState === 'open') {
if (this.mediaSource.readyState !== 'open') {
this.mediaSource.addEventListener('sourceopen', setDuration);
} else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
} else {
this.mediaSource.addEventListener('sourceopen', setDuration);
}
}
};
......@@ -507,6 +547,8 @@ videojs.Hls.prototype.cancelSegmentXhr = function() {
this.segmentXhr_.abort();
this.segmentXhr_ = null;
}
// clear out the segment being processed
this.pendingSegment_ = null;
};
/**
......@@ -667,11 +709,17 @@ videojs.Hls.prototype.stopCheckingBuffer_ = function() {
*/
videojs.Hls.prototype.findCurrentBuffered_ = function() {
var
tech = this.tech_,
currentTime = tech.currentTime(),
buffered = this.tech_.buffered(),
ranges,
i;
i,
tech = this.tech_,
// !!The order of the next two lines is important!!
// `currentTime` must be equal-to or greater-than the start of the
// buffered range. Flash executes out-of-process so, every value can
// change behind the scenes from line-to-line. By reading `currentTime`
// after `buffered`, we ensure that it is always a current or later
// value during playback.
buffered = tech.buffered(),
currentTime = tech.currentTime();
if (buffered && buffered.length) {
// Search for a range containing the play-head
......@@ -697,13 +745,13 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() {
* @param seekToTime (optional) {number} the offset into the downloaded segment
* to seek to, in seconds
*/
videojs.Hls.prototype.fillBuffer = function(seekToTime) {
videojs.Hls.prototype.fillBuffer = function(mediaIndex) {
var
tech = this.tech_,
currentTime = tech.currentTime(),
currentBuffered = this.findCurrentBuffered_(),
currentBufferedEnd = 0,
bufferedTime = 0,
mediaIndex = 0,
segment,
segmentInfo;
......@@ -739,39 +787,46 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) {
return;
}
// find the next segment to download
if (typeof seekToTime === 'number') {
mediaIndex = this.playlists.getMediaIndexForTime_(seekToTime);
} else if (currentBuffered && currentBuffered.length) {
mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0));
bufferedTime = Math.max(0, currentBuffered.end(0) - currentTime);
if (mediaIndex === undefined) {
if (currentBuffered && currentBuffered.length) {
currentBufferedEnd = currentBuffered.end(0);
mediaIndex = this.playlists.getMediaIndexForTime_(currentBufferedEnd);
bufferedTime = Math.max(0, currentBufferedEnd - currentTime);
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if (bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
return;
}
} else {
mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
}
}
segment = this.playlists.media().segments[mediaIndex];
// if the video has finished downloading, stop trying to buffer
// if the video has finished downloading
if (!segment) {
return;
}
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if (typeof seekToTime !== 'number' &&
bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
return;
// we have entered a state where we are fetching the same segment,
// try to walk forward
if (this.lastSegmentLoaded_ &&
this.lastSegmentLoaded_ === this.playlistUriToUrl(segment.uri)) {
return this.fillBuffer(mediaIndex + 1);
}
// package up all the work to append the segment
segmentInfo = {
// resolve the segment URL relative to the playlist
uri: this.playlistUriToUrl(segment.uri),
// the segment's mediaIndex at the time it was received
// the segment's mediaIndex & mediaSequence at the time it was requested
mediaIndex: mediaIndex,
mediaSequence: this.playlists.media().mediaSequence,
// the segment's playlist
playlist: this.playlists.media(),
// optionally, a time offset to seek to within the segment
offset: seekToTime,
// The state of the buffer when this segment was requested
currentBufferedEnd: currentBufferedEnd,
// unencrypted bytes of the segment
bytes: null,
// when a key is defined for this segment, the encrypted bytes
......@@ -856,6 +911,7 @@ videojs.Hls.prototype.loadSegment = function(segmentInfo) {
return;
}
self.lastSegmentLoaded_ = segmentInfo.uri;
self.setBandwidth(request);
if (segment.key) {
......@@ -944,41 +1000,33 @@ videojs.Hls.prototype.drainBuffer = function(event) {
event = event || {};
if (segmentInfo.mediaIndex > 0) {
segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist,
playlist.mediaSequence + segmentInfo.mediaIndex);
}
// If we have seeked into a non-buffered time-range, remove all buffered
// time-ranges because they could have been incorrectly placed originally
if (this.tech_.seeking() && outsideBufferedRanges) {
if (hasBufferedContent) {
// In Chrome, it seems that too many independent buffered time-ranges can
// cause playback to fail to resume when seeking so just kill all of them
this.sourceBuffer.remove(0, Infinity);
return;
}
// If there are discontinuities in the playlist, we can't be sure of anything
// related to time so we reset the timestamp offset and start appending data
// anew on every seek
if (segmentInfo.playlist.discontinuityStarts.length) {
if (segmentInfo.mediaIndex > 0) {
segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, segmentInfo.mediaIndex);
}
// Now that the forward buffer is clear, we have to set timestamp offset to
// the start of the buffered region
this.sourceBuffer.timestampOffset = segmentTimestampOffset;
}
} else if (segment.discontinuity) {
} else if (segment.discontinuity && currentBuffered.length) {
// If we aren't seeking and are crossing a discontinuity, we should set
// timestampOffset for new segments to be appended the end of the current
// buffered time-range
this.sourceBuffer.timestampOffset = currentBuffered.end(0);
} else if (!hasBufferedContent && this.tech_.currentTime() > 0.05) {
// If we are trying to play at a position that is not zero but we aren't
// currently seeking according to the video element
this.sourceBuffer.timestampOffset = segmentTimestampOffset;
}
if (currentBuffered.length) {
// Chrome 45 stalls if appends overlap the playhead
this.sourceBuffer.appendWindowStart = Math.min(this.tech_.currentTime(), currentBuffered.end(0));
} else {
this.sourceBuffer.appendWindowStart = 0;
}
this.pendingSegment_.buffered = this.tech_.buffered();
// the segment is asynchronously added to the current buffered data
......
......@@ -653,8 +653,8 @@
equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero');
equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2');
equal(loader.getMediaIndexForTime_(22),
3,
'time greater than the length is index 3');
2,
'time greater than the length is index 2');
});
test('returns the lower index when calculating for a segment boundary', function() {
......@@ -683,9 +683,9 @@
'1002.ts\n');
loader.media().segments[0].start = 150;
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_(0), -1, 'the lowest returned value is negative one');
equal(loader.getMediaIndexForTime_(45), -1, 'expired content returns negative one');
equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns negative one');
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');
......
......@@ -218,6 +218,7 @@ module('HLS', {
var el = document.createElement('div');
el.id = 'vjs_mock_flash_' + nextId++;
el.className = 'vjs-tech vjs-mock-flash';
el.duration = Infinity;
el.vjs_load = function() {};
el.vjs_getProperty = function(attr) {
if (attr === 'buffered') {
......@@ -1131,7 +1132,7 @@ test('buffers based on the correct TimeRange if multiple ranges exist', function
return videojs.createTimeRange(buffered);
};
currentTime = 8;
buffered = [[0, 10], [20, 40]];
buffered = [[0, 10], [20, 30]];
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -1144,14 +1145,9 @@ test('buffers based on the correct TimeRange if multiple ranges exist', function
currentTime = 22;
player.tech_.hls.sourceBuffer.trigger('updateend');
player.tech_.hls.checkBuffer_();
strictEqual(requests.length, 2, 'made no additional requests');
buffered = [[0, 10], [20, 30]];
player.tech_.hls.checkBuffer_();
standardXHRResponse(requests[2]);
strictEqual(requests.length, 3, 'made three requests');
strictEqual(requests[2].url,
absoluteUrl('manifest/media-00004.ts'),
absoluteUrl('manifest/media-00003.ts'),
'made segment request');
});
......@@ -1381,7 +1377,7 @@ test('seeking in an empty playlist is a non-erroring noop', function() {
equal(requests.length, requestsLength, 'made no additional requests');
});
test('duration is Infinity for live playlists', function() {
test('tech\'s duration reports Infinity for live playlists', function() {
player.src({
src: 'http://example.com/manifest/missingEndlist.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -1390,9 +1386,13 @@ test('duration is Infinity for live playlists', function() {
standardXHRResponse(requests[0]);
strictEqual(player.tech_.hls.mediaSource.duration,
strictEqual(player.tech_.duration(),
Infinity,
'duration on the tech is infinity');
notEqual(player.tech_.hls.mediaSource.duration,
Infinity,
'duration is infinity');
'duration on the mediaSource is not infinity');
});
test('live playlist starts three target durations before live', function() {
......@@ -1644,7 +1644,7 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() {
});
test('sets timestampOffset when seeking with discontinuities', function() {
var removes = [], timeRange = videojs.createTimeRange(0, 10);
var timeRange = videojs.createTimeRange(0, 10);
player.src({
src: 'discontinuity.m3u8',
......@@ -1670,18 +1670,12 @@ test('sets timestampOffset when seeking with discontinuities', function() {
'3.ts\n' +
'#EXT-X-ENDLIST\n');
player.tech_.hls.sourceBuffer.timestampOffset = 0;
player.tech_.hls.sourceBuffer.remove = function(start, end) {
timeRange = videojs.createTimeRange();
removes.push([start, end]);
};
player.currentTime(21);
clock.tick(1);
equal(requests.shift().aborted, true, 'aborted first request');
standardXHRResponse(requests.pop()); // 3.ts
clock.tick(1000);
equal(player.tech_.hls.sourceBuffer.timestampOffset, 20, 'timestampOffset starts at zero');
equal(removes.length, 1, 'remove was called');
});
test('can seek before the source buffer opens', function() {
......