3e33b1f5 by David LaPalomento

Use information from buffering to calculate seekable

Look forward and backward through the playlist to find an anchor to base seekable calculations on. This is helpful for live playlists in particular, where the only way to determine the correct starting position for the seekable range is to look at where future segments have ended up in the buffer. Without this change, the start of seekable was always zero, even if the live sliding window had moved forward.
1 parent 56919c5f
......@@ -389,7 +389,10 @@
return 0;
}
// 1) Walk backward until we find the first segment with timeline
// find segments with known timing information that bound the
// target time
// Walk backward until we find the first segment with timeline
// information that is earlier than `time`
for (i = lastSegment; i >= 0; i--) {
segment = this.media_.segments[i];
......@@ -414,7 +417,7 @@
}
}
// 2) Walk forward until we find the first segment with timeline
// Walk forward until we find the first segment with timeline
// information that is greater than `time`
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
......@@ -423,7 +426,8 @@
knownEnd = segment.start;
if (endIndex < 0) {
// The first segment claims to start *after* the time we are
// searching for so just return it
// searching for so the target segment must no longer be
// available
return -1;
}
break;
......@@ -435,6 +439,9 @@
}
}
// use the bounds we just found and playlist information to
// estimate the segment that contains the time we are looking for
if (startIndex !== undefined) {
// We have a known-start point that is before our desired time so
// walk from that point forwards
......
......@@ -4,23 +4,70 @@
(function(window, videojs) {
'use strict';
var DEFAULT_TARGET_DURATION = 10;
var duration, intervalDuration, optionalMin, optionalMax, seekable;
// Math.min that will return the alternative input if one of its
// parameters in undefined
optionalMin = function(left, right) {
left = isFinite(left) ? left : Infinity;
right = isFinite(right) ? right : Infinity;
return Math.min(left, right);
var duration, intervalDuration, backwardDuration, forwardDuration, seekable;
backwardDuration = function(playlist, endSequence) {
var result = 0, segment, i;
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) {
if (segment.start !== undefined) {
return { result: segment.start, precise: true };
}
if (segment.end !== undefined) {
return {
result: segment.end - segment.duration,
precise: true
};
}
}
while (i--) {
segment = playlist.segments[i];
if (segment.end !== undefined) {
return { result: result + segment.end, precise: true };
}
result += segment.duration;
if (segment.start !== undefined) {
return { result: result + segment.start, precise: true };
}
}
return { result: result, precise: false };
};
// Math.max that will return the alternative input if one of its
// parameters in undefined
optionalMax = function(left, right) {
left = isFinite(left) ? left: -Infinity;
right = isFinite(right) ? right: -Infinity;
return Math.max(left, right);
forwardDuration = function(playlist, endSequence) {
var result = 0, segment, i;
i = endSequence - playlist.mediaSequence;
// Walk forward until we find the earliest segment with timeline
// information
for (; i < playlist.segments.length; i++) {
segment = playlist.segments[i];
if (segment.start !== undefined) {
return {
result: segment.start - result,
precise: true
};
}
result += segment.duration;
if (segment.end !== undefined) {
return {
result: segment.end - result,
precise: true
};
}
}
// indicate we didn't find a useful duration estimate
return { result: -1, precise: false };
};
/**
......@@ -31,42 +78,40 @@
* @param playlist {object} a media playlist object
* @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.
* @return {number} the duration between the first available segment
* and end index.
*/
intervalDuration = function(playlist, endSequence) {
var result = 0, segment, targetDuration, i;
var backward, forward;
if (endSequence === undefined) {
endSequence = playlist.mediaSequence + (playlist.segments || []).length;
endSequence = playlist.mediaSequence + playlist.segments.length;
}
if (endSequence < 0) {
if (endSequence < playlist.mediaSequence) {
return 0;
}
targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
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;
// do a backward walk to estimate the duration
backward = backwardDuration(playlist, endSequence);
if (backward.precise) {
// if we were able to base our duration estimate on timing
// information provided directly from the Media Source, return
// it
return backward.result;
}
while (i--) {
segment = playlist.segments[i];
if (segment.end !== undefined) {
return result + segment.end;
}
result += (segment.duration || targetDuration);
if (segment.start !== undefined) {
return result + segment.start;
}
// walk forward to see if a precise duration estimate can be made
// that way
forward = forwardDuration(playlist, endSequence);
if (forward.precise) {
// we found a segment that has been buffered and so it's
// position is known precisely
return forward.result;
}
return result;
// return the less-precise, playlist-based duration estimate
return backward.result;
};
/**
......
......@@ -93,15 +93,50 @@
equal(duration, 50.0002, 'calculated with mixed intervals');
});
test('uses timeline values for the expired duration of live playlists', function() {
var playlist = {
mediaSequence: 12,
segments: [{
duration: 10,
end: 120.5,
uri: '0.ts'
}, {
duration: 9,
uri: '1.ts'
}]
}, duration;
duration = Playlist.duration(playlist, playlist.mediaSequence);
equal(duration, 110.5, 'used segment end time');
duration = Playlist.duration(playlist, playlist.mediaSequence + 1);
equal(duration, 120.5, 'used segment end time');
duration = Playlist.duration(playlist, playlist.mediaSequence + 2);
equal(duration, 120.5 + 9, 'used segment end time');
});
test('looks outside the queried interval for live playlist timeline values', function() {
var playlist = {
mediaSequence: 12,
segments: [{
duration: 10,
uri: '0.ts'
}, {
duration: 9,
end: 120.5,
uri: '1.ts'
}]
}, duration;
duration = Playlist.duration(playlist, playlist.mediaSequence);
equal(duration, 120.5 - 9 - 10, 'used segment end time');
});
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,
duration: 10,
uri: '0.ts'
}, {
discontinuity: true,
......@@ -200,17 +235,10 @@
endList: true,
discontinuityStarts: [1],
segments: [{
minVideoPts: 0,
minAudioPts: 0,
maxVideoPts: 1 * 10 * 1000,
maxAudioPts: 1 * 10 * 1000,
duration: 10,
uri: '0.ts'
}, {
discontinuity: true,
minVideoPts: 2 * 10 * 1000,
minAudioPts: 2 * 10 * 1000,
maxVideoPts: 3 * 10 * 1000,
maxAudioPts: 3 * 10 * 1000,
duration: 10,
uri: '1.ts'
}]
......@@ -219,54 +247,6 @@
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'
}]
}, 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'
}]
}, 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,
......@@ -341,16 +321,19 @@
test('only considers available segments', function() {
var seekable = Playlist.seekable({
targetDuration: 10,
mediaSequence: 7,
segments: [{
uri: '8.ts'
uri: '8.ts',
duration: 10
}, {
uri: '9.ts'
uri: '9.ts',
duration: 10
}, {
uri: '10.ts'
uri: '10.ts',
duration: 10
}, {
uri: '11.ts'
uri: '11.ts',
duration: 10
}]
});
equal(seekable.length, 1, 'there are seekable ranges');
......