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.
Showing
3 changed files
with
143 additions
and
108 deletions
... | @@ -389,7 +389,10 @@ | ... | @@ -389,7 +389,10 @@ |
389 | return 0; | 389 | return 0; |
390 | } | 390 | } |
391 | 391 | ||
392 | // 1) Walk backward until we find the first segment with timeline | 392 | // find segments with known timing information that bound the |
393 | // target time | ||
394 | |||
395 | // Walk backward until we find the first segment with timeline | ||
393 | // information that is earlier than `time` | 396 | // information that is earlier than `time` |
394 | for (i = lastSegment; i >= 0; i--) { | 397 | for (i = lastSegment; i >= 0; i--) { |
395 | segment = this.media_.segments[i]; | 398 | segment = this.media_.segments[i]; |
... | @@ -414,7 +417,7 @@ | ... | @@ -414,7 +417,7 @@ |
414 | } | 417 | } |
415 | } | 418 | } |
416 | 419 | ||
417 | // 2) Walk forward until we find the first segment with timeline | 420 | // Walk forward until we find the first segment with timeline |
418 | // information that is greater than `time` | 421 | // information that is greater than `time` |
419 | for (i = 0; i < numSegments; i++) { | 422 | for (i = 0; i < numSegments; i++) { |
420 | segment = this.media_.segments[i]; | 423 | segment = this.media_.segments[i]; |
... | @@ -423,7 +426,8 @@ | ... | @@ -423,7 +426,8 @@ |
423 | knownEnd = segment.start; | 426 | knownEnd = segment.start; |
424 | if (endIndex < 0) { | 427 | if (endIndex < 0) { |
425 | // The first segment claims to start *after* the time we are | 428 | // The first segment claims to start *after* the time we are |
426 | // searching for so just return it | 429 | // searching for so the target segment must no longer be |
430 | // available | ||
427 | return -1; | 431 | return -1; |
428 | } | 432 | } |
429 | break; | 433 | break; |
... | @@ -435,6 +439,9 @@ | ... | @@ -435,6 +439,9 @@ |
435 | } | 439 | } |
436 | } | 440 | } |
437 | 441 | ||
442 | // use the bounds we just found and playlist information to | ||
443 | // estimate the segment that contains the time we are looking for | ||
444 | |||
438 | if (startIndex !== undefined) { | 445 | if (startIndex !== undefined) { |
439 | // We have a known-start point that is before our desired time so | 446 | // We have a known-start point that is before our desired time so |
440 | // walk from that point forwards | 447 | // walk from that point forwards | ... | ... |
... | @@ -4,23 +4,70 @@ | ... | @@ -4,23 +4,70 @@ |
4 | (function(window, videojs) { | 4 | (function(window, videojs) { |
5 | 'use strict'; | 5 | 'use strict'; |
6 | 6 | ||
7 | var DEFAULT_TARGET_DURATION = 10; | 7 | var duration, intervalDuration, backwardDuration, forwardDuration, seekable; |
8 | var duration, intervalDuration, optionalMin, optionalMax, seekable; | 8 | |
9 | 9 | backwardDuration = function(playlist, endSequence) { | |
10 | // Math.min that will return the alternative input if one of its | 10 | var result = 0, segment, i; |
11 | // parameters in undefined | 11 | |
12 | optionalMin = function(left, right) { | 12 | i = endSequence - playlist.mediaSequence; |
13 | left = isFinite(left) ? left : Infinity; | 13 | // if a start time is available for segment immediately following |
14 | right = isFinite(right) ? right : Infinity; | 14 | // the interval, use it |
15 | return Math.min(left, right); | 15 | segment = playlist.segments[i]; |
16 | // Walk backward until we find the latest segment with timeline | ||
17 | // information that is earlier than endSequence | ||
18 | if (segment) { | ||
19 | if (segment.start !== undefined) { | ||
20 | return { result: segment.start, precise: true }; | ||
21 | } | ||
22 | if (segment.end !== undefined) { | ||
23 | return { | ||
24 | result: segment.end - segment.duration, | ||
25 | precise: true | ||
16 | }; | 26 | }; |
27 | } | ||
28 | } | ||
29 | while (i--) { | ||
30 | segment = playlist.segments[i]; | ||
31 | if (segment.end !== undefined) { | ||
32 | return { result: result + segment.end, precise: true }; | ||
33 | } | ||
34 | |||
35 | result += segment.duration; | ||
17 | 36 | ||
18 | // Math.max that will return the alternative input if one of its | 37 | if (segment.start !== undefined) { |
19 | // parameters in undefined | 38 | return { result: result + segment.start, precise: true }; |
20 | optionalMax = function(left, right) { | 39 | } |
21 | left = isFinite(left) ? left: -Infinity; | 40 | } |
22 | right = isFinite(right) ? right: -Infinity; | 41 | return { result: result, precise: false }; |
23 | return Math.max(left, right); | 42 | }; |
43 | |||
44 | forwardDuration = function(playlist, endSequence) { | ||
45 | var result = 0, segment, i; | ||
46 | |||
47 | i = endSequence - playlist.mediaSequence; | ||
48 | // Walk forward until we find the earliest segment with timeline | ||
49 | // information | ||
50 | for (; i < playlist.segments.length; i++) { | ||
51 | segment = playlist.segments[i]; | ||
52 | if (segment.start !== undefined) { | ||
53 | return { | ||
54 | result: segment.start - result, | ||
55 | precise: true | ||
56 | }; | ||
57 | } | ||
58 | |||
59 | result += segment.duration; | ||
60 | |||
61 | if (segment.end !== undefined) { | ||
62 | return { | ||
63 | result: segment.end - result, | ||
64 | precise: true | ||
65 | }; | ||
66 | } | ||
67 | |||
68 | } | ||
69 | // indicate we didn't find a useful duration estimate | ||
70 | return { result: -1, precise: false }; | ||
24 | }; | 71 | }; |
25 | 72 | ||
26 | /** | 73 | /** |
... | @@ -31,42 +78,40 @@ | ... | @@ -31,42 +78,40 @@ |
31 | * @param playlist {object} a media playlist object | 78 | * @param playlist {object} a media playlist object |
32 | * @param endSequence {number} (optional) an exclusive upper boundary | 79 | * @param endSequence {number} (optional) an exclusive upper boundary |
33 | * for the playlist. Defaults to playlist length. | 80 | * for the playlist. Defaults to playlist length. |
34 | * @return {number} the duration between the start index and end | 81 | * @return {number} the duration between the first available segment |
35 | * index. | 82 | * and end index. |
36 | */ | 83 | */ |
37 | intervalDuration = function(playlist, endSequence) { | 84 | intervalDuration = function(playlist, endSequence) { |
38 | var result = 0, segment, targetDuration, i; | 85 | var backward, forward; |
39 | 86 | ||
40 | if (endSequence === undefined) { | 87 | if (endSequence === undefined) { |
41 | endSequence = playlist.mediaSequence + (playlist.segments || []).length; | 88 | endSequence = playlist.mediaSequence + playlist.segments.length; |
42 | } | 89 | } |
43 | if (endSequence < 0) { | 90 | |
91 | if (endSequence < playlist.mediaSequence) { | ||
44 | return 0; | 92 | return 0; |
45 | } | 93 | } |
46 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | ||
47 | 94 | ||
48 | i = endSequence - playlist.mediaSequence; | 95 | // do a backward walk to estimate the duration |
49 | // if a start time is available for segment immediately following | 96 | backward = backwardDuration(playlist, endSequence); |
50 | // the interval, use it | 97 | if (backward.precise) { |
51 | segment = playlist.segments[i]; | 98 | // if we were able to base our duration estimate on timing |
52 | // Walk backward until we find the latest segment with timeline | 99 | // information provided directly from the Media Source, return |
53 | // information that is earlier than endSequence | 100 | // it |
54 | if (segment && segment.start !== undefined) { | 101 | return backward.result; |
55 | return segment.start; | ||
56 | } | 102 | } |
57 | while (i--) { | ||
58 | segment = playlist.segments[i]; | ||
59 | if (segment.end !== undefined) { | ||
60 | return result + segment.end; | ||
61 | } | ||
62 | |||
63 | result += (segment.duration || targetDuration); | ||
64 | 103 | ||
65 | if (segment.start !== undefined) { | 104 | // walk forward to see if a precise duration estimate can be made |
66 | return result + segment.start; | 105 | // that way |
67 | } | 106 | forward = forwardDuration(playlist, endSequence); |
107 | if (forward.precise) { | ||
108 | // we found a segment that has been buffered and so it's | ||
109 | // position is known precisely | ||
110 | return forward.result; | ||
68 | } | 111 | } |
69 | return result; | 112 | |
113 | // return the less-precise, playlist-based duration estimate | ||
114 | return backward.result; | ||
70 | }; | 115 | }; |
71 | 116 | ||
72 | /** | 117 | /** | ... | ... |
... | @@ -93,15 +93,50 @@ | ... | @@ -93,15 +93,50 @@ |
93 | equal(duration, 50.0002, 'calculated with mixed intervals'); | 93 | equal(duration, 50.0002, 'calculated with mixed intervals'); |
94 | }); | 94 | }); |
95 | 95 | ||
96 | test('uses timeline values for the expired duration of live playlists', function() { | ||
97 | var playlist = { | ||
98 | mediaSequence: 12, | ||
99 | segments: [{ | ||
100 | duration: 10, | ||
101 | end: 120.5, | ||
102 | uri: '0.ts' | ||
103 | }, { | ||
104 | duration: 9, | ||
105 | uri: '1.ts' | ||
106 | }] | ||
107 | }, duration; | ||
108 | |||
109 | duration = Playlist.duration(playlist, playlist.mediaSequence); | ||
110 | equal(duration, 110.5, 'used segment end time'); | ||
111 | duration = Playlist.duration(playlist, playlist.mediaSequence + 1); | ||
112 | equal(duration, 120.5, 'used segment end time'); | ||
113 | duration = Playlist.duration(playlist, playlist.mediaSequence + 2); | ||
114 | equal(duration, 120.5 + 9, 'used segment end time'); | ||
115 | }); | ||
116 | |||
117 | test('looks outside the queried interval for live playlist timeline values', function() { | ||
118 | var playlist = { | ||
119 | mediaSequence: 12, | ||
120 | segments: [{ | ||
121 | duration: 10, | ||
122 | uri: '0.ts' | ||
123 | }, { | ||
124 | duration: 9, | ||
125 | end: 120.5, | ||
126 | uri: '1.ts' | ||
127 | }] | ||
128 | }, duration; | ||
129 | |||
130 | duration = Playlist.duration(playlist, playlist.mediaSequence); | ||
131 | equal(duration, 120.5 - 9 - 10, 'used segment end time'); | ||
132 | }); | ||
133 | |||
96 | test('ignores discontinuity sequences later than the end', function() { | 134 | test('ignores discontinuity sequences later than the end', function() { |
97 | var duration = Playlist.duration({ | 135 | var duration = Playlist.duration({ |
98 | mediaSequence: 0, | 136 | mediaSequence: 0, |
99 | discontinuityStarts: [1, 3], | 137 | discontinuityStarts: [1, 3], |
100 | segments: [{ | 138 | segments: [{ |
101 | minVideoPts: 0, | 139 | duration: 10, |
102 | minAudioPts: 0, | ||
103 | maxVideoPts: 10 * 1000, | ||
104 | maxAudioPts: 10 * 1000, | ||
105 | uri: '0.ts' | 140 | uri: '0.ts' |
106 | }, { | 141 | }, { |
107 | discontinuity: true, | 142 | discontinuity: true, |
... | @@ -200,17 +235,10 @@ | ... | @@ -200,17 +235,10 @@ |
200 | endList: true, | 235 | endList: true, |
201 | discontinuityStarts: [1], | 236 | discontinuityStarts: [1], |
202 | segments: [{ | 237 | segments: [{ |
203 | minVideoPts: 0, | 238 | duration: 10, |
204 | minAudioPts: 0, | ||
205 | maxVideoPts: 1 * 10 * 1000, | ||
206 | maxAudioPts: 1 * 10 * 1000, | ||
207 | uri: '0.ts' | 239 | uri: '0.ts' |
208 | }, { | 240 | }, { |
209 | discontinuity: true, | 241 | discontinuity: true, |
210 | minVideoPts: 2 * 10 * 1000, | ||
211 | minAudioPts: 2 * 10 * 1000, | ||
212 | maxVideoPts: 3 * 10 * 1000, | ||
213 | maxAudioPts: 3 * 10 * 1000, | ||
214 | duration: 10, | 242 | duration: 10, |
215 | uri: '1.ts' | 243 | uri: '1.ts' |
216 | }] | 244 | }] |
... | @@ -219,54 +247,6 @@ | ... | @@ -219,54 +247,6 @@ |
219 | equal(duration, 10 + 10, 'handles discontinuities'); | 247 | equal(duration, 10 + 10, 'handles discontinuities'); |
220 | }); | 248 | }); |
221 | 249 | ||
222 | test('does not count ending segment gaps across a discontinuity', function() { | ||
223 | var duration = Playlist.duration({ | ||
224 | mediaSequence: 0, | ||
225 | discontinuityStarts: [1], | ||
226 | endList: true, | ||
227 | segments: [{ | ||
228 | minVideoPts: 0, | ||
229 | minAudioPts: 0, | ||
230 | maxVideoPts: 1 * 10 * 1000, | ||
231 | maxAudioPts: 1 * 10 * 1000, | ||
232 | uri: '0.ts' | ||
233 | }, { | ||
234 | discontinuity: true, | ||
235 | minVideoPts: 1 * 10 * 1000 + 100, | ||
236 | minAudioPts: 1 * 10 * 1000 + 100, | ||
237 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
238 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
239 | duration: 10, | ||
240 | uri: '1.ts' | ||
241 | }] | ||
242 | }, 1); | ||
243 | |||
244 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | ||
245 | }); | ||
246 | |||
247 | test('trailing duration on the final segment can be excluded', function() { | ||
248 | var duration = Playlist.duration({ | ||
249 | mediaSequence: 0, | ||
250 | endList: true, | ||
251 | segments: [{ | ||
252 | minVideoPts: 0, | ||
253 | minAudioPts: 0, | ||
254 | maxVideoPts: 1 * 10 * 1000, | ||
255 | maxAudioPts: 1 * 10 * 1000, | ||
256 | uri: '0.ts' | ||
257 | }, { | ||
258 | minVideoPts: 1 * 10 * 1000 + 100, | ||
259 | minAudioPts: 1 * 10 * 1000 + 100, | ||
260 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
261 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
262 | duration: 10, | ||
263 | uri: '1.ts' | ||
264 | }] | ||
265 | }, 1, false); | ||
266 | |||
267 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | ||
268 | }); | ||
269 | |||
270 | test('a non-positive length interval has zero duration', function() { | 250 | test('a non-positive length interval has zero duration', function() { |
271 | var playlist = { | 251 | var playlist = { |
272 | mediaSequence: 0, | 252 | mediaSequence: 0, |
... | @@ -341,16 +321,19 @@ | ... | @@ -341,16 +321,19 @@ |
341 | 321 | ||
342 | test('only considers available segments', function() { | 322 | test('only considers available segments', function() { |
343 | var seekable = Playlist.seekable({ | 323 | var seekable = Playlist.seekable({ |
344 | targetDuration: 10, | ||
345 | mediaSequence: 7, | 324 | mediaSequence: 7, |
346 | segments: [{ | 325 | segments: [{ |
347 | uri: '8.ts' | 326 | uri: '8.ts', |
327 | duration: 10 | ||
348 | }, { | 328 | }, { |
349 | uri: '9.ts' | 329 | uri: '9.ts', |
330 | duration: 10 | ||
350 | }, { | 331 | }, { |
351 | uri: '10.ts' | 332 | uri: '10.ts', |
333 | duration: 10 | ||
352 | }, { | 334 | }, { |
353 | uri: '11.ts' | 335 | uri: '11.ts', |
336 | duration: 10 | ||
354 | }] | 337 | }] |
355 | }); | 338 | }); |
356 | equal(seekable.length, 1, 'there are seekable ranges'); | 339 | equal(seekable.length, 1, 'there are seekable ranges'); | ... | ... |
-
Please register or sign in to post a comment