55303704 by David LaPalomento

Fix seeks between segments. Improve duration calculation. Closes #339

2 parents af92753d e81de1a7
Showing 49 changed files with 621 additions and 192 deletions
......@@ -2,7 +2,7 @@ CHANGELOG
=========
## HEAD (Unreleased)
_(none)_
* Fix seeks between segments. Improve duration calculation. ([view](https://github.com/videojs/videojs-contrib-hls/pull/339))
--------------------
......
......@@ -375,7 +375,8 @@
// the manifest is empty until the parse stream begins delivering data
this.manifest = {
allowCache: true
allowCache: true,
discontinuityStarts: []
};
// update the manifest with the m3u8 entry from the parse stream
......@@ -513,6 +514,7 @@
},
'discontinuity': function() {
currentUri.discontinuity = true;
this.manifest.discontinuityStarts.push(uris.length);
},
'targetduration': function() {
if (!isFinite(entry.duration) || entry.duration < 0) {
......
......@@ -431,11 +431,12 @@
for (i = 0; i < this.media_.segments.length; i++) {
time -= Playlist.duration(this.media_,
this.media_.mediaSequence + i,
this.media_.mediaSequence + i + 1);
this.media_.mediaSequence + i + 1,
false);
// HLS version 3 and lower round segment durations to the
// nearest decimal integer. When the correct media index is
// ambiguous, prefer the lower one.
// ambiguous, prefer the higher one.
if (time <= 0) {
return i;
}
......
......@@ -5,7 +5,130 @@
'use strict';
var DEFAULT_TARGET_DURATION = 10;
var duration, seekable, segmentsDuration;
var accumulateDuration, ascendingNumeric, duration, intervalDuration, rangeDuration, seekable;
// Array.sort comparator to sort numbers in ascending order
ascendingNumeric = function(left, right) {
return left - right;
};
/**
* Returns the media duration for the segments between a start and
* exclusive end index. The start and end parameters are interpreted
* as indices into the currently available segments. This method
* does not calculate durations for segments that have expired.
* @param playlist {object} a media playlist object
* @param start {number} an inclusive lower boundary for the
* segments to examine.
* @param end {number} an exclusive upper boundary for the segments
* to examine.
* @param includeTrailingTime {boolean} if false, the interval between
* the final segment and the subsequent segment will not be included
* in the result
* @return {number} the duration between the start index and end
* index in seconds.
*/
accumulateDuration = function(playlist, start, end, includeTrailingTime) {
var
ranges = [],
rangeEnds = (playlist.discontinuityStarts || []).concat(end),
result = 0,
i;
// short circuit if start and end don't specify a non-empty range
// of segments
if (start >= end) {
return 0;
}
// create a range object for each discontinuity sequence
rangeEnds.sort(ascendingNumeric);
for (i = 0; i < rangeEnds.length; i++) {
if (rangeEnds[i] > start) {
ranges.push({ start: start, end: rangeEnds[i] });
i++;
break;
}
}
for (; i < rangeEnds.length; i++) {
// ignore times ranges later than end
if (rangeEnds[i] >= end) {
ranges.push({ start: rangeEnds[i - 1], end: end });
break;
}
ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] });
}
// add up the durations for each of the ranges
for (i = 0; i < ranges.length; i++) {
result += rangeDuration(playlist,
ranges[i],
i === ranges.length - 1 && includeTrailingTime);
}
return result;
};
/**
* Returns the duration of the specified range of segments. The
* range *must not* cross a discontinuity.
* @param playlist {object} a media playlist object
* @param range {object} an object that specifies a starting and
* ending index into the available segments.
* @param includeTrailingTime {boolean} if false, the interval between
* the final segment and the subsequent segment will not be included
* in the result
* @return {number} the duration of the range in seconds.
*/
rangeDuration = function(playlist, range, includeTrailingTime) {
var
result = 0,
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION,
segment,
left, right;
// accumulate while searching for the earliest segment with
// available PTS information
for (left = range.start; left < range.end; left++) {
segment = playlist.segments[left];
if (segment.minVideoPts !== undefined) {
break;
}
result += segment.duration || targetDuration;
}
// see if there's enough information to include the trailing time
if (includeTrailingTime) {
segment = playlist.segments[range.end];
if (segment && segment.minVideoPts !== undefined) {
result += 0.001 *
(Math.min(segment.minVideoPts, segment.minAudioPts) -
Math.min(playlist.segments[left].minVideoPts,
playlist.segments[left].minAudioPts));
return result;
}
}
// do the same thing while finding the latest segment
for (right = range.end - 1; right >= left; right--) {
segment = playlist.segments[right];
if (segment.maxVideoPts !== undefined) {
break;
}
result += segment.duration || targetDuration;
}
// add in the PTS interval in seconds between them
if (right >= left) {
result += 0.001 *
(Math.max(playlist.segments[right].maxVideoPts,
playlist.segments[right].maxAudioPts) -
Math.min(playlist.segments[left].minVideoPts,
playlist.segments[left].minAudioPts));
}
return result;
};
/**
* Calculate the media duration from the segments associated with a
......@@ -17,47 +140,28 @@
* boundary for the playlist. Defaults to 0.
* @param endSequence {number} (optional) an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @param includeTrailingTime {boolean} if false, the interval between
* the final segment and the subsequent segment will not be included
* in the result
* @return {number} the duration between the start index and end
* index.
*/
segmentsDuration = function(playlist, startSequence, endSequence) {
var targetDuration, i, j, segment, endSegment, expiredSegmentCount, result = 0;
intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) {
var result = 0, targetDuration, expiredSegmentCount;
startSequence = startSequence || 0;
i = startSequence;
endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length;
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
// estimate expired segment duration using the target duration
expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0);
result += expiredSegmentCount * targetDuration;
i += expiredSegmentCount;
// accumulate the segment durations into the result
for (; i < endSequence; i++) {
segment = playlist.segments[i - playlist.mediaSequence];
// when PTS values aren't available, use information from the playlist
if (segment.minVideoPts === undefined) {
result += segment.duration ||
targetDuration;
continue;
}
// find the last segment with PTS info and use that to calculate
// the interval duration
for(j = i; j < endSequence - 1; j++) {
endSegment = playlist.segments[j - playlist.mediaSequence + 1];
if (endSegment.maxVideoPts === undefined ||
endSegment.discontinuity) {
break;
}
}
endSegment = playlist.segments[j - playlist.mediaSequence];
result += (Math.max(endSegment.maxVideoPts, endSegment.maxAudioPts) -
Math.min(segment.minVideoPts, segment.minAudioPts)) * 0.001;
i = j;
}
result += accumulateDuration(playlist,
startSequence + expiredSegmentCount - playlist.mediaSequence,
endSequence - playlist.mediaSequence,
includeTrailingTime);
return result;
};
......@@ -72,14 +176,21 @@
* boundary for the playlist. Defaults to 0.
* @param endSequence {number} (optional) an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @param includeTrailingTime {boolean} (optional) if false, the interval between
* the final segment and the subsequent segment will not be included
* in the result
* @return {number} the duration between the start index and end
* index.
*/
duration = function(playlist, startSequence, endSequence) {
duration = function(playlist, startSequence, endSequence, includeTrailingTime) {
if (!playlist) {
return 0;
}
if (includeTrailingTime === undefined) {
includeTrailingTime = true;
}
// if a slice of the total duration is not requested, use
// playlist-level duration indicators when they're present
if (startSequence === undefined && endSequence === undefined) {
......@@ -95,9 +206,10 @@
}
// calculate the total duration based on the segment durations
return segmentsDuration(playlist,
return intervalDuration(playlist,
startSequence,
endSequence);
endSequence,
includeTrailingTime);
};
/**
......@@ -119,8 +231,8 @@
return videojs.createTimeRange(0, duration(playlist));
}
start = segmentsDuration(playlist, 0, playlist.mediaSequence);
end = start + segmentsDuration(playlist,
start = intervalDuration(playlist, 0, playlist.mediaSequence);
end = start + intervalDuration(playlist,
playlist.mediaSequence,
playlist.mediaSequence + playlist.segments.length);
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
......
......@@ -31,15 +31,6 @@
// allow in-band metadata to be observed
self.metadataStream = new MetadataStream();
this.mediaTimelineOffset = null;
// The first timestamp value encountered during parsing. This
// value can be used to determine the relative timing between
// frames and the start of the current timestamp sequence. It
// should be reset to null before parsing a segment with
// discontinuous timestamp values from previous segments.
self.timestampOffset = null;
// For information on the FLV format, see
// http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
// Technically, this function returns the header and a metadata FLV tag
......@@ -360,13 +351,6 @@
// Skip past "optional" portion of PTS header
offset += pesHeaderLength;
// keep track of the earliest encounted PTS value so
// external parties can align timestamps across
// discontinuities
if (self.timestampOffset === null) {
self.timestampOffset = pts;
}
if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
h264Stream.setNextTimeStamp(pts,
dts,
......
......@@ -260,7 +260,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
// add a metadata cue whenever a metadata event is triggered during
// segment parsing
metadataStream.on('data', function(metadata) {
var i, cue, frame, time, media, segmentOffset, hexDigit;
var i, hexDigit;
// create the metadata track if this is the first ID3 tag we've
// seen
......@@ -276,19 +276,11 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
}
}
// calculate the start time for the segment that is currently being parsed
media = tech.playlists.media();
segmentOffset = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_;
segmentOffset += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex);
// create cue points for all the ID3 frames in this metadata event
for (i = 0; i < metadata.frames.length; i++) {
frame = metadata.frames[i];
time = tech.segmentParser_.mediaTimelineOffset + ((metadata.pts - tech.segmentParser_.timestampOffset) * 0.001);
cue = new window.VTTCue(time, time, frame.value || frame.url || '');
cue.frame = frame;
textTrack.addCue(cue);
}
// store this event for processing once the muxing has finished
tech.segmentBuffer_[0].pendingMetadata.push({
textTrack: textTrack,
metadata: metadata
});
});
// when seeking, clear out all cues ahead of the earliest position
......@@ -312,6 +304,30 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
});
};
videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time;
segmentOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist,
segmentInfo.playlist.mediaSequence,
segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex);
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
minPts = Math.min(segment.minVideoPts, segment.minAudioPts);
while (segmentInfo.pendingMetadata.length) {
metadata = segmentInfo.pendingMetadata[0].metadata;
textTrack = segmentInfo.pendingMetadata[0].textTrack;
// create cue points for all the ID3 frames in this metadata event
for (i = 0; i < metadata.frames.length; i++) {
frame = metadata.frames[i];
time = segmentOffset + ((metadata.pts - minPts) * 0.001);
cue = new window.VTTCue(time, time, frame.value || frame.url || '');
cue.frame = frame;
textTrack.addCue(cue);
}
segmentInfo.pendingMetadata.shift();
}
};
/**
* Reset the mediaIndex if play() is called after the video has
* ended.
......@@ -780,7 +796,10 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
// when a key is defined for this segment, the encrypted bytes
encryptedBytes: null,
// optionally, the decrypter that is unencrypting the segment
decrypter: null
decrypter: null,
// metadata events discovered during muxing that need to be
// translated into cue points
pendingMetadata: []
};
if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) {
segmentInfo.encryptedBytes = new Uint8Array(this.response);
......@@ -870,20 +889,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
event = event || {};
segmentOffset = this.playlists.expiredPreDiscontinuity_;
segmentOffset += this.playlists.expiredPostDiscontinuity_;
segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex);
segmentOffset *= 1000;
// if this segment starts is the start of a new discontinuity
// sequence, the segment parser's timestamp offset must be
// re-calculated
if (segment.discontinuity) {
this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001;
this.segmentParser_.timestampOffset = null;
} else if (this.segmentParser_.mediaTimelineOffset === null) {
this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001;
}
// transmux the segment data from MP2T to FLV
this.segmentParser_.parseSegmentBinaryData(bytes);
......@@ -904,20 +909,27 @@ videojs.Hls.prototype.drainBuffer = function(event) {
tags.push(this.segmentParser_.getNextTag());
}
this.addCuesForMetadata_(segmentInfo);
this.updateDuration(this.playlists.media());
// if we're refilling the buffer after a seek, scan through the muxed
// FLV tags until we find the one that is closest to the desired
// playback time
if (typeof offset === 'number') {
ptsTime = offset - segmentOffset + tags[0].pts;
while (tags[i].pts < ptsTime) {
// determine the offset within this segment we're seeking to
segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_;
segmentOffset += videojs.Hls.Playlist.duration(playlist,
playlist.mediaSequence,
playlist.mediaSequence + mediaIndex);
segmentOffset = offset - (segmentOffset * 1000);
ptsTime = segmentOffset + tags[0].pts;
while (tags[i + 1] && tags[i].pts < ptsTime) {
i++;
}
// tell the SWF where we will be seeking to
this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001);
// tell the SWF the media position of the first tag we'll be delivering
this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
tags = tags.slice(i);
......@@ -1139,29 +1151,6 @@ videojs.Hls.getMediaIndexByTime = function() {
};
/**
* Determine the current time in seconds in one playlist by a media index. This
* function iterates through the segments of a playlist up to the specified index
* and then returns the time up to that point.
*
* @param playlist {object} The playlist of the segments being searched.
* @param mediaIndex {number} The index of the target segment in the playlist.
* @returns {number} The current time to that point, or 0 if none appropriate.
*/
videojs.Hls.prototype.getCurrentTimeByMediaIndex_ = function(playlist, mediaIndex) {
var index, time = 0;
if (!playlist.segments || mediaIndex === 0) {
return 0;
}
for (index = 0; index < mediaIndex; index++) {
time += playlist.segments[index].duration;
}
return time;
};
/**
* A comparator function to sort two playlist object by bandwidth.
* @param left {object} a media playlist object
* @param right {object} a media playlist object
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -142,5 +142,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -14,5 +14,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -41,5 +41,6 @@
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}
]
],
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -138,5 +138,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -14,5 +14,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
}
],
"targetDuration": 19,
"endList": true
"endList": true,
"discontinuityStarts": [2]
}
......
......@@ -44,5 +44,6 @@
}
],
"targetDuration": 19,
"endList": true
"endList": true,
"discontinuityStarts": [2, 4, 7]
}
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -14,5 +14,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -29,5 +29,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -41,5 +41,6 @@
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}
]
],
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -2,6 +2,7 @@
"allowCache": true,
"mediaSequence": 7794,
"discontinuitySequence": 0,
"discontinuityStarts": [],
"segments": [
{
"duration": 2.833,
......
......@@ -30,5 +30,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -9,5 +9,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -142,5 +142,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -14,5 +14,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -29,5 +29,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -141,5 +141,6 @@
}
],
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -17,5 +17,6 @@
}
],
"targetDuration": 8,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -40,5 +40,6 @@
}
],
"targetDuration": 10,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -8,5 +8,6 @@
}
],
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -25,5 +25,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -9,5 +9,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -41,5 +41,6 @@
},
"uri": "media3.m3u8"
}
]
],
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -12,5 +12,6 @@
}
],
"targetDuration": 10,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -18,5 +18,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -19,5 +19,6 @@
"duration": 10
}
],
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -142,5 +142,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -10,5 +10,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -10,5 +10,6 @@
{
"uri": "media1.m3u8"
}
]
],
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 8,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -10,5 +10,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -22,5 +22,6 @@
],
"targetDuration": 10,
"endList": true,
"discontinuitySequence": 0
"discontinuitySequence": 0,
"discontinuityStarts": []
}
\ No newline at end of file
......
......@@ -3,7 +3,7 @@
'use strict';
var Playlist = videojs.Hls.Playlist;
module('Playlist Utilities');
module('Playlist Duration');
test('total duration for live playlists is Infinity', function() {
var duration = Playlist.duration({
......@@ -16,7 +16,9 @@
equal(duration, Infinity, 'duration is infinity');
});
test('interval duration accounts for media sequences', function() {
module('Playlist Interval Duration');
test('accounts for media sequences', function() {
var duration = Playlist.duration({
mediaSequence: 10,
endList: true,
......@@ -38,7 +40,7 @@
equal(duration, 14 * 10, 'duration includes dropped segments');
});
test('interval duration uses PTS values when available', function() {
test('uses PTS values when available', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
......@@ -67,52 +69,244 @@
equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values');
});
test('interval duration works when partial PTS information is available', function() {
var firstInterval, secondInterval, duration = Playlist.duration({
test('works when partial PTS information is available', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 1,
minAudioPts: 2,
maxVideoPts: 1 * 10 * 1000 + 1,
maxVideoPts: 10 * 1000 + 1,
// intentionally less duration than video
// the max stream duration should be used
maxAudioPts: 1 * 10 * 1000 + 1,
maxAudioPts: 10 * 1000 + 1,
uri: '0.ts'
}, {
duration: 9,
uri: '1.ts'
}, {
duration: 10,
uri: '2.ts'
}, {
duration: 10,
minVideoPts: 30 * 1000 + 7,
minAudioPts: 30 * 1000 + 10,
maxVideoPts: 40 * 1000 + 1,
maxAudioPts: 40 * 1000 + 2,
uri: '3.ts'
}, {
duration: 10,
maxVideoPts: 50 * 1000 + 1,
maxAudioPts: 50 * 1000 + 2,
uri: '4.ts'
}]
}, 0, 5);
equal(duration,
((50 * 1000 + 2) - 1) * 0.001,
'calculated with mixed intervals');
});
test('ignores segments before the start', function() {
var duration = Playlist.duration({
mediaSequence: 0,
segments: [{
duration: 10,
uri: '0.ts'
}, {
duration: 10,
uri: '1.ts'
}, {
duration: 10,
minVideoPts: 2 * 10 * 1000 + 7,
minAudioPts: 2 * 10 * 1000 + 10,
maxVideoPts: 3 * 10 * 1000 + 1,
maxAudioPts: 3 * 10 * 1000 + 2,
uri: '2.ts'
}]
}, 1, 3);
equal(duration, 10 + 10, 'ignored the first segment');
});
test('ignores discontinuity sequences earlier than the start', function() {
var duration = Playlist.duration({
mediaSequence: 0,
discontinuityStarts: [1, 3],
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
uri: '0.ts'
}, {
discontinuity: true,
duration: 9,
uri: '1.ts'
}, {
duration: 10,
uri: '2.ts'
}, {
discontinuity: true,
duration: 10,
maxVideoPts: 4 * 10 * 1000 + 1,
maxAudioPts: 4 * 10 * 1000 + 2,
uri: '3.ts'
}]
}, 0, 4);
}, 2, 4);
equal(duration, 10 + 10, 'excluded the earlier segments');
});
firstInterval = (1 * 10 * 1000 + 1) - 1;
firstInterval *= 0.001;
secondInterval = (4 * 10 * 1000 + 2) - (2 * 10 * 1000 + 7);
secondInterval *= 0.001;
test('ignores discontinuity sequences later than the end', function() {
var duration = Playlist.duration({
mediaSequence: 0,
discontinuityStarts: [1, 3],
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
uri: '0.ts'
}, {
discontinuity: true,
duration: 9,
uri: '1.ts'
}, {
duration: 10,
uri: '2.ts'
}, {
discontinuity: true,
duration: 10,
uri: '3.ts'
}]
}, 0, 2);
equal(duration, firstInterval + 10 + secondInterval, 'calculated with mixed intervals');
equal(duration, 19, 'excluded the later segments');
});
test('interval duration accounts for discontinuities', function() {
test('handles trailing segments without PTS information', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
uri: '0.ts'
}, {
duration: 9,
uri: '1.ts'
}, {
duration: 10,
uri: '2.ts'
}, {
minVideoPts: 29.5 * 1000,
minAudioPts: 29.5 * 1000,
maxVideoPts: 39.5 * 1000,
maxAudioPts: 39.5 * 1000,
uri: '3.ts'
}]
}, 0, 3);
equal(duration, 29.5, 'calculated duration');
});
test('uses PTS intervals when the start and end segment have them', function() {
var playlist, duration;
playlist = {
mediaSequence: 0,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
uri: '0.ts'
}, {
duration: 9,
uri: '1.ts'
},{
minVideoPts: 20 * 1000 + 100,
minAudioPts: 20 * 1000 + 100,
maxVideoPts: 30 * 1000 + 100,
maxAudioPts: 30 * 1000 + 100,
duration: 10,
uri: '2.ts'
}]
};
duration = Playlist.duration(playlist, 0, 2);
equal(duration, 20.1, 'used the PTS-based interval');
duration = Playlist.duration(playlist, 0, 3);
equal(duration, 30.1, 'used the PTS-based interval');
});
test('uses the largest continuous available PTS ranges', function() {
var playlist = {
mediaSequence: 0,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
uri: '0.ts'
}, {
duration: 10,
uri: '1.ts'
}, {
// starts 0.5s earlier than the previous segment indicates
minVideoPts: 19.5 * 1000,
minAudioPts: 19.5 * 1000,
maxVideoPts: 29.5 * 1000,
maxAudioPts: 29.5 * 1000,
uri: '2.ts'
}, {
duration: 10,
uri: '3.ts'
}, {
// ... but by the last segment, there is actual 0.5s more
// content than duration indicates
minVideoPts: 40.5 * 1000,
minAudioPts: 40.5 * 1000,
maxVideoPts: 50.5 * 1000,
maxAudioPts: 50.5 * 1000,
uri: '4.ts'
}]
};
equal(Playlist.duration(playlist, 0, 5),
50.5,
'calculated across the larger PTS interval');
});
test('counts the time between segments as part of the earlier segment\'s duration', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 1 * 10 * 1000,
maxAudioPts: 1 * 10 * 1000,
uri: '0.ts'
}, {
minVideoPts: 1 * 10 * 1000 + 100,
minAudioPts: 1 * 10 * 1000 + 100,
maxVideoPts: 2 * 10 * 1000 + 100,
maxAudioPts: 2 * 10 * 1000 + 100,
duration: 10,
uri: '1.ts'
}]
}, 0, 1);
equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap');
});
test('accounts for discontinuities', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
discontinuityStarts: [1],
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 1 * 10 * 1000,
maxAudioPts: 1 * 10 * 1000,
uri: '0.ts'
......@@ -130,6 +324,76 @@
equal(duration, 10 + 10, 'handles discontinuities');
});
test('does not count ending segment gaps across a discontinuity', function() {
var duration = Playlist.duration({
mediaSequence: 0,
discontinuityStarts: [1],
endList: true,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 1 * 10 * 1000,
maxAudioPts: 1 * 10 * 1000,
uri: '0.ts'
}, {
discontinuity: true,
minVideoPts: 1 * 10 * 1000 + 100,
minAudioPts: 1 * 10 * 1000 + 100,
maxVideoPts: 2 * 10 * 1000 + 100,
maxAudioPts: 2 * 10 * 1000 + 100,
duration: 10,
uri: '1.ts'
}]
}, 0, 1);
equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
});
test('trailing duration on the final segment can be excluded', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 1 * 10 * 1000,
maxAudioPts: 1 * 10 * 1000,
uri: '0.ts'
}, {
minVideoPts: 1 * 10 * 1000 + 100,
minAudioPts: 1 * 10 * 1000 + 100,
maxVideoPts: 2 * 10 * 1000 + 100,
maxAudioPts: 2 * 10 * 1000 + 100,
duration: 10,
uri: '1.ts'
}]
}, 0, 1, false);
equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
});
test('a non-positive length interval has zero duration', function() {
var playlist = {
mediaSequence: 0,
discontinuityStarts: [1],
segments: [{
duration: 10,
uri: '0.ts'
}, {
discontinuity: true,
duration: 10,
uri: '1.ts'
}]
};
equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero');
equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero');
equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero');
equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero');
});
module('Playlist Seekable');
test('calculates seekable time ranges from the available segments', function() {
var playlist = {
mediaSequence: 0,
......
......@@ -97,15 +97,13 @@ var
var MockSegmentParser;
if (tags === undefined) {
tags = [];
tags = [{ pts: 0, bytes: new Uint8Array(1) }];
}
MockSegmentParser = function() {
this.getFlvHeader = function() {
return 'flv';
};
this.parseSegmentBinaryData = function() {};
this.timestampOffset = 0;
this.mediaTimelineOffset = 0;
this.flushTags = function() {};
this.tagsAvailable = function() {
return tags.length;
......@@ -1287,30 +1285,32 @@ test('clears in-band cues ahead of current time on seek', function() {
player.hls.segmentParser_.parseSegmentBinaryData = function() {
// trigger a metadata event
if (events.length) {
while (events.length) {
player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
}
};
standardXHRResponse(requests.shift()); // media
tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) });
tags.push({ pts: 0, bytes: new Uint8Array(1) },
{ pts: 10 * 1000, bytes: new Uint8Array(1) });
events.push({
pts: 20 * 1000,
pts: 9.9 * 1000,
data: new Uint8Array([]),
frames: [{
id: 'TXXX',
value: 'cue 3'
value: 'cue 1'
}]
});
events.push({
pts: 9.9 * 1000,
pts: 20 * 1000,
data: new Uint8Array([]),
frames: [{
id: 'TXXX',
value: 'cue 1'
value: 'cue 3'
}]
});
standardXHRResponse(requests.shift()); // segment 0
tags.push({ pts: 20 * 1000, bytes: new Uint8Array(1) });
tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) },
{ pts: 20 * 1000, bytes: new Uint8Array(1) });
events.push({
pts: 19.9 * 1000,
data: new Uint8Array([]),
......@@ -1323,12 +1323,12 @@ test('clears in-band cues ahead of current time on seek', function() {
standardXHRResponse(requests.shift()); // segment 1
track = player.textTracks()[0];
equal(track.cues.length, 2, 'added the cues');
equal(track.cues.length, 3, 'added the cues');
// seek into segment 1
player.currentTime(11);
player.trigger('seeking');
equal(track.cues.length, 1, 'removed a cue');
equal(track.cues.length, 1, 'removed later cues');
equal(track.cues[0].startTime, 9.9, 'retained the earlier cue');
});
......@@ -1342,9 +1342,6 @@ test('translates ID3 PTS values to cue media timeline positions', function() {
openMediaSource(player);
player.hls.segmentParser_.parseSegmentBinaryData = function() {
// setup the timestamp offset
this.timestampOffset = tags[0].pts;
// trigger a metadata event
player.hls.segmentParser_.metadataStream.trigger('data', {
pts: 5 * 1000,
......@@ -1373,9 +1370,6 @@ test('translates ID3 PTS values across discontinuities', function() {
openMediaSource(player);
player.hls.segmentParser_.parseSegmentBinaryData = function() {
if (this.timestampOffset === null) {
this.timestampOffset = tags[0].pts;
}
// trigger a metadata event
if (events.length) {
player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
......@@ -1393,7 +1387,6 @@ test('translates ID3 PTS values across discontinuities', function() {
'1.ts\n');
// segment 0 starts at PTS 14000 and has a cue point at 15000
player.hls.segmentParser_.timestampOffset = 14 * 1000;
tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) },
{ pts: 24 * 1000, bytes: new Uint8Array(1) });
events.push({
......@@ -2010,6 +2003,48 @@ test('continues playing after seek to discontinuity', function() {
strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
});
test('seeking does not fail when targeted between segments', function() {
var tags = [], currentTime, segmentUrl;
videojs.Hls.SegmentParser = mockSegmentParser(tags);
player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
// mock out the currentTime callbacks
player.hls.el().vjs_setProperty = function(property, value) {
if (property === 'currentTime') {
currentTime = value;
}
};
player.hls.el().vjs_getProperty = function(property) {
if (property === 'currentTime') {
return currentTime;
}
};
standardXHRResponse(requests.shift()); // media
tags.push({ pts: 100, bytes: new Uint8Array(1) },
{ pts: 9 * 1000 + 100, bytes: new Uint8Array(1) });
standardXHRResponse(requests.shift()); // segment 0
player.hls.checkBuffer_();
tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) },
{ pts: 20 * 1000 + 100, bytes: new Uint8Array(1) });
segmentUrl = requests[0].url;
standardXHRResponse(requests.shift()); // segment 1
// seek to a time that is greater than the last tag in segment 0 but
// less than the first in segment 1
player.currentTime(9.4);
equal(requests[0].url, segmentUrl, 'requested the later segment');
tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) },
{ pts: 20 * 1000 + 100, bytes: new Uint8Array(1) });
standardXHRResponse(requests.shift()); // segment 1
equal(player.currentTime(), 9.5, 'seeked to the later time');
});
test('resets the switching algorithm if a request times out', function() {
player.src({
src: 'master.m3u8',
......@@ -2666,12 +2701,13 @@ test('treats invalid keys as a key request failure', function() {
equal(bytes[0], 'flv', 'appended the flv header');
tags.length = 0;
tags.push({ pts: 1, bytes: new Uint8Array([1]) });
tags.push({ pts: 2833, bytes: new Uint8Array([1]) },
{ pts: 4833, bytes: new Uint8Array([2]) });
// second segment request
standardXHRResponse(requests.shift());
equal(bytes.length, 2, 'appended bytes');
deepEqual(new Uint8Array([1]), bytes[1], 'skipped to the second segment');
deepEqual(bytes[1], new Uint8Array([1, 2]), 'skipped to the second segment');
});
test('live stream should not call endOfStream', function(){
......