5b501b00 by David LaPalomento

Remove expired time tracking

Media sources implicitly track expired time by retaining the mapping between presentation timestamp values and the media timeline in the buffer. That allows us to simplify a good deal of code by not tracking it ourselves. Finish updating tests to work against the new timeline start and end annotations on segments instead of the old PTS values. Remove metadata cue translation because that is now handled by contrib-media-sources. Update key fetching in HLSe to occur concurrently with the segment download. All tests are now passing.
1 parent 2633a46d
......@@ -2,13 +2,7 @@
* playlist-loader
*
* A state machine that manages the loading, caching, and updating of
* 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 looks like this:
*
* |-- expired --|-- segments --|
* M3U8 playlists.
*
*/
(function(window, videojs) {
......@@ -16,7 +10,6 @@
var
resolveUrl = videojs.Hls.resolveUrl,
xhr = videojs.Hls.xhr,
Playlist = videojs.Hls.Playlist,
mergeOptions = videojs.mergeOptions,
/**
......@@ -158,14 +151,6 @@
// initialize the loader state
loader.state = 'HAVE_NOTHING';
// The total duration of all segments that expired and have been
// removed from the current playlist, in seconds. This property
// should always be zero for non-live playlists. In a live
// playlist, this is the total amount of time that has been
// removed from the stream since the playlist loader began
// tracking it.
loader.expired_ = 0;
// capture the prototype dispose function
dispose = this.dispose;
......@@ -360,43 +345,10 @@
* @param update {object} the updated media playlist object
*/
PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
var expiredCount;
if (this.media_) {
expiredCount = update.mediaSequence - this.media_.mediaSequence;
// update the expired time count
this.expired_ += Playlist.duration(this.media_,
this.media_.mediaSequence,
update.mediaSequence);
}
this.media_ = this.master.playlists[update.uri];
};
/**
* When switching variant playlists in a live stream, the player may
* discover that the new set of available segments is shifted in
* time relative to the old playlist. If that is the case, you can
* call this method to synchronize the playlist loader so that
* subsequent calls to getMediaIndexForTime_() return values
* appropriate for the new playlist.
*
* @param mediaIndex {integer} the index of the segment that will be
* the used to base timeline calculations on
* @param startTime {number} the media timeline position of the
* first moment of video data for the specified segment. That is,
* data from the specified segment will first be displayed when
* `currentTime` is equal to `startTime`.
*/
PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) {
var segmentOffset = Playlist.duration(this.media_,
this.media_.mediaSequence,
this.media_.mediaSequence + mediaIndex);
this.expired_ = startingTime - segmentOffset;
};
/**
* Determine the index of the segment that contains a specified
* playback position in the current media playlist. Early versions
* of the HLS specification require segment durations to be rounded
......@@ -423,7 +375,6 @@
// when the requested position is earlier than the current set of
// segments, return the earliest segment index
time -= this.expired_;
if (time < 0) {
return 0;
}
......@@ -447,6 +398,11 @@
time -= segment.start;
time -= segment.duration || targetDuration;
if (time < 0) {
// the segment with start information is also our best guess
// for the momment
return i;
}
break;
}
}
......
......@@ -26,47 +26,46 @@
/**
* 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.
* may be calculated by specifying an end index.
*
* @param playlist {object} a media playlist object
* @param startSequence {number} (optional) an inclusive lower
* boundary for the playlist. Defaults to 0.
* @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.
*/
intervalDuration = function(playlist, startSequence, endSequence) {
var result = 0, targetDuration, i, start, end, expiredSegmentCount;
intervalDuration = function(playlist, endSequence) {
var result = 0, segment, targetDuration, i;
if (startSequence === undefined) {
startSequence = playlist.mediaSequence || 0;
}
if (endSequence === undefined) {
endSequence = startSequence + (playlist.segments || []).length;
endSequence = playlist.mediaSequence + (playlist.segments || []).length;
}
if (endSequence < 0) {
return 0;
}
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
// accumulate while looking for the latest known segment-timeline mapping
expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0);
start = startSequence + expiredSegmentCount - playlist.mediaSequence;
end = endSequence - playlist.mediaSequence;
for (i = end - 1; i >= start; i--) {
if (playlist.segments[i].end !== undefined) {
result += playlist.segments[i].end;
return result;
i = endSequence - playlist.mediaSequence;
// if a start time is available for segment immediately following
// the interval, use it
segment = playlist.segments[i];
// Walk backward until we find the latest segment with timeline
// information that is earlier than endSequence
if (segment && segment.start !== undefined) {
return segment.start;
}
while (i--) {
segment = playlist.segments[i];
if (segment.end !== undefined) {
return result + segment.end;
}
result += playlist.segments[i].duration || targetDuration;
result += (segment.duration || targetDuration);
if (playlist.segments[i].start !== undefined) {
result += playlist.segments[i].start;
return result;
if (segment.start !== undefined) {
return result + segment.start;
}
}
// neither a start or end time was found in the interval so we
// have to estimate the expired duration
result += expiredSegmentCount * targetDuration;
return result;
};
......@@ -76,17 +75,16 @@
* timeline between those two indices. The total duration for live
* playlists is always Infinity.
* @param playlist {object} a media playlist object
* @param startSequence {number} (optional) an inclusive lower
* 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
* @param endSequence {number} (optional) an exclusive upper
* boundary for the playlist. Defaults to the playlist media
* sequence number plus its 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, includeTrailingTime) {
duration = function(playlist, endSequence, includeTrailingTime) {
if (!playlist) {
return 0;
}
......@@ -97,7 +95,7 @@
// if a slice of the total duration is not requested, use
// playlist-level duration indicators when they're present
if (startSequence === undefined && endSequence === undefined) {
if (endSequence === undefined) {
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
......@@ -111,7 +109,6 @@
// calculate the total duration based on the segment durations
return intervalDuration(playlist,
startSequence,
endSequence,
includeTrailingTime);
};
......@@ -128,7 +125,7 @@
* for seeking
*/
seekable = function(playlist) {
var start, end, liveBuffer, targetDuration, segment, pending, i;
var start, end;
// without segments, there are no seekable ranges
if (!playlist.segments) {
......@@ -139,33 +136,14 @@
return videojs.createTimeRange(0, duration(playlist));
}
start = 0;
end = intervalDuration(playlist,
playlist.mediaSequence,
playlist.mediaSequence + playlist.segments.length);
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
// 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 = optionalMin(duration(playlist,
playlist.mediaSequence + i,
playlist.mediaSequence + i + 1),
liveBuffer);
liveBuffer -= pending;
end -= pending;
}
}
start = intervalDuration(playlist, playlist.mediaSequence);
end = intervalDuration(playlist,
playlist.mediaSequence + playlist.segments.length);
end -= (playlist.targetDuration || DEFAULT_TARGET_DURATION) * 3;
end = Math.max(0, end);
return videojs.createTimeRange(start, end);
};
......
......@@ -53,15 +53,6 @@
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.expired_, 0, 'zero seconds expired');
});
test('requests the initial playlist immediately', function() {
new videojs.Hls.PlaylistLoader('master.m3u8');
strictEqual(requests.length, 1, 'made a request');
......@@ -175,101 +166,6 @@
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.expired_, 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.expired_, 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.expired_, 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.expired_, 13 + 4, 'tracked expired prior to the 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.expired_, 4 + 5 + 6, 'tracked both expired discontinuities');
});
test('emits an error when an initial playlist request fails', function() {
var
errors = [],
......@@ -757,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),
2,
'the index is never greater than the length');
3,
'time greater than the length is index 3');
});
test('returns the lower index when calculating for a segment boundary', function() {
......@@ -785,7 +681,7 @@
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
loader.expired_ = 150;
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');
......@@ -797,30 +693,6 @@
equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
});
test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() {
var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:23\n' +
'#EXTINF:4,\n' +
'23.ts\n' +
'#EXTINF:5,\n' +
'24.ts\n' +
'#EXTINF:6,\n' +
'25.ts\n' +
'#EXTINF:7,\n' +
'26.ts\n');
loader.updateTimelineOffset(0, 150);
equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment');
equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero');
equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment');
loader.updateTimelineOffset(2, 30);
equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment');
equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment');
equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment');
});
test('does not misintrepret playlists missing newlines at the end', function() {
var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
requests.shift().respond(200, null,
......
......@@ -18,27 +18,6 @@
module('Playlist Interval Duration');
test('accounts expired duration for live playlists', function() {
var duration = Playlist.duration({
mediaSequence: 10,
segments: [{
duration: 10,
uri: '10.ts'
}, {
duration: 10,
uri: '11.ts'
}, {
duration: 10,
uri: '12.ts'
}, {
duration: 10,
uri: '13.ts'
}]
}, 0, 14);
equal(duration, 14 * 10, 'duration includes dropped segments');
});
test('accounts for non-zero starting VOD media sequences', function() {
var duration = Playlist.duration({
mediaSequence: 10,
......@@ -61,47 +40,37 @@
equal(duration, 4 * 10, 'includes only listed segments');
});
test('uses PTS values when available', function() {
test('uses timeline values when available', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 1,
minAudioPts: 2,
start: 0,
uri: '0.ts'
}, {
duration: 10,
maxVideoPts: 2 * 10 * 1000 + 1,
maxAudioPts: 2 * 10 * 1000 + 2,
end: 2 * 10 + 2,
uri: '1.ts'
}, {
duration: 10,
maxVideoPts: 3 * 10 * 1000 + 1,
maxAudioPts: 3 * 10 * 1000 + 2,
end: 3 * 10 + 2,
uri: '2.ts'
}, {
duration: 10,
maxVideoPts: 4 * 10 * 1000 + 1,
maxAudioPts: 4 * 10 * 1000 + 2,
end: 4 * 10 + 2,
uri: '3.ts'
}]
}, 0, 4);
}, 4);
equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values');
equal(duration, 4 * 10 + 2, 'used timeline values');
});
test('works when partial PTS information is available', function() {
test('works when partial timeline information is available', function() {
var duration = Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 1,
minAudioPts: 2,
maxVideoPts: 10 * 1000 + 1,
// intentionally less duration than video
// the max stream duration should be used
maxAudioPts: 10 * 1000 + 1,
start: 0,
uri: '0.ts'
}, {
duration: 9,
......@@ -111,67 +80,17 @@
uri: '2.ts'
}, {
duration: 10,
minVideoPts: 30 * 1000 + 7,
minAudioPts: 30 * 1000 + 10,
maxVideoPts: 40 * 1000 + 1,
maxAudioPts: 40 * 1000 + 2,
start: 30.007,
end: 40.002,
uri: '3.ts'
}, {
duration: 10,
maxVideoPts: 50 * 1000 + 1,
maxAudioPts: 50 * 1000 + 2,
end: 50.0002,
uri: '4.ts'
}]
}, 0, 5);
}, 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,
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,
uri: '3.ts'
}]
}, 2, 4);
equal(duration, 10 + 10, 'excluded the earlier segments');
equal(duration, 50.0002, 'calculated with mixed intervals');
});
test('ignores discontinuity sequences later than the end', function() {
......@@ -196,20 +115,19 @@
duration: 10,
uri: '3.ts'
}]
}, 0, 2);
}, 2);
equal(duration, 19, 'excluded the later segments');
});
test('handles trailing segments without PTS information', function() {
var duration = Playlist.duration({
test('handles trailing segments without timeline information', function() {
var playlist, duration;
playlist = {
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
start: 0,
end: 10.5,
uri: '0.ts'
}, {
duration: 9,
......@@ -218,107 +136,43 @@
duration: 10,
uri: '2.ts'
}, {
minVideoPts: 29.5 * 1000,
minAudioPts: 29.5 * 1000,
maxVideoPts: 39.5 * 1000,
maxAudioPts: 39.5 * 1000,
start: 29.45,
end: 39.5,
uri: '3.ts'
}]
}, 0, 3);
};
duration = Playlist.duration(playlist, 3);
equal(duration, 29.45, 'calculated duration');
equal(duration, 29.5, 'calculated duration');
duration = Playlist.duration(playlist, 2);
equal(duration, 19.5, 'calculated duration');
});
test('uses PTS intervals when the start and end segment have them', function() {
test('uses timeline intervals when segments have them', function() {
var playlist, duration;
playlist = {
mediaSequence: 0,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 10 * 1000,
maxAudioPts: 10 * 1000,
start: 0,
end: 10,
uri: '0.ts'
}, {
duration: 9,
uri: '1.ts'
},{
minVideoPts: 20 * 1000 + 100,
minAudioPts: 20 * 1000 + 100,
maxVideoPts: 30 * 1000 + 100,
maxAudioPts: 30 * 1000 + 100,
start: 20.1,
end: 30.1,
duration: 10,
uri: '2.ts'
}]
};
duration = Playlist.duration(playlist, 0, 2);
duration = Playlist.duration(playlist, 2);
equal(duration, 20.1, 'used the PTS-based interval');
equal(duration, 20.1, 'used the timeline-based interval');
duration = Playlist.duration(playlist, 0, 3);
equal(duration, 30.1, 'used the PTS-based interval');
});
test('works for media without audio', function() {
equal(Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 0,
maxVideoPts: 9 * 1000,
uri: 'no-audio.ts'
}]
}), 9, 'used video PTS values');
});
test('works for media without video', function() {
equal(Playlist.duration({
mediaSequence: 0,
endList: true,
segments: [{
minAudioPts: 0,
maxAudioPts: 9 * 1000,
uri: 'no-video.ts'
}]
}), 9, 'used video PTS values');
});
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');
duration = Playlist.duration(playlist, 3);
equal(duration, 30.1, 'used the timeline-based interval');
});
test('counts the time between segments as part of the earlier segment\'s duration', function() {
......@@ -326,22 +180,18 @@
mediaSequence: 0,
endList: true,
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 1 * 10 * 1000,
maxAudioPts: 1 * 10 * 1000,
start: 0,
end: 10,
uri: '0.ts'
}, {
minVideoPts: 1 * 10 * 1000 + 100,
minAudioPts: 1 * 10 * 1000 + 100,
maxVideoPts: 2 * 10 * 1000 + 100,
maxAudioPts: 2 * 10 * 1000 + 100,
start: 10.1,
end: 20.1,
duration: 10,
uri: '1.ts'
}]
}, 0, 1);
}, 1);
equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap');
equal(duration, 10.1, 'included the segment gap');
});
test('accounts for discontinuities', function() {
......@@ -364,7 +214,7 @@
duration: 10,
uri: '1.ts'
}]
}, 0, 2);
}, 2);
equal(duration, 10 + 10, 'handles discontinuities');
});
......@@ -389,7 +239,7 @@
duration: 10,
uri: '1.ts'
}]
}, 0, 1);
}, 1);
equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
});
......@@ -412,7 +262,7 @@
duration: 10,
uri: '1.ts'
}]
}, 0, 1, false);
}, 1, false);
equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
});
......@@ -431,10 +281,9 @@
}]
};
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');
equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero');
equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero');
equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
});
module('Playlist Seekable');
......