b27cedcc by David LaPalomento

Report seekable relative to the earliest possible position at load start

Keep track of the accurate durations of expired segments in the playlist loader so that it's possible to accurately calculate the start and end points of the seekable ranges relative to media timeline position zero, even if we switch variant streams or seek within a live stream. Track the media timeline position of the last discontinuity to allow for PTS-based variant stream synchronization instead of the incorrect media sequence based method we're currently using. Stop rewriting timestamps in the transmuxed FLV tags for that reason as well. Add m3u8 parser support for EXT-X-DISCONTINUITY-SEQUENCE.
1 parent d74b33c8
Showing 48 changed files with 521 additions and 207 deletions
......@@ -12,7 +12,6 @@ var
window.videojs.Hls.AacStream = function() {
var
next_pts, // :uint
pts_offset, // :int
state, // :uint
pes_length, // :int
lastMetaPts,
......@@ -32,7 +31,6 @@ window.videojs.Hls.AacStream = function() {
// (pts:uint):void
this.setTimeStampOffset = function(pts) {
pts_offset = pts;
// keep track of the last time a metadata tag was written out
// set the initial value so metadata will be generated before any
......@@ -42,7 +40,7 @@ window.videojs.Hls.AacStream = function() {
// (pts:uint, pes_size:int, dataAligned:Boolean):void
this.setNextTimeStamp = function(pts, pes_size, dataAligned) {
next_pts = pts - pts_offset;
next_pts = pts;
pes_length = pes_size;
// If data is aligned, flush all internal buffers
......
......@@ -37,7 +37,6 @@
window.videojs.Hls.H264Stream = H264Stream = function() {
this._next_pts = 0; // :uint;
this._next_dts = 0; // :uint;
this._pts_offset = 0; // :int
this._h264Frame = null; // :FlvTag
......@@ -52,15 +51,13 @@
};
//(pts:uint):void
H264Stream.prototype.setTimeStampOffset = function(pts) {
this._pts_offset = pts;
};
H264Stream.prototype.setTimeStampOffset = function() {};
//(pts:uint, dts:uint, dataAligned:Boolean):void
H264Stream.prototype.setNextTimeStamp = function(pts, dts, dataAligned) {
// We could end up with a DTS less than 0 here. We need to deal with that!
this._next_pts = pts - this._pts_offset;
this._next_dts = dts - this._pts_offset;
this._next_pts = pts;
this._next_dts = dts;
// If data is aligned, flush all internal buffers
if (dataAligned) {
......
......@@ -210,6 +210,18 @@
this.trigger('data', event);
return;
}
match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'discontinuity-sequence'
};
if (match[1]) {
event.number = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
if (match) {
event = {
......@@ -308,7 +320,7 @@
event.attributes = parseAttributes(match[1]);
// parse the IV string into a Uint32Array
if (event.attributes.IV) {
if (event.attributes.IV.substring(0,2) === '0x') {
if (event.attributes.IV.substring(0,2) === '0x') {
event.attributes.IV = event.attributes.IV.substring(2);
}
......@@ -409,6 +421,12 @@
message: 'defaulting media sequence to zero'
});
}
if (!('discontinuitySequence' in this.manifest)) {
this.manifest.discontinuitySequence = 0;
this.trigger('info', {
message: 'defaulting discontinuity sequence to zero'
});
}
if (entry.duration >= 0) {
currentUri.duration = entry.duration;
}
......@@ -459,6 +477,15 @@
}
this.manifest.mediaSequence = entry.number;
},
'discontinuity-sequence': function() {
if (!isFinite(entry.number)) {
this.trigger('warn', {
message: 'ignoring invalid discontinuity sequence: ' + entry.number
});
return;
}
this.manifest.discontinuitySequence = entry.number;
},
'playlist-type': function() {
if (!(/VOD|EVENT/).test(entry.playlistType)) {
this.trigger('warn', {
......
/**
* A state machine that manages the loading, caching, and updating of
* M3U8 playlists.
* M3U8 playlists. When tracking a live playlist, loaders will keep
* track of the duration of content that expired since the loader was
* initialized and when the current discontinuity sequence was
* encountered. A complete media timeline for a live playlist with
* expiring segments and discontinuities looks like this:
*
* |-- expiredPreDiscontinuity --|-- expiredPostDiscontinuity --|-- segments --|
*
* You can use these values to calculate how much time has elapsed
* since the stream began loading or how long it has been since the
* most recent discontinuity was encountered, for instance.
*/
(function(window, videojs) {
'use strict';
var
resolveUrl = videojs.Hls.resolveUrl,
xhr = videojs.Hls.xhr,
Playlist = videojs.Hls.Playlist,
/**
* Returns a new master playlist that is the result of merging an
......@@ -51,66 +62,84 @@
var
loader = this,
dispose,
media,
mediaUpdateTimeout,
request,
haveMetadata;
haveMetadata = function(error, xhr, url) {
var parser, refreshDelay, update;
PlaylistLoader.prototype.init.call(this);
loader.setBandwidth(request || xhr);
if (!srcUrl) {
throw new Error('A non-empty playlist URL is required');
}
// any in-flight request is now finished
request = null;
// update the playlist loader's state in response to a new or
// updated playlist.
haveMetadata = function(error, xhr, url) {
var parser, refreshDelay, update;
if (error) {
loader.error = {
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
responseText: xhr.responseText,
code: (xhr.status >= 500) ? 4 : 2
};
return loader.trigger('error');
}
loader.setBandwidth(request || xhr);
loader.state = 'HAVE_METADATA';
// any in-flight request is now finished
request = null;
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
parser.end();
parser.manifest.uri = url;
// merge this playlist into the master
update = updateMaster(loader.master, parser.manifest);
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (update) {
loader.master = update;
media = loader.master.playlists[url];
} else {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
refreshDelay /= 2;
}
if (error) {
loader.error = {
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
responseText: xhr.responseText,
code: (xhr.status >= 500) ? 4 : 2
};
return loader.trigger('error');
}
// refresh live playlists after a target duration passes
if (!loader.media().endList) {
window.clearTimeout(mediaUpdateTimeout);
mediaUpdateTimeout = window.setTimeout(function() {
loader.trigger('mediaupdatetimeout');
}, refreshDelay);
}
loader.state = 'HAVE_METADATA';
loader.trigger('loadedplaylist');
};
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
parser.end();
parser.manifest.uri = url;
// merge this playlist into the master
update = updateMaster(loader.master, parser.manifest);
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (update) {
loader.master = update;
loader.updateMediaPlaylist_(parser.manifest);
} else {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
refreshDelay /= 2;
}
PlaylistLoader.prototype.init.call(this);
// refresh live playlists after a target duration passes
if (!loader.media().endList) {
window.clearTimeout(mediaUpdateTimeout);
mediaUpdateTimeout = window.setTimeout(function() {
loader.trigger('mediaupdatetimeout');
}, refreshDelay);
}
if (!srcUrl) {
throw new Error('A non-empty playlist URL is required');
}
loader.trigger('loadedplaylist');
};
// initialize the loader state
loader.state = 'HAVE_NOTHING';
// the total duration of all segments that expired and have been
// removed from the current playlist after the last
// #EXT-X-DISCONTINUITY. In a live playlist without
// discontinuities, this is the total amount of time that has
// been removed from the stream since the playlist loader began
// tracking it.
loader.expiredPostDiscontinuity_ = 0;
// the total duration of all segments that expired and have been
// removed from the current playlist before the last
// #EXT-X-DISCONTINUITY. The total amount of time that has
// expired is always the sum of expiredPreDiscontinuity_ and
// expiredPostDiscontinuity_.
loader.expiredPreDiscontinuity_ = 0;
// capture the prototype dispose function
dispose = this.dispose;
......@@ -141,7 +170,7 @@
var mediaChange = false;
// getter
if (!playlist) {
return media;
return loader.media_;
}
// setter
......@@ -158,7 +187,7 @@
playlist = loader.master.playlists[playlist];
}
mediaChange = playlist.uri !== media.uri;
mediaChange = playlist.uri !== loader.media_.uri;
// switch to fully loaded playlists immediately
if (loader.master.playlists[playlist.uri].endList) {
......@@ -169,7 +198,7 @@
request = null;
}
loader.state = 'HAVE_METADATA';
media = playlist;
loader.media_ = playlist;
// trigger media change if the active media has been updated
if (mediaChange) {
......@@ -292,5 +321,46 @@
};
PlaylistLoader.prototype = new videojs.Hls.Stream();
/**
* Update the PlaylistLoader state to reflect the changes in an
* update to the current media playlist.
* @param update {object} the updated media playlist object
*/
PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
var lastDiscontinuity, expiredCount, i;
if (this.media_) {
expiredCount = update.mediaSequence - this.media_.mediaSequence;
// setup the index for duration calculations so that the newly
// expired time will be accumulated after the last
// discontinuity, unless we discover otherwise
lastDiscontinuity = this.media_.mediaSequence;
if (this.media_.discontinuitySequence !== update.discontinuitySequence) {
i = expiredCount;
while (i--) {
if (this.media_.segments[i].discontinuity) {
// a segment that begins a new discontinuity sequence has expired
lastDiscontinuity = i + this.media_.mediaSequence;
this.expiredPreDiscontinuity_ += this.expiredPostDiscontinuity_;
this.expiredPostDiscontinuity_ = 0;
break;
}
}
}
// update the expirated durations
this.expiredPreDiscontinuity_ += Playlist.duration(this.media_,
this.media_.mediaSequence,
lastDiscontinuity);
this.expiredPostDiscontinuity_ += Playlist.duration(this.media_,
lastDiscontinuity,
this.media_.mediaSequence + expiredCount);
}
this.media_ = this.master.playlists[update.uri];
};
videojs.Hls.PlaylistLoader = PlaylistLoader;
})(window, window.videojs);
......
......@@ -10,38 +10,37 @@
/**
* Calculate the media duration from the segments associated with a
* playlist. The duration of a subinterval of the available segments
* may be calculated by specifying a start and end index. The
* minimum recommended live buffer is automatically subtracted for
* the last segments of live playlists.
* may be calculated by specifying a start and end index.
*
* @param playlist {object} a media playlist object
* @param startIndex {number} (optional) an inclusive lower
* @param startSequence {number} (optional) an inclusive lower
* boundary for the playlist. Defaults to 0.
* @param endIndex {number} (optional) an exclusive upper boundary
* @param endSequence {number} (optional) an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @return {number} the duration between the start index and end
* index.
*/
segmentsDuration = function(playlist, startIndex, endIndex) {
var targetDuration, i, segment, result = 0;
segmentsDuration = function(playlist, startSequence, endSequence) {
var targetDuration, i, segment, expiredSegmentCount, result = 0;
startIndex = startIndex || 0;
endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
startSequence = startSequence || 0;
i = startSequence;
endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length;
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
for (i = endIndex - 1; i >= startIndex; i--) {
segment = playlist.segments[i];
// 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];
result += segment.preciseDuration ||
segment.duration ||
targetDuration;
}
// live playlists should not expose three segment durations worth
// of content from the end of the playlist
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
if (!playlist.endList) {
result -= targetDuration * (3 - (playlist.segments.length - endIndex));
}
return result;
};
......@@ -51,21 +50,21 @@
* timeline between those two indices. The total duration for live
* playlists is always Infinity.
* @param playlist {object} a media playlist object
* @param startIndex {number} (optional) an inclusive lower
* @param startSequence {number} (optional) an inclusive lower
* boundary for the playlist. Defaults to 0.
* @param endIndex {number} (optional) an exclusive upper boundary
* @param endSequence {number} (optional) an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @return {number} the duration between the start index and end
* index.
*/
duration = function(playlist, startIndex, endIndex) {
duration = function(playlist, startSequence, endSequence) {
if (!playlist) {
return 0;
}
// if a slice of the total duration is not requested, use
// playlist-level duration indicators when they're present
if (startIndex === undefined && endIndex === undefined) {
if (startSequence === undefined && endSequence === undefined) {
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
......@@ -79,8 +78,8 @@
// calculate the total duration based on the segment durations
return segmentsDuration(playlist,
startIndex,
endIndex);
startSequence,
endSequence);
};
/**
......@@ -91,7 +90,8 @@
* for seeking
*/
seekable = function(playlist) {
var startOffset, targetDuration;
var start, end, liveBuffer, targetDuration, segment, pending, i;
// without segments, there are no seekable ranges
if (!playlist.segments) {
return videojs.createTimeRange();
......@@ -101,10 +101,34 @@
return videojs.createTimeRange(0, duration(playlist));
}
start = segmentsDuration(playlist, 0, playlist.mediaSequence);
end = start + segmentsDuration(playlist,
playlist.mediaSequence,
playlist.mediaSequence + playlist.segments.length);
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
startOffset = targetDuration * (playlist.mediaSequence || 0);
return videojs.createTimeRange(startOffset,
startOffset + segmentsDuration(playlist));
// live playlists should not expose three segment durations worth
// of content from the end of the playlist
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
if (!playlist.endList) {
liveBuffer = targetDuration * 3;
// walk backward from the last available segment and track how
// much media time has elapsed until three target durations have
// been traversed. if a segment is part of the interval being
// reported, subtract the overlapping portion of its duration
// from the result.
for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) {
segment = playlist.segments[i];
pending = Math.min(segment.preciseDuration ||
segment.duration ||
targetDuration,
liveBuffer);
liveBuffer -= pending;
end -= pending;
}
}
return videojs.createTimeRange(start, end);
};
// exports
......
......@@ -39,11 +39,20 @@ videojs.Hls = videojs.Flash.extend({
this.currentTime = videojs.Hls.prototype.currentTime;
this.setCurrentTime = videojs.Hls.prototype.setCurrentTime;
// a queue of segments that need to be transmuxed and processed,
// and then fed to the source buffer
this.segmentBuffer_ = [];
// periodically check if new data needs to be downloaded or
// buffered data should be appended to the source buffer
this.segmentBuffer_ = [];
this.startCheckingBuffer_();
// the earliest presentation timestamp (PTS) encountered since the
// last #EXT-X-DISCONTINUITY. In a playlist without
// discontinuities, this will be the PTS value for the first frame
// in the video. PTS values are necessary to properly synchronize
// playback when switching to a variant stream.
this.lastStartingPts_ = undefined;
videojs.Hls.prototype.src.call(this, options.source && options.source.src);
}
});
......@@ -356,8 +365,16 @@ videojs.Hls.prototype.duration = function() {
};
videojs.Hls.prototype.seekable = function() {
var absoluteSeekable, startOffset, media;
if (this.playlists) {
return videojs.Hls.Playlist.seekable(this.playlists.media());
// report the seekable range relative to the earliest possible
// position when the stream was first loaded
media = this.playlists.media();
absoluteSeekable = videojs.Hls.Playlist.seekable(media);
startOffset = this.playlists.expiredPostDiscontinuity_ - this.playlists.expiredPreDiscontinuity_;
return videojs.createTimeRange(startOffset,
startOffset + (absoluteSeekable.end(0) - absoluteSeekable.start(0)));
}
return videojs.createTimeRange();
};
......@@ -691,9 +708,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
tech.setBandwidth(this);
// package up all the work to append the segment
// if the segment is the start of a timestamp discontinuity,
// we have to wait until the sourcebuffer is empty before
// aborting the source buffer processing
segmentInfo = {
// the segment's mediaIndex at the time it was received
mediaIndex: tech.mediaIndex,
......@@ -796,7 +810,10 @@ videojs.Hls.prototype.drainBuffer = function(event) {
}
event = event || {};
segmentOffset = videojs.Hls.Playlist.duration(playlist, 0, mediaIndex) * 1000;
segmentOffset = this.playlists.expiredPreDiscontinuity_;
segmentOffset += this.playlists.expiredPostDiscontinuity_;
segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex);
segmentOffset *= 1000;
// transmux the segment data from MP2T to FLV
this.segmentParser_.parseSegmentBinaryData(bytes);
......@@ -808,10 +825,10 @@ videojs.Hls.prototype.drainBuffer = function(event) {
tags.push(this.segmentParser_.getNextTag());
}
// 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) {
// 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
segment.preciseDuration = videojs.Hls.FlvTag.durationFromTags(tags) * 0.001;
}
......
......@@ -61,45 +61,6 @@ test('metadata is generated for IDRs after a full NAL unit is written', function
ok(h264Stream.tags[2].keyFrame, 'key frame is written');
});
test('starting PTS values can be negative', function() {
var
H264ExtraData = videojs.Hls.H264ExtraData,
oldExtraData = H264ExtraData.prototype.extraDataTag,
oldMetadata = H264ExtraData.prototype.metaDataTag,
h264Stream;
H264ExtraData.prototype.extraDataTag = function() {
return 'extraDataTag';
};
H264ExtraData.prototype.metaDataTag = function() {
return 'metaDataTag';
};
h264Stream = new videojs.Hls.H264Stream();
h264Stream.setTimeStampOffset(-100);
h264Stream.setNextTimeStamp(-100, -100, true);
h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
h264Stream.setNextTimeStamp(-99, -99, true);
h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
h264Stream.setNextTimeStamp(0, 0, true);
h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
// flush out the last tag
h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
strictEqual(h264Stream.tags.length, 3, 'three tags are ready');
strictEqual(h264Stream.tags[0].pts, 0, 'the first PTS is zero');
strictEqual(h264Stream.tags[0].dts, 0, 'the first DTS is zero');
strictEqual(h264Stream.tags[1].pts, 1, 'the second PTS is one');
strictEqual(h264Stream.tags[1].dts, 1, 'the second DTS is one');
strictEqual(h264Stream.tags[2].pts, 100, 'the third PTS is 100');
strictEqual(h264Stream.tags[2].dts, 100, 'the third DTS is 100');
H264ExtraData.prototype.extraDataTag = oldExtraData;
H264ExtraData.prototype.metaDataTag = oldMetadata;
});
test('make sure we add metadata and extra data at the beginning of a stream', function() {
var
H264ExtraData = videojs.Hls.H264ExtraData,
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -137,5 +137,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
{
"allowCache": true,
"mediaSequence": 0,
"discontinuitySequence": 3,
"segments": [
{
"duration": 10,
"uri": "001.ts"
},
{
"duration": 19,
"uri": "002.ts"
},
{
"discontinuity": true,
"duration": 10,
"uri": "003.ts"
},
{
"duration": 11,
"uri": "004.ts"
}
],
"targetDuration": 19,
"endList": true
}
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:19
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-DISCONTINUITY-SEQUENCE:3
#EXTINF:10,0
001.ts
#EXTINF:19,0
002.ts
#EXT-X-DISCONTINUITY
#EXTINF:10,0
003.ts
#EXTINF:11,0
004.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"discontinuitySequence": 0,
"segments": [
{
"duration": 10,
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -28,5 +28,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
{
"allowCache": true,
"mediaSequence": 7794,
"discontinuitySequence": 0,
"segments": [
{
"duration": 2.833,
......
......@@ -29,5 +29,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -8,5 +8,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -13,5 +13,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -28,5 +28,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -140,5 +140,6 @@
"uri": "hls_450k_video.ts"
}
],
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -16,5 +16,6 @@
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"discontinuitySequence": 0
}
......
......@@ -39,5 +39,6 @@
"uri": "009.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
"targetDuration": 10,
"discontinuitySequence": 0
}
......
......@@ -7,5 +7,6 @@
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -24,5 +24,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -8,5 +8,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -11,5 +11,6 @@
"uri": "00002.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"discontinuitySequence": 0
}
......
......@@ -17,5 +17,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -18,5 +18,6 @@
"uri": "004.ts",
"duration": 10
}
]
],
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -141,5 +141,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -9,5 +9,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -9,5 +9,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -21,5 +21,6 @@
}
],
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
"endList": true,
"discontinuitySequence": 0
}
......
......@@ -50,6 +50,20 @@
strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
});
test('starts with no expired time', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
equal(loader.expiredPreDiscontinuity_,
0,
'zero seconds expired pre-discontinuity');
equal(loader.expiredPostDiscontinuity_,
0,
'zero seconds expired post-discontinuity');
});
test('requests the initial playlist immediately', function() {
new videojs.Hls.PlaylistLoader('master.m3u8');
strictEqual(requests.length, 1, 'made a request');
......@@ -160,6 +174,105 @@
strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
});
test('increments expired seconds after a segment is removed', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n' +
'#EXTINF:10,\n' +
'4.ts\n');
equal(loader.expiredPostDiscontinuity_, 10, 'expired one segment');
});
test('increments expired seconds after a discontinuity', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
equal(loader.expiredPreDiscontinuity_, 0, 'identifies pre-discontinuity time');
equal(loader.expiredPostDiscontinuity_, 10, 'expired one segment');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:2\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
equal(loader.expiredPreDiscontinuity_, 0, 'tracked time across the discontinuity');
equal(loader.expiredPostDiscontinuity_, 13, 'no expirations after the discontinuity yet');
clock.tick(10 * 1000); // 10s, one target duration
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'3.ts\n');
equal(loader.expiredPreDiscontinuity_, 13, 'did not increment pre-discontinuity');
equal(loader.expiredPostDiscontinuity_, 4, 'expired post-discontinuity');
});
test('tracks expired seconds properly when two discontinuities expire at once', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXTINF:7,\n' +
'3.ts\n');
clock.tick(10 * 1000);
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
'#EXTINF:7,\n' +
'3.ts\n');
equal(loader.expiredPreDiscontinuity_, 4 + 5, 'tracked pre-discontinuity time');
equal(loader.expiredPostDiscontinuity_, 6, 'tracked post-discontinuity time');
});
test('emits an error when an initial playlist request fails', function() {
var
errors = [],
......
......@@ -16,28 +16,31 @@
equal(duration, Infinity, 'duration is infinity');
});
test('interval duration does not include upcoming live segments', function() {
test('interval duration accounts for media sequences', function() {
var duration = Playlist.duration({
mediaSequence: 10,
endList: true,
segments: [{
duration: 4,
uri: '0.ts'
duration: 10,
uri: '10.ts'
}, {
duration: 10,
uri: '1.ts'
uri: '11.ts'
}, {
duration: 10,
uri: '2.ts'
uri: '12.ts'
}, {
duration: 10,
uri: '3.ts'
uri: '13.ts'
}]
}, 0, 3);
}, 0, 14);
equal(duration, 4, 'does not include upcoming live segments');
equal(duration, 14 * 10, 'duration includes dropped segments');
});
test('calculates seekable time ranges from the available segments', function() {
var playlist = {
mediaSequence: 0,
segments: [{
duration: 10,
uri: '0.ts'
......@@ -66,6 +69,7 @@
test('seekable end is three target durations from the actual end of live playlists', function() {
var seekable = Playlist.seekable({
mediaSequence: 0,
segments: [{
duration: 7,
uri: '0.ts'
......@@ -107,6 +111,7 @@
test('seekable end accounts for non-standard target durations', function() {
var seekable = Playlist.seekable({
targetDuration: 2,
mediaSequence: 0,
segments: [{
duration: 2,
uri: '0.ts'
......
......@@ -293,6 +293,31 @@ test('calculates the duration if needed', function() {
'duration is calculated');
});
test('translates seekable by the starting time for live playlists', function() {
var seekable;
player.src({
src: 'media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:15\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
seekable = player.seekable();
equal(seekable.length, 1, 'one seekable range');
equal(seekable.start(0), 0, 'the earliest possible position is at zero');
equal(seekable.end(0), 10, 'end is relative to the start');
});
test('starts downloading a segment on loadedmetadata', function() {
player.src({
src: 'manifest/media.m3u8',
......