d2c53be3 by David LaPalomento

Merge pull request #411 from dmlap/next-segment-calculation

Determine the segment to load by looking at buffered
2 parents e3c7a1bc 0de3d793
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);
261 loader.trigger('mediachange'); 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 {
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, 323 }
329 request, 324 return;
330 parser.manifest.playlists[0].uri);
331 if (!error) {
332 loader.trigger('loadedmetadata');
333 }
334 });
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--) {
432 387 segment = this.media_.segments[i];
433 // HLS version 3 and lower round segment durations to the 388 if (segment.end !== undefined && segment.end <= time) {
434 // nearest decimal integer. When the correct media index is 389 time -= segment.end;
435 // ambiguous, prefer the higher one. 390 break;
436 if (time <= 0) { 391 }
437 return i; 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 }
398
399 time -= segment.start;
400 time -= segment.duration || targetDuration;
401 if (time < 0) {
402 // the segment with start information is also our best guess
403 // for the momment
404 return i;
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);
438 } 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;
......
...@@ -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');
......