00b5acf1 by David LaPalomento

Calculate preciseDuration correctly around discontinuities

Don't use previous segment timestamps to calculate the precise duration of a segment so that we don't have to worry about timestamp discontinuities. Update to contrib-media-sources 1.0.
1 parent b8baf3c8
......@@ -44,7 +44,7 @@
},
"dependencies": {
"pkcs7": "^0.2.2",
"videojs-contrib-media-sources": "^0.3.0",
"videojs-contrib-media-sources": "^1.0.0",
"videojs-swf": "^4.6.0"
}
}
......
/**
* An object that stores the bytes of an FLV tag and methods for
* querying and manipulating that data.
* @see http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf
*/
(function(window) {
window.videojs = window.videojs || {};
......@@ -358,4 +363,26 @@ hls.FlvTag.frameTime = function(tag) {
return pts;
};
/**
* Calculate the media timeline duration represented by an array of
* tags. This function assumes the tags are already pre-sorted by
* presentation timestamp (PTS), in ascending order. Returns zero if
* there are less than two FLV tags to inspect.
* @param tags {array} the FlvTag objects to query
* @return the number of milliseconds between the display time of the
* first tag and the last tag.
*/
hls.FlvTag.durationFromTags = function(tags) {
if (tags.length < 2) {
return 0;
}
var first = tags[0], last = tags[tags.length - 1], frameDuration;
// use the interval between the last two tags or assume 24 fps
frameDuration = last.pts - tags[tags.length - 2].pts || (1/24);
return (last.pts - first.pts) + frameDuration;
};
})(this);
......
......@@ -714,7 +714,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
tags,
bytes,
segment,
durationOffset,
decrypter,
segIv,
ptsTime,
......@@ -788,23 +787,11 @@ videojs.Hls.prototype.drainBuffer = function(event) {
tags.push(this.segmentParser_.getNextTag());
}
// This block of code uses the presentation timestamp of the ts segment to calculate its exact duration, since this
// may differ by fractions of a second from what is reported. Using the exact, calculated 'preciseDuration' allows
// for smoother seeking and calculation of the total playlist duration, which previously (especially in short videos)
// was reported erroneously and made the play head overrun the end of the progress bar.
// Use the presentation timestamp of the ts segment to calculate its
// exact duration, since this may differ by fractions of a second
// from what is reported in the playlist
if (tags.length > 0) {
segment.preciseTimestamp = tags[tags.length - 1].pts;
if (playlist.segments[mediaIndex - 1]) {
if (playlist.segments[mediaIndex - 1].preciseTimestamp) {
durationOffset = playlist.segments[mediaIndex - 1].preciseTimestamp;
} else {
durationOffset = (playlist.targetDuration * (mediaIndex - 1) + playlist.segments[mediaIndex - 1].duration) * 1000;
}
segment.preciseDuration = (segment.preciseTimestamp - durationOffset) / 1000;
} else if (mediaIndex === 0) {
segment.preciseDuration = segment.preciseTimestamp / 1000;
}
segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001;
}
this.updateDuration(this.playlists.media());
......
......@@ -57,4 +57,32 @@ test('writeBytes grows the internal byte array dynamically', function() {
}
});
test('calculates the duration of a tag array from PTS values', function() {
var tags = [], count = 20, i;
for (i = 0; i < count; i++) {
tags[i] = new FlvTag(FlvTag.VIDEO_TAG);
tags[i].pts = i * 1000;
}
equal(FlvTag.durationFromTags(tags), count * 1000, 'calculated duration from PTS values');
});
test('durationFromTags() assumes 24fps if the last frame duration cannot be calculated', function() {
var tags = [
new FlvTag(FlvTag.VIDEO_TAG),
new FlvTag(FlvTag.VIDEO_TAG),
new FlvTag(FlvTag.VIDEO_TAG)
];
tags[0].pts = 0;
tags[1].pts = tags[2].pts = 1000;
equal(FlvTag.durationFromTags(tags), 1000 + (1/24) , 'assumes 24fps video');
});
test('durationFromTags() returns zero if there are less than two frames', function() {
equal(FlvTag.durationFromTags([]), 0, 'returns zero for empty input');
equal(FlvTag.durationFromTags([new FlvTag(FlvTag.VIDEO_TAG)]), 0, 'returns zero for a singleton input');
});
})(this);
......
......@@ -1086,8 +1086,11 @@ test('flushes the parser after each segment', function() {
strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
});
test('calculates preciseTimestamp and preciseDuration for a new segment', function() {
var tags = [{ pts : 200000, bytes: new Uint8Array(1) }];
test('calculates preciseDuration for a new segment', function() {
var tags = [
{ pts : 200 * 1000, bytes: new Uint8Array(1) },
{ pts : 300 * 1000, bytes: new Uint8Array(1) }
];
videojs.Hls.SegmentParser = mockSegmentParser(tags);
player.src({
......@@ -1099,11 +1102,40 @@ test('calculates preciseTimestamp and preciseDuration for a new segment', functi
standardXHRResponse(requests[0]);
strictEqual(player.duration(), 40, 'player duration is read from playlist on load');
standardXHRResponse(requests[1]);
strictEqual(player.hls.playlists.media().segments[0].preciseTimestamp, 200000, 'preciseTimestamp is calculated and stored');
strictEqual(player.hls.playlists.media().segments[0].preciseDuration, 200, 'preciseDuration is calculated and stored');
strictEqual(player.duration(), 230, 'player duration is calculated using preciseDuration');
});
test('calculates preciseDuration correctly around discontinuities', function() {
var tags = [];
videojs.Hls.SegmentParser = mockSegmentParser(tags);
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) });
standardXHRResponse(requests.shift()); // segment 0
player.hls.checkBuffer_();
// the PTS value of the second segment is *earlier* than the first
tags.push({ pts: 0 * 1000, bytes: new Uint8Array(1) });
tags.push({ pts: 5 * 1000, bytes: new Uint8Array(1) });
standardXHRResponse(requests.shift()); // segment 1
equal(player.hls.playlists.media().segments[1].preciseDuration,
5 + 5, // duration includes the time to display the second tag
'duration is independent of previous segments');
});
test('exposes in-band metadata events as cues', function() {
var track;
player.src({
......