Merge pull request #411 from dmlap/next-segment-calculation
Determine the segment to load by looking at buffered
Showing
7 changed files
with
150 additions
and
418 deletions
1 | (function(window) { | 1 | (function(window) { |
2 | var textRange = function(range, i) { | ||
3 | return range.start(i) + '-' + range.end(i); | ||
4 | }; | ||
2 | var module = { | 5 | var module = { |
3 | hexDump: function(data) { | 6 | hexDump: function(data) { |
4 | var | 7 | var |
... | @@ -26,6 +29,13 @@ | ... | @@ -26,6 +29,13 @@ |
26 | }, | 29 | }, |
27 | tagDump: function(tag) { | 30 | tagDump: function(tag) { |
28 | return module.hexDump(tag.bytes); | 31 | return module.hexDump(tag.bytes); |
32 | }, | ||
33 | textRanges: function(ranges) { | ||
34 | var result = '', i; | ||
35 | for (i = 0; i < ranges.length; i++) { | ||
36 | result += textRange(ranges, i) + ' '; | ||
37 | } | ||
38 | return result; | ||
29 | } | 39 | } |
30 | }; | 40 | }; |
31 | 41 | ... | ... |
... | @@ -2,13 +2,7 @@ | ... | @@ -2,13 +2,7 @@ |
2 | * playlist-loader | 2 | * playlist-loader |
3 | * | 3 | * |
4 | * A state machine that manages the loading, caching, and updating of | 4 | * A state machine that manages the loading, caching, and updating of |
5 | * M3U8 playlists. When tracking a live playlist, loaders will keep | 5 | * M3U8 playlists. |
6 | * track of the duration of content that expired since the loader was | ||
7 | * initialized and when the current discontinuity sequence was | ||
8 | * encountered. A complete media timeline for a live playlist with | ||
9 | * expiring segments looks like this: | ||
10 | * | ||
11 | * |-- expired --|-- segments --| | ||
12 | * | 6 | * |
13 | */ | 7 | */ |
14 | (function(window, videojs) { | 8 | (function(window, videojs) { |
... | @@ -16,7 +10,6 @@ | ... | @@ -16,7 +10,6 @@ |
16 | var | 10 | var |
17 | resolveUrl = videojs.Hls.resolveUrl, | 11 | resolveUrl = videojs.Hls.resolveUrl, |
18 | xhr = videojs.Hls.xhr, | 12 | xhr = videojs.Hls.xhr, |
19 | Playlist = videojs.Hls.Playlist, | ||
20 | mergeOptions = videojs.mergeOptions, | 13 | mergeOptions = videojs.mergeOptions, |
21 | 14 | ||
22 | /** | 15 | /** |
... | @@ -158,14 +151,6 @@ | ... | @@ -158,14 +151,6 @@ |
158 | // initialize the loader state | 151 | // initialize the loader state |
159 | loader.state = 'HAVE_NOTHING'; | 152 | loader.state = 'HAVE_NOTHING'; |
160 | 153 | ||
161 | // The total duration of all segments that expired and have been | ||
162 | // removed from the current playlist, in seconds. This property | ||
163 | // should always be zero for non-live playlists. In a live | ||
164 | // playlist, this is the total amount of time that has been | ||
165 | // removed from the stream since the playlist loader began | ||
166 | // tracking it. | ||
167 | loader.expired_ = 0; | ||
168 | |||
169 | // capture the prototype dispose function | 154 | // capture the prototype dispose function |
170 | dispose = this.dispose; | 155 | dispose = this.dispose; |
171 | 156 | ||
... | @@ -187,20 +172,20 @@ | ... | @@ -187,20 +172,20 @@ |
187 | * active media playlist. When called with a single argument, | 172 | * active media playlist. When called with a single argument, |
188 | * triggers the playlist loader to asynchronously switch to the | 173 | * triggers the playlist loader to asynchronously switch to the |
189 | * specified media playlist. Calling this method while the | 174 | * specified media playlist. Calling this method while the |
190 | * loader is in the HAVE_NOTHING or HAVE_MASTER states causes an | 175 | * loader is in the HAVE_NOTHING causes an error to be emitted |
191 | * error to be emitted but otherwise has no effect. | 176 | * but otherwise has no effect. |
192 | * @param playlist (optional) {object} the parsed media playlist | 177 | * @param playlist (optional) {object} the parsed media playlist |
193 | * object to switch to | 178 | * object to switch to |
194 | */ | 179 | */ |
195 | loader.media = function(playlist) { | 180 | loader.media = function(playlist) { |
196 | var mediaChange = false; | 181 | var startingState = loader.state, mediaChange; |
197 | // getter | 182 | // getter |
198 | if (!playlist) { | 183 | if (!playlist) { |
199 | return loader.media_; | 184 | return loader.media_; |
200 | } | 185 | } |
201 | 186 | ||
202 | // setter | 187 | // setter |
203 | if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') { | 188 | if (loader.state === 'HAVE_NOTHING') { |
204 | throw new Error('Cannot switch media playlist from ' + loader.state); | 189 | throw new Error('Cannot switch media playlist from ' + loader.state); |
205 | } | 190 | } |
206 | 191 | ||
... | @@ -213,7 +198,7 @@ | ... | @@ -213,7 +198,7 @@ |
213 | playlist = loader.master.playlists[playlist]; | 198 | playlist = loader.master.playlists[playlist]; |
214 | } | 199 | } |
215 | 200 | ||
216 | mediaChange = playlist.uri !== loader.media_.uri; | 201 | mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri; |
217 | 202 | ||
218 | // switch to fully loaded playlists immediately | 203 | // switch to fully loaded playlists immediately |
219 | if (loader.master.playlists[playlist.uri].endList) { | 204 | if (loader.master.playlists[playlist.uri].endList) { |
... | @@ -258,7 +243,17 @@ | ... | @@ -258,7 +243,17 @@ |
258 | withCredentials: withCredentials | 243 | withCredentials: withCredentials |
259 | }, function(error, request) { | 244 | }, function(error, request) { |
260 | haveMetadata(error, request, playlist.uri); | 245 | haveMetadata(error, request, playlist.uri); |
246 | |||
247 | if (error) { | ||
248 | return; | ||
249 | } | ||
250 | |||
251 | // fire loadedmetadata the first time a media playlist is loaded | ||
252 | if (startingState === 'HAVE_MASTER') { | ||
253 | loader.trigger('loadedmetadata'); | ||
254 | } else { | ||
261 | loader.trigger('mediachange'); | 255 | loader.trigger('mediachange'); |
256 | } | ||
262 | }); | 257 | }); |
263 | }; | 258 | }; |
264 | 259 | ||
... | @@ -320,19 +315,13 @@ | ... | @@ -320,19 +315,13 @@ |
320 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; | 315 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; |
321 | } | 316 | } |
322 | 317 | ||
323 | request = xhr({ | 318 | loader.trigger('loadedplaylist'); |
324 | uri: resolveUrl(srcUrl, parser.manifest.playlists[0].uri), | 319 | if (!request) { |
325 | withCredentials: withCredentials | 320 | // no media playlist was specifically selected so start |
326 | }, function(error, request) { | 321 | // from the first listed one |
327 | // pass along the URL specified in the master playlist | 322 | loader.media(parser.manifest.playlists[0]); |
328 | haveMetadata(error, | ||
329 | request, | ||
330 | parser.manifest.playlists[0].uri); | ||
331 | if (!error) { | ||
332 | loader.trigger('loadedmetadata'); | ||
333 | } | 323 | } |
334 | }); | 324 | return; |
335 | return loader.trigger('loadedplaylist'); | ||
336 | } | 325 | } |
337 | 326 | ||
338 | // loaded a media playlist | 327 | // loaded a media playlist |
... | @@ -356,43 +345,10 @@ | ... | @@ -356,43 +345,10 @@ |
356 | * @param update {object} the updated media playlist object | 345 | * @param update {object} the updated media playlist object |
357 | */ | 346 | */ |
358 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { | 347 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { |
359 | var expiredCount; | ||
360 | |||
361 | if (this.media_) { | ||
362 | expiredCount = update.mediaSequence - this.media_.mediaSequence; | ||
363 | |||
364 | // update the expired time count | ||
365 | this.expired_ += Playlist.duration(this.media_, | ||
366 | this.media_.mediaSequence, | ||
367 | update.mediaSequence); | ||
368 | } | ||
369 | |||
370 | this.media_ = this.master.playlists[update.uri]; | 348 | this.media_ = this.master.playlists[update.uri]; |
371 | }; | 349 | }; |
372 | 350 | ||
373 | /** | 351 | /** |
374 | * When switching variant playlists in a live stream, the player may | ||
375 | * discover that the new set of available segments is shifted in | ||
376 | * time relative to the old playlist. If that is the case, you can | ||
377 | * call this method to synchronize the playlist loader so that | ||
378 | * subsequent calls to getMediaIndexForTime_() return values | ||
379 | * appropriate for the new playlist. | ||
380 | * | ||
381 | * @param mediaIndex {integer} the index of the segment that will be | ||
382 | * the used to base timeline calculations on | ||
383 | * @param startTime {number} the media timeline position of the | ||
384 | * first moment of video data for the specified segment. That is, | ||
385 | * data from the specified segment will first be displayed when | ||
386 | * `currentTime` is equal to `startTime`. | ||
387 | */ | ||
388 | PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) { | ||
389 | var segmentOffset = Playlist.duration(this.media_, | ||
390 | this.media_.mediaSequence, | ||
391 | this.media_.mediaSequence + mediaIndex); | ||
392 | this.expired_ = startingTime - segmentOffset; | ||
393 | }; | ||
394 | |||
395 | /** | ||
396 | * Determine the index of the segment that contains a specified | 352 | * Determine the index of the segment that contains a specified |
397 | * playback position in the current media playlist. Early versions | 353 | * playback position in the current media playlist. Early versions |
398 | * of the HLS specification require segment durations to be rounded | 354 | * of the HLS specification require segment durations to be rounded |
... | @@ -411,7 +367,7 @@ | ... | @@ -411,7 +367,7 @@ |
411 | * closest playback position that is currently available. | 367 | * closest playback position that is currently available. |
412 | */ | 368 | */ |
413 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { | 369 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { |
414 | var i; | 370 | var i, j, segment, targetDuration; |
415 | 371 | ||
416 | if (!this.media_) { | 372 | if (!this.media_) { |
417 | return 0; | 373 | return 0; |
... | @@ -419,28 +375,61 @@ | ... | @@ -419,28 +375,61 @@ |
419 | 375 | ||
420 | // when the requested position is earlier than the current set of | 376 | // when the requested position is earlier than the current set of |
421 | // segments, return the earliest segment index | 377 | // segments, return the earliest segment index |
422 | time -= this.expired_; | ||
423 | if (time < 0) { | 378 | if (time < 0) { |
424 | return 0; | 379 | return 0; |
425 | } | 380 | } |
426 | 381 | ||
427 | for (i = 0; i < this.media_.segments.length; i++) { | 382 | // 1) Walk backward until we find the latest segment with timeline |
428 | time -= Playlist.duration(this.media_, | 383 | // information that is earlier than `time` |
429 | this.media_.mediaSequence + i, | 384 | targetDuration = this.media_.targetDuration || 10; |
430 | this.media_.mediaSequence + i + 1, | 385 | i = this.media_.segments.length; |
431 | false); | 386 | while (i--) { |
387 | segment = this.media_.segments[i]; | ||
388 | if (segment.end !== undefined && segment.end <= time) { | ||
389 | time -= segment.end; | ||
390 | break; | ||
391 | } | ||
392 | if (segment.start !== undefined && segment.start < time) { | ||
393 | |||
394 | if (segment.end !== undefined && segment.end > time) { | ||
395 | // we've found the target segment exactly | ||
396 | return i; | ||
397 | } | ||
432 | 398 | ||
433 | // HLS version 3 and lower round segment durations to the | 399 | time -= segment.start; |
434 | // nearest decimal integer. When the correct media index is | 400 | time -= segment.duration || targetDuration; |
435 | // ambiguous, prefer the higher one. | 401 | if (time < 0) { |
436 | if (time <= 0) { | 402 | // the segment with start information is also our best guess |
403 | // for the momment | ||
437 | return i; | 404 | return i; |
438 | } | 405 | } |
406 | break; | ||
407 | } | ||
408 | } | ||
409 | i++; | ||
410 | |||
411 | // 2) Walk forward, testing each segment to see if `time` falls within it | ||
412 | for (j = i; j < this.media_.segments.length; j++) { | ||
413 | segment = this.media_.segments[j]; | ||
414 | time -= segment.duration || targetDuration; | ||
415 | |||
416 | if (time < 0) { | ||
417 | return j; | ||
418 | } | ||
419 | |||
420 | // 2a) If we discover a segment that has timeline information | ||
421 | // before finding the result segment, the playlist information | ||
422 | // must have been inaccurate. Start a binary search for the | ||
423 | // segment which contains `time`. If the guess turns out to be | ||
424 | // incorrect, we'll have more info to work with next time. | ||
425 | if (segment.start !== undefined || segment.end !== undefined) { | ||
426 | return Math.floor((j - i) * 0.5); | ||
427 | } | ||
439 | } | 428 | } |
440 | 429 | ||
441 | // the playback position is outside the range of available | 430 | // the playback position is outside the range of available |
442 | // segments so return the last one | 431 | // segments so return the length |
443 | return this.media_.segments.length - 1; | 432 | return this.media_.segments.length; |
444 | }; | 433 | }; |
445 | 434 | ||
446 | videojs.Hls.PlaylistLoader = PlaylistLoader; | 435 | videojs.Hls.PlaylistLoader = PlaylistLoader; | ... | ... |
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
... | @@ -53,15 +53,6 @@ | ... | @@ -53,15 +53,6 @@ |
53 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | 53 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); |
54 | }); | 54 | }); |
55 | 55 | ||
56 | test('starts with no expired time', function() { | ||
57 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
58 | requests.pop().respond(200, null, | ||
59 | '#EXTM3U\n' + | ||
60 | '#EXTINF:10,\n' + | ||
61 | '0.ts\n'); | ||
62 | equal(loader.expired_, 0, 'zero seconds expired'); | ||
63 | }); | ||
64 | |||
65 | test('requests the initial playlist immediately', function() { | 56 | test('requests the initial playlist immediately', function() { |
66 | new videojs.Hls.PlaylistLoader('master.m3u8'); | 57 | new videojs.Hls.PlaylistLoader('master.m3u8'); |
67 | strictEqual(requests.length, 1, 'made a request'); | 58 | strictEqual(requests.length, 1, 'made a request'); |
... | @@ -69,13 +60,16 @@ | ... | @@ -69,13 +60,16 @@ |
69 | }); | 60 | }); |
70 | 61 | ||
71 | test('moves to HAVE_MASTER after loading a master playlist', function() { | 62 | test('moves to HAVE_MASTER after loading a master playlist', function() { |
72 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 63 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'), state; |
64 | loader.on('loadedplaylist', function() { | ||
65 | state = loader.state; | ||
66 | }); | ||
73 | requests.pop().respond(200, null, | 67 | requests.pop().respond(200, null, |
74 | '#EXTM3U\n' + | 68 | '#EXTM3U\n' + |
75 | '#EXT-X-STREAM-INF:\n' + | 69 | '#EXT-X-STREAM-INF:\n' + |
76 | 'media.m3u8\n'); | 70 | 'media.m3u8\n'); |
77 | ok(loader.master, 'the master playlist is available'); | 71 | ok(loader.master, 'the master playlist is available'); |
78 | strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct'); | 72 | strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); |
79 | }); | 73 | }); |
80 | 74 | ||
81 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | 75 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { |
... | @@ -172,101 +166,6 @@ | ... | @@ -172,101 +166,6 @@ |
172 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 166 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
173 | }); | 167 | }); |
174 | 168 | ||
175 | test('increments expired seconds after a segment is removed', function() { | ||
176 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
177 | requests.pop().respond(200, null, | ||
178 | '#EXTM3U\n' + | ||
179 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
180 | '#EXTINF:10,\n' + | ||
181 | '0.ts\n' + | ||
182 | '#EXTINF:10,\n' + | ||
183 | '1.ts\n' + | ||
184 | '#EXTINF:10,\n' + | ||
185 | '2.ts\n' + | ||
186 | '#EXTINF:10,\n' + | ||
187 | '3.ts\n'); | ||
188 | clock.tick(10 * 1000); // 10s, one target duration | ||
189 | requests.pop().respond(200, null, | ||
190 | '#EXTM3U\n' + | ||
191 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
192 | '#EXTINF:10,\n' + | ||
193 | '1.ts\n' + | ||
194 | '#EXTINF:10,\n' + | ||
195 | '2.ts\n' + | ||
196 | '#EXTINF:10,\n' + | ||
197 | '3.ts\n' + | ||
198 | '#EXTINF:10,\n' + | ||
199 | '4.ts\n'); | ||
200 | equal(loader.expired_, 10, 'expired one segment'); | ||
201 | }); | ||
202 | |||
203 | test('increments expired seconds after a discontinuity', function() { | ||
204 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
205 | requests.pop().respond(200, null, | ||
206 | '#EXTM3U\n' + | ||
207 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
208 | '#EXTINF:10,\n' + | ||
209 | '0.ts\n' + | ||
210 | '#EXTINF:3,\n' + | ||
211 | '1.ts\n' + | ||
212 | '#EXT-X-DISCONTINUITY\n' + | ||
213 | '#EXTINF:4,\n' + | ||
214 | '2.ts\n'); | ||
215 | clock.tick(10 * 1000); // 10s, one target duration | ||
216 | requests.pop().respond(200, null, | ||
217 | '#EXTM3U\n' + | ||
218 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
219 | '#EXTINF:3,\n' + | ||
220 | '1.ts\n' + | ||
221 | '#EXT-X-DISCONTINUITY\n' + | ||
222 | '#EXTINF:4,\n' + | ||
223 | '2.ts\n'); | ||
224 | equal(loader.expired_, 10, 'expired one segment'); | ||
225 | |||
226 | clock.tick(10 * 1000); // 10s, one target duration | ||
227 | requests.pop().respond(200, null, | ||
228 | '#EXTM3U\n' + | ||
229 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
230 | '#EXT-X-DISCONTINUITY\n' + | ||
231 | '#EXTINF:4,\n' + | ||
232 | '2.ts\n'); | ||
233 | equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | ||
234 | |||
235 | clock.tick(10 * 1000); // 10s, one target duration | ||
236 | requests.pop().respond(200, null, | ||
237 | '#EXTM3U\n' + | ||
238 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
239 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | ||
240 | '#EXTINF:10,\n' + | ||
241 | '3.ts\n'); | ||
242 | equal(loader.expired_, 13 + 4, 'tracked expired prior to the discontinuity'); | ||
243 | }); | ||
244 | |||
245 | test('tracks expired seconds properly when two discontinuities expire at once', function() { | ||
246 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
247 | requests.pop().respond(200, null, | ||
248 | '#EXTM3U\n' + | ||
249 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
250 | '#EXTINF:4,\n' + | ||
251 | '0.ts\n' + | ||
252 | '#EXT-X-DISCONTINUITY\n' + | ||
253 | '#EXTINF:5,\n' + | ||
254 | '1.ts\n' + | ||
255 | '#EXT-X-DISCONTINUITY\n' + | ||
256 | '#EXTINF:6,\n' + | ||
257 | '2.ts\n' + | ||
258 | '#EXTINF:7,\n' + | ||
259 | '3.ts\n'); | ||
260 | clock.tick(10 * 1000); | ||
261 | requests.pop().respond(200, null, | ||
262 | '#EXTM3U\n' + | ||
263 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
264 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | ||
265 | '#EXTINF:7,\n' + | ||
266 | '3.ts\n'); | ||
267 | equal(loader.expired_, 4 + 5 + 6, 'tracked both expired discontinuities'); | ||
268 | }); | ||
269 | |||
270 | test('emits an error when an initial playlist request fails', function() { | 169 | test('emits an error when an initial playlist request fails', function() { |
271 | var | 170 | var |
272 | errors = [], | 171 | errors = [], |
... | @@ -453,6 +352,20 @@ | ... | @@ -453,6 +352,20 @@ |
453 | 'updated the active media'); | 352 | 'updated the active media'); |
454 | }); | 353 | }); |
455 | 354 | ||
355 | test('can switch playlists immediately after the master is downloaded', function() { | ||
356 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
357 | loader.on('loadedplaylist', function() { | ||
358 | loader.media('high.m3u8'); | ||
359 | }); | ||
360 | requests.pop().respond(200, null, | ||
361 | '#EXTM3U\n' + | ||
362 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
363 | 'low.m3u8\n' + | ||
364 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
365 | 'high.m3u8\n'); | ||
366 | equal(requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); | ||
367 | }); | ||
368 | |||
456 | test('can switch media playlists based on URI', function() { | 369 | test('can switch media playlists based on URI', function() { |
457 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 370 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); |
458 | requests.pop().respond(200, null, | 371 | requests.pop().respond(200, null, |
... | @@ -624,9 +537,6 @@ | ... | @@ -624,9 +537,6 @@ |
624 | 'low.m3u8\n' + | 537 | 'low.m3u8\n' + |
625 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 538 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
626 | 'high.m3u8\n'); | 539 | 'high.m3u8\n'); |
627 | throws(function() { | ||
628 | loader.media('high.m3u8'); | ||
629 | }, 'throws an error from HAVE_MASTER'); | ||
630 | }); | 540 | }); |
631 | 541 | ||
632 | test('throws an error if a switch to an unrecognized playlist is requested', function() { | 542 | test('throws an error if a switch to an unrecognized playlist is requested', function() { |
... | @@ -743,8 +653,8 @@ | ... | @@ -743,8 +653,8 @@ |
743 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | 653 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); |
744 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | 654 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); |
745 | equal(loader.getMediaIndexForTime_(22), | 655 | equal(loader.getMediaIndexForTime_(22), |
746 | 2, | 656 | 3, |
747 | 'the index is never greater than the length'); | 657 | 'time greater than the length is index 3'); |
748 | }); | 658 | }); |
749 | 659 | ||
750 | test('returns the lower index when calculating for a segment boundary', function() { | 660 | test('returns the lower index when calculating for a segment boundary', function() { |
... | @@ -757,10 +667,8 @@ | ... | @@ -757,10 +667,8 @@ |
757 | '#EXTINF:5,\n' + | 667 | '#EXTINF:5,\n' + |
758 | '1.ts\n' + | 668 | '1.ts\n' + |
759 | '#EXT-X-ENDLIST\n'); | 669 | '#EXT-X-ENDLIST\n'); |
760 | equal(loader.getMediaIndexForTime_(4), 0, 'rounds down exact matches'); | 670 | equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); |
761 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | 671 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); |
762 | // FIXME: the test below should pass for HLSv3 | ||
763 | //equal(loader.getMediaIndexForTime_(4.2), 0, 'rounds down'); | ||
764 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | 672 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); |
765 | }); | 673 | }); |
766 | 674 | ||
... | @@ -773,7 +681,7 @@ | ... | @@ -773,7 +681,7 @@ |
773 | '1001.ts\n' + | 681 | '1001.ts\n' + |
774 | '#EXTINF:5,\n' + | 682 | '#EXTINF:5,\n' + |
775 | '1002.ts\n'); | 683 | '1002.ts\n'); |
776 | loader.expired_ = 150; | 684 | loader.media().segments[0].start = 150; |
777 | 685 | ||
778 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); | 686 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); |
779 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); | 687 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); |
... | @@ -785,30 +693,6 @@ | ... | @@ -785,30 +693,6 @@ |
785 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | 693 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); |
786 | }); | 694 | }); |
787 | 695 | ||
788 | test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() { | ||
789 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
790 | requests.pop().respond(200, null, | ||
791 | '#EXTM3U\n' + | ||
792 | '#EXT-X-MEDIA-SEQUENCE:23\n' + | ||
793 | '#EXTINF:4,\n' + | ||
794 | '23.ts\n' + | ||
795 | '#EXTINF:5,\n' + | ||
796 | '24.ts\n' + | ||
797 | '#EXTINF:6,\n' + | ||
798 | '25.ts\n' + | ||
799 | '#EXTINF:7,\n' + | ||
800 | '26.ts\n'); | ||
801 | loader.updateTimelineOffset(0, 150); | ||
802 | equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment'); | ||
803 | equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero'); | ||
804 | equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment'); | ||
805 | |||
806 | loader.updateTimelineOffset(2, 30); | ||
807 | equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment'); | ||
808 | equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment'); | ||
809 | equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment'); | ||
810 | }); | ||
811 | |||
812 | test('does not misintrepret playlists missing newlines at the end', function() { | 696 | test('does not misintrepret playlists missing newlines at the end', function() { |
813 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 697 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); |
814 | requests.shift().respond(200, null, | 698 | requests.shift().respond(200, null, | ... | ... |
... | @@ -18,27 +18,6 @@ | ... | @@ -18,27 +18,6 @@ |
18 | 18 | ||
19 | module('Playlist Interval Duration'); | 19 | module('Playlist Interval Duration'); |
20 | 20 | ||
21 | test('accounts expired duration for live playlists', function() { | ||
22 | var duration = Playlist.duration({ | ||
23 | mediaSequence: 10, | ||
24 | segments: [{ | ||
25 | duration: 10, | ||
26 | uri: '10.ts' | ||
27 | }, { | ||
28 | duration: 10, | ||
29 | uri: '11.ts' | ||
30 | }, { | ||
31 | duration: 10, | ||
32 | uri: '12.ts' | ||
33 | }, { | ||
34 | duration: 10, | ||
35 | uri: '13.ts' | ||
36 | }] | ||
37 | }, 0, 14); | ||
38 | |||
39 | equal(duration, 14 * 10, 'duration includes dropped segments'); | ||
40 | }); | ||
41 | |||
42 | test('accounts for non-zero starting VOD media sequences', function() { | 21 | test('accounts for non-zero starting VOD media sequences', function() { |
43 | var duration = Playlist.duration({ | 22 | var duration = Playlist.duration({ |
44 | mediaSequence: 10, | 23 | mediaSequence: 10, |
... | @@ -61,47 +40,37 @@ | ... | @@ -61,47 +40,37 @@ |
61 | equal(duration, 4 * 10, 'includes only listed segments'); | 40 | equal(duration, 4 * 10, 'includes only listed segments'); |
62 | }); | 41 | }); |
63 | 42 | ||
64 | test('uses PTS values when available', function() { | 43 | test('uses timeline values when available', function() { |
65 | var duration = Playlist.duration({ | 44 | var duration = Playlist.duration({ |
66 | mediaSequence: 0, | 45 | mediaSequence: 0, |
67 | endList: true, | 46 | endList: true, |
68 | segments: [{ | 47 | segments: [{ |
69 | minVideoPts: 1, | 48 | start: 0, |
70 | minAudioPts: 2, | ||
71 | uri: '0.ts' | 49 | uri: '0.ts' |
72 | }, { | 50 | }, { |
73 | duration: 10, | 51 | duration: 10, |
74 | maxVideoPts: 2 * 10 * 1000 + 1, | 52 | end: 2 * 10 + 2, |
75 | maxAudioPts: 2 * 10 * 1000 + 2, | ||
76 | uri: '1.ts' | 53 | uri: '1.ts' |
77 | }, { | 54 | }, { |
78 | duration: 10, | 55 | duration: 10, |
79 | maxVideoPts: 3 * 10 * 1000 + 1, | 56 | end: 3 * 10 + 2, |
80 | maxAudioPts: 3 * 10 * 1000 + 2, | ||
81 | uri: '2.ts' | 57 | uri: '2.ts' |
82 | }, { | 58 | }, { |
83 | duration: 10, | 59 | duration: 10, |
84 | maxVideoPts: 4 * 10 * 1000 + 1, | 60 | end: 4 * 10 + 2, |
85 | maxAudioPts: 4 * 10 * 1000 + 2, | ||
86 | uri: '3.ts' | 61 | uri: '3.ts' |
87 | }] | 62 | }] |
88 | }, 0, 4); | 63 | }, 4); |
89 | 64 | ||
90 | equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); | 65 | equal(duration, 4 * 10 + 2, 'used timeline values'); |
91 | }); | 66 | }); |
92 | 67 | ||
93 | test('works when partial PTS information is available', function() { | 68 | test('works when partial timeline information is available', function() { |
94 | var duration = Playlist.duration({ | 69 | var duration = Playlist.duration({ |
95 | mediaSequence: 0, | 70 | mediaSequence: 0, |
96 | endList: true, | 71 | endList: true, |
97 | segments: [{ | 72 | segments: [{ |
98 | minVideoPts: 1, | 73 | start: 0, |
99 | minAudioPts: 2, | ||
100 | maxVideoPts: 10 * 1000 + 1, | ||
101 | |||
102 | // intentionally less duration than video | ||
103 | // the max stream duration should be used | ||
104 | maxAudioPts: 10 * 1000 + 1, | ||
105 | uri: '0.ts' | 74 | uri: '0.ts' |
106 | }, { | 75 | }, { |
107 | duration: 9, | 76 | duration: 9, |
... | @@ -111,67 +80,17 @@ | ... | @@ -111,67 +80,17 @@ |
111 | uri: '2.ts' | 80 | uri: '2.ts' |
112 | }, { | 81 | }, { |
113 | duration: 10, | 82 | duration: 10, |
114 | minVideoPts: 30 * 1000 + 7, | 83 | start: 30.007, |
115 | minAudioPts: 30 * 1000 + 10, | 84 | end: 40.002, |
116 | maxVideoPts: 40 * 1000 + 1, | ||
117 | maxAudioPts: 40 * 1000 + 2, | ||
118 | uri: '3.ts' | 85 | uri: '3.ts' |
119 | }, { | 86 | }, { |
120 | duration: 10, | 87 | duration: 10, |
121 | maxVideoPts: 50 * 1000 + 1, | 88 | end: 50.0002, |
122 | maxAudioPts: 50 * 1000 + 2, | ||
123 | uri: '4.ts' | 89 | uri: '4.ts' |
124 | }] | 90 | }] |
125 | }, 0, 5); | 91 | }, 5); |
126 | 92 | ||
127 | equal(duration, | 93 | equal(duration, 50.0002, 'calculated with mixed intervals'); |
128 | ((50 * 1000 + 2) - 1) * 0.001, | ||
129 | 'calculated with mixed intervals'); | ||
130 | }); | ||
131 | |||
132 | test('ignores segments before the start', function() { | ||
133 | var duration = Playlist.duration({ | ||
134 | mediaSequence: 0, | ||
135 | segments: [{ | ||
136 | duration: 10, | ||
137 | uri: '0.ts' | ||
138 | }, { | ||
139 | duration: 10, | ||
140 | uri: '1.ts' | ||
141 | }, { | ||
142 | duration: 10, | ||
143 | uri: '2.ts' | ||
144 | }] | ||
145 | }, 1, 3); | ||
146 | |||
147 | equal(duration, 10 + 10, 'ignored the first segment'); | ||
148 | }); | ||
149 | |||
150 | test('ignores discontinuity sequences earlier than the start', function() { | ||
151 | var duration = Playlist.duration({ | ||
152 | mediaSequence: 0, | ||
153 | discontinuityStarts: [1, 3], | ||
154 | segments: [{ | ||
155 | minVideoPts: 0, | ||
156 | minAudioPts: 0, | ||
157 | maxVideoPts: 10 * 1000, | ||
158 | maxAudioPts: 10 * 1000, | ||
159 | uri: '0.ts' | ||
160 | }, { | ||
161 | discontinuity: true, | ||
162 | duration: 9, | ||
163 | uri: '1.ts' | ||
164 | }, { | ||
165 | duration: 10, | ||
166 | uri: '2.ts' | ||
167 | }, { | ||
168 | discontinuity: true, | ||
169 | duration: 10, | ||
170 | uri: '3.ts' | ||
171 | }] | ||
172 | }, 2, 4); | ||
173 | |||
174 | equal(duration, 10 + 10, 'excluded the earlier segments'); | ||
175 | }); | 94 | }); |
176 | 95 | ||
177 | test('ignores discontinuity sequences later than the end', function() { | 96 | test('ignores discontinuity sequences later than the end', function() { |
... | @@ -196,20 +115,19 @@ | ... | @@ -196,20 +115,19 @@ |
196 | duration: 10, | 115 | duration: 10, |
197 | uri: '3.ts' | 116 | uri: '3.ts' |
198 | }] | 117 | }] |
199 | }, 0, 2); | 118 | }, 2); |
200 | 119 | ||
201 | equal(duration, 19, 'excluded the later segments'); | 120 | equal(duration, 19, 'excluded the later segments'); |
202 | }); | 121 | }); |
203 | 122 | ||
204 | test('handles trailing segments without PTS information', function() { | 123 | test('handles trailing segments without timeline information', function() { |
205 | var duration = Playlist.duration({ | 124 | var playlist, duration; |
125 | playlist = { | ||
206 | mediaSequence: 0, | 126 | mediaSequence: 0, |
207 | endList: true, | 127 | endList: true, |
208 | segments: [{ | 128 | segments: [{ |
209 | minVideoPts: 0, | 129 | start: 0, |
210 | minAudioPts: 0, | 130 | end: 10.5, |
211 | maxVideoPts: 10 * 1000, | ||
212 | maxAudioPts: 10 * 1000, | ||
213 | uri: '0.ts' | 131 | uri: '0.ts' |
214 | }, { | 132 | }, { |
215 | duration: 9, | 133 | duration: 9, |
... | @@ -218,107 +136,43 @@ | ... | @@ -218,107 +136,43 @@ |
218 | duration: 10, | 136 | duration: 10, |
219 | uri: '2.ts' | 137 | uri: '2.ts' |
220 | }, { | 138 | }, { |
221 | minVideoPts: 29.5 * 1000, | 139 | start: 29.45, |
222 | minAudioPts: 29.5 * 1000, | 140 | end: 39.5, |
223 | maxVideoPts: 39.5 * 1000, | ||
224 | maxAudioPts: 39.5 * 1000, | ||
225 | uri: '3.ts' | 141 | uri: '3.ts' |
226 | }] | 142 | }] |
227 | }, 0, 3); | 143 | }; |
144 | |||
145 | duration = Playlist.duration(playlist, 3); | ||
146 | equal(duration, 29.45, 'calculated duration'); | ||
228 | 147 | ||
229 | equal(duration, 29.5, 'calculated duration'); | 148 | duration = Playlist.duration(playlist, 2); |
149 | equal(duration, 19.5, 'calculated duration'); | ||
230 | }); | 150 | }); |
231 | 151 | ||
232 | test('uses PTS intervals when the start and end segment have them', function() { | 152 | test('uses timeline intervals when segments have them', function() { |
233 | var playlist, duration; | 153 | var playlist, duration; |
234 | playlist = { | 154 | playlist = { |
235 | mediaSequence: 0, | 155 | mediaSequence: 0, |
236 | segments: [{ | 156 | segments: [{ |
237 | minVideoPts: 0, | 157 | start: 0, |
238 | minAudioPts: 0, | 158 | end: 10, |
239 | maxVideoPts: 10 * 1000, | ||
240 | maxAudioPts: 10 * 1000, | ||
241 | uri: '0.ts' | 159 | uri: '0.ts' |
242 | }, { | 160 | }, { |
243 | duration: 9, | 161 | duration: 9, |
244 | uri: '1.ts' | 162 | uri: '1.ts' |
245 | },{ | 163 | },{ |
246 | minVideoPts: 20 * 1000 + 100, | 164 | start: 20.1, |
247 | minAudioPts: 20 * 1000 + 100, | 165 | end: 30.1, |
248 | maxVideoPts: 30 * 1000 + 100, | ||
249 | maxAudioPts: 30 * 1000 + 100, | ||
250 | duration: 10, | 166 | duration: 10, |
251 | uri: '2.ts' | 167 | uri: '2.ts' |
252 | }] | 168 | }] |
253 | }; | 169 | }; |
254 | duration = Playlist.duration(playlist, 0, 2); | 170 | duration = Playlist.duration(playlist, 2); |
255 | 171 | ||
256 | equal(duration, 20.1, 'used the PTS-based interval'); | 172 | equal(duration, 20.1, 'used the timeline-based interval'); |
257 | 173 | ||
258 | duration = Playlist.duration(playlist, 0, 3); | 174 | duration = Playlist.duration(playlist, 3); |
259 | equal(duration, 30.1, 'used the PTS-based interval'); | 175 | equal(duration, 30.1, 'used the timeline-based interval'); |
260 | }); | ||
261 | |||
262 | test('works for media without audio', function() { | ||
263 | equal(Playlist.duration({ | ||
264 | mediaSequence: 0, | ||
265 | endList: true, | ||
266 | segments: [{ | ||
267 | minVideoPts: 0, | ||
268 | maxVideoPts: 9 * 1000, | ||
269 | uri: 'no-audio.ts' | ||
270 | }] | ||
271 | }), 9, 'used video PTS values'); | ||
272 | }); | ||
273 | |||
274 | test('works for media without video', function() { | ||
275 | equal(Playlist.duration({ | ||
276 | mediaSequence: 0, | ||
277 | endList: true, | ||
278 | segments: [{ | ||
279 | minAudioPts: 0, | ||
280 | maxAudioPts: 9 * 1000, | ||
281 | uri: 'no-video.ts' | ||
282 | }] | ||
283 | }), 9, 'used video PTS values'); | ||
284 | }); | ||
285 | |||
286 | test('uses the largest continuous available PTS ranges', function() { | ||
287 | var playlist = { | ||
288 | mediaSequence: 0, | ||
289 | segments: [{ | ||
290 | minVideoPts: 0, | ||
291 | minAudioPts: 0, | ||
292 | maxVideoPts: 10 * 1000, | ||
293 | maxAudioPts: 10 * 1000, | ||
294 | uri: '0.ts' | ||
295 | }, { | ||
296 | duration: 10, | ||
297 | uri: '1.ts' | ||
298 | }, { | ||
299 | // starts 0.5s earlier than the previous segment indicates | ||
300 | minVideoPts: 19.5 * 1000, | ||
301 | minAudioPts: 19.5 * 1000, | ||
302 | maxVideoPts: 29.5 * 1000, | ||
303 | maxAudioPts: 29.5 * 1000, | ||
304 | uri: '2.ts' | ||
305 | }, { | ||
306 | duration: 10, | ||
307 | uri: '3.ts' | ||
308 | }, { | ||
309 | // ... but by the last segment, there is actual 0.5s more | ||
310 | // content than duration indicates | ||
311 | minVideoPts: 40.5 * 1000, | ||
312 | minAudioPts: 40.5 * 1000, | ||
313 | maxVideoPts: 50.5 * 1000, | ||
314 | maxAudioPts: 50.5 * 1000, | ||
315 | uri: '4.ts' | ||
316 | }] | ||
317 | }; | ||
318 | |||
319 | equal(Playlist.duration(playlist, 0, 5), | ||
320 | 50.5, | ||
321 | 'calculated across the larger PTS interval'); | ||
322 | }); | 176 | }); |
323 | 177 | ||
324 | test('counts the time between segments as part of the earlier segment\'s duration', function() { | 178 | test('counts the time between segments as part of the earlier segment\'s duration', function() { |
... | @@ -326,22 +180,18 @@ | ... | @@ -326,22 +180,18 @@ |
326 | mediaSequence: 0, | 180 | mediaSequence: 0, |
327 | endList: true, | 181 | endList: true, |
328 | segments: [{ | 182 | segments: [{ |
329 | minVideoPts: 0, | 183 | start: 0, |
330 | minAudioPts: 0, | 184 | end: 10, |
331 | maxVideoPts: 1 * 10 * 1000, | ||
332 | maxAudioPts: 1 * 10 * 1000, | ||
333 | uri: '0.ts' | 185 | uri: '0.ts' |
334 | }, { | 186 | }, { |
335 | minVideoPts: 1 * 10 * 1000 + 100, | 187 | start: 10.1, |
336 | minAudioPts: 1 * 10 * 1000 + 100, | 188 | end: 20.1, |
337 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
338 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
339 | duration: 10, | 189 | duration: 10, |
340 | uri: '1.ts' | 190 | uri: '1.ts' |
341 | }] | 191 | }] |
342 | }, 0, 1); | 192 | }, 1); |
343 | 193 | ||
344 | equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap'); | 194 | equal(duration, 10.1, 'included the segment gap'); |
345 | }); | 195 | }); |
346 | 196 | ||
347 | test('accounts for discontinuities', function() { | 197 | test('accounts for discontinuities', function() { |
... | @@ -364,7 +214,7 @@ | ... | @@ -364,7 +214,7 @@ |
364 | duration: 10, | 214 | duration: 10, |
365 | uri: '1.ts' | 215 | uri: '1.ts' |
366 | }] | 216 | }] |
367 | }, 0, 2); | 217 | }, 2); |
368 | 218 | ||
369 | equal(duration, 10 + 10, 'handles discontinuities'); | 219 | equal(duration, 10 + 10, 'handles discontinuities'); |
370 | }); | 220 | }); |
... | @@ -389,7 +239,7 @@ | ... | @@ -389,7 +239,7 @@ |
389 | duration: 10, | 239 | duration: 10, |
390 | uri: '1.ts' | 240 | uri: '1.ts' |
391 | }] | 241 | }] |
392 | }, 0, 1); | 242 | }, 1); |
393 | 243 | ||
394 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | 244 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); |
395 | }); | 245 | }); |
... | @@ -412,7 +262,7 @@ | ... | @@ -412,7 +262,7 @@ |
412 | duration: 10, | 262 | duration: 10, |
413 | uri: '1.ts' | 263 | uri: '1.ts' |
414 | }] | 264 | }] |
415 | }, 0, 1, false); | 265 | }, 1, false); |
416 | 266 | ||
417 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | 267 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); |
418 | }); | 268 | }); |
... | @@ -431,10 +281,9 @@ | ... | @@ -431,10 +281,9 @@ |
431 | }] | 281 | }] |
432 | }; | 282 | }; |
433 | 283 | ||
434 | equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero'); | 284 | equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero'); |
435 | equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero'); | 285 | equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero'); |
436 | equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero'); | 286 | equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero'); |
437 | equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero'); | ||
438 | }); | 287 | }); |
439 | 288 | ||
440 | module('Playlist Seekable'); | 289 | module('Playlist Seekable'); | ... | ... |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment