09fa9fb5 by Jon-Carlos Rivera

Merge pull request #547 from videojs/duration-fixes

Duration and endOfStream fixes
2 parents 264b9516 aac49993
......@@ -297,6 +297,72 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) {
return result[0];
};
/**
* Updates segment with information about its end-point in time and, optionally,
* the segment duration if we have enough information to determine a segment duration
* accurately.
* @param playlist {object} a media playlist object
* @param segmentIndex {number} the index of segment we last appended
* @param segmentEnd {number} the known of the segment referenced by segmentIndex
*/
videojs.HlsHandler.prototype.updateSegmentMetadata_ = function(playlist, segmentIndex, segmentEnd) {
var
segment,
previousSegment;
if (!playlist) {
return;
}
segment = playlist.segments[segmentIndex];
previousSegment = playlist.segments[segmentIndex - 1];
if (segmentEnd && segment) {
segment.end = segmentEnd;
// fix up segment durations based on segment end data
if (!previousSegment) {
// first segment is always has a start time of 0 making its duration
// equal to the segment end
segment.duration = segment.end;
} else if (previousSegment.end) {
segment.duration = segment.end - previousSegment.end;
}
}
};
/**
* Determines if we should call endOfStream on the media source based on the state
* of the buffer or if appened segment was the final segment in the playlist.
* @param playlist {object} a media playlist object
* @param segmentIndex {number} the index of segment we last appended
* @param currentBuffered {object} the buffered region that currentTime resides in
* @return {boolean} whether the calling function should call endOfStream on the MediaSource
*/
videojs.HlsHandler.prototype.isEndOfStream_ = function(playlist, segmentIndex, currentBuffered) {
var
segments = playlist.segments,
appendedLastSegment,
bufferedToEnd;
if (!playlist) {
return false;
}
// determine a few boolean values to help make the branch below easier
// to read
appendedLastSegment = (segmentIndex === segments.length - 1);
bufferedToEnd = (currentBuffered.length &&
segments[segments.length - 1].end <= currentBuffered.end(0));
// if we've buffered to the end of the video, we need to call endOfStream
// so that MediaSources can trigger the `ended` event when it runs out of
// buffered data instead of waiting for me
return playlist.endList &&
this.mediaSource.readyState === 'open' &&
(appendedLastSegment || bufferedToEnd);
};
var parseCodecs = function(codecs) {
var result = {
codecCount: 0,
......@@ -506,11 +572,18 @@ videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
};
videojs.HlsHandler.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return videojs.Hls.Playlist.duration(playlists.media());
var
playlists = this.playlists;
if (!playlists) {
return 0;
}
if (this.mediaSource) {
return this.mediaSource.duration;
}
return 0;
return videojs.Hls.Playlist.duration(playlists.media());
};
videojs.HlsHandler.prototype.seekable = function() {
......@@ -551,6 +624,7 @@ videojs.HlsHandler.prototype.seekable = function() {
videojs.HlsHandler.prototype.updateDuration = function(playlist) {
var oldDuration = this.mediaSource.duration,
newDuration = videojs.Hls.Playlist.duration(playlist),
buffered = this.tech_.buffered(),
setDuration = function() {
this.mediaSource.duration = newDuration;
this.tech_.trigger('durationchange');
......@@ -558,6 +632,10 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) {
this.mediaSource.removeEventListener('sourceopen', setDuration);
}.bind(this);
if (buffered.length > 0) {
newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
}
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
// update the duration
......@@ -1227,7 +1305,8 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
currentMediaIndex,
currentBuffered,
seekable,
timelineUpdate;
timelineUpdate,
isEndOfStream;
// stop here if the update errored or was aborted
if (!segmentInfo) {
......@@ -1243,14 +1322,18 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
this.pendingSegment_ = null;
playlist = this.playlists.media();
playlist = segmentInfo.playlist;
segments = playlist.segments;
currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
currentBuffered = this.findBufferedRange_();
isEndOfStream = this.isEndOfStream_(playlist, currentMediaIndex, currentBuffered);
// if we switched renditions don't try to add segment timeline
// information to the playlist
if (segmentInfo.playlist.uri !== this.playlists.media().uri) {
if (isEndOfStream) {
return this.mediaSource.endOfStream();
}
return this.fillBuffer();
}
......@@ -1275,21 +1358,16 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
}
}
timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered,
this.tech_.buffered());
if (timelineUpdate && segment) {
segment.end = timelineUpdate;
}
// Update segment meta-data (duration and end-point) based on timeline
this.updateSegmentMetadata_(playlist, currentMediaIndex, timelineUpdate);
// if we've buffered to the end of the video, let the MediaSource know
if (this.playlists.media().endList &&
currentBuffered.length &&
segments[segments.length - 1].end <= currentBuffered.end(0) &&
this.mediaSource.readyState === 'open') {
this.mediaSource.endOfStream();
return;
// If we decide to signal the end of stream, then we can return instead
// of trying to fetch more segments
if (isEndOfStream) {
return this.mediaSource.endOfStream();
}
if (timelineUpdate !== null ||
......
......@@ -2144,6 +2144,79 @@ test('tracks segment end times as they are buffered', function() {
equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration');
});
test('updates first segment duration as it is buffered', function() {
var bufferEnd = 0;
player.src({
src: 'media.m3u8',
type: 'application/x-mpegURL'
});
openMediaSource(player);
// as new segments are downloaded, the buffer end is updated
player.tech_.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
// 0.ts is shorter than advertised
standardXHRResponse(requests.shift());
equal(player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
equal(player.tech_.hls.playlists.media().segments[0].duration, 10,
'segment duration initially based on playlist');
bufferEnd = 9.5;
player.tech_.hls.sourceBuffer.trigger('update');
player.tech_.hls.sourceBuffer.trigger('updateend');
equal(player.tech_.hls.playlists.media().segments[0].duration, 9.5,
'updated segment duration');
});
test('updates segment durations as they are buffered', function() {
var bufferEnd = 0;
player.src({
src: 'media.m3u8',
type: 'application/x-mpegURL'
});
openMediaSource(player);
// as new segments are downloaded, the buffer end is updated
player.tech_.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
};
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
// 0.ts is shorter than advertised
standardXHRResponse(requests.shift());
equal(player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
equal(player.tech_.hls.playlists.media().segments[1].duration, 10,
'segment duration initially based on playlist');
bufferEnd = 9.5;
player.tech_.hls.sourceBuffer.trigger('update');
player.tech_.hls.sourceBuffer.trigger('updateend');
clock.tick(1);
standardXHRResponse(requests.shift());
bufferEnd = 19;
player.tech_.hls.sourceBuffer.trigger('update');
player.tech_.hls.sourceBuffer.trigger('updateend');
equal(player.tech_.hls.playlists.media().segments[1].duration, 9.5,
'updated segment duration');
});
QUnit.skip('seeking does not fail when targeted between segments', function() {
var currentTime, segmentUrl;
player.src({
......@@ -2428,7 +2501,7 @@ test('can be disposed before finishing initialization', function() {
}
});
test('calls ended() on the media source at the end of a playlist', function() {
test('calls endOfStream on the media source after appending the last segment', function() {
var endOfStreams = 0, buffered = [[]];
player.src({
src: 'http://example.com/media.m3u8',
......@@ -2441,11 +2514,15 @@ test('calls ended() on the media source at the end of a playlist', function() {
player.tech_.hls.mediaSource.endOfStream = function() {
endOfStreams++;
};
player.currentTime(20);
clock.tick(1);
// playlist response
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
// segment response
requests[0].response = new ArrayBuffer(17);
......@@ -2454,7 +2531,50 @@ test('calls ended() on the media source at the end of a playlist', function() {
buffered =[[0, 10]];
player.tech_.hls.sourceBuffer.trigger('updateend');
strictEqual(endOfStreams, 1, 'ended media source');
strictEqual(endOfStreams, 1, 'called endOfStream on the media source');
});
test('calls endOfStream on the media source when the current buffer ends at duration', function() {
var endOfStreams = 0, buffered = [[]];
player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.buffered = function() {
return videojs.createTimeRanges(buffered);
};
player.tech_.hls.mediaSource.endOfStream = function() {
endOfStreams++;
};
player.currentTime(19);
clock.tick(1);
// playlist response
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
// segment response
requests[0].response = new ArrayBuffer(17);
requests.shift().respond(200, null, '');
strictEqual(endOfStreams, 0, 'waits for the buffer update to finish');
buffered =[[10, 20]];
player.tech_.hls.sourceBuffer.trigger('updateend');
player.currentTime(5);
clock.tick(1);
// segment response
requests[0].response = new ArrayBuffer(17);
requests.shift().respond(200, null, '');
buffered =[[0, 20]];
player.tech_.hls.sourceBuffer.trigger('updateend');
strictEqual(endOfStreams, 2, 'called endOfStream on the media source twice');
});
test('calling play() at the end of a video replays', function() {
......