205d32e2 by David LaPalomento

Track expired content duration

When we switch playlists in a live video, we have to find the right place in the new playlist to continue buffering. This is complicated because we can't guarantee the two variants are segmented at the same time positions or that the windows of time they represent are exactly in sync. Most of the time, they're pretty close to one another and we can use that fact to make better guesses at which segment to download when switching.

This PR adds back tracking of expired content in the playlist loader, which can then be used to estimate the seekable window for live playlists even before we've buffered any segments from them. This also allows seekable to be accurate even when the player has paused for a long time and all the segment timing information we gathered has gone out of date. To make rejoining or seeking in a live stream even more robust, we detect when a seek "misses" the live window and seek again to a safe position.
1 parent e02911f3
...@@ -152,6 +152,11 @@ ...@@ -152,6 +152,11 @@
152 // initialize the loader state 152 // initialize the loader state
153 loader.state = 'HAVE_NOTHING'; 153 loader.state = 'HAVE_NOTHING';
154 154
155 // track the time that has expired from the live window
156 // this allows the seekable start range to be calculated even if
157 // all segments with timing information have expired
158 this.expired_ = 0;
159
155 // capture the prototype dispose function 160 // capture the prototype dispose function
156 dispose = this.dispose; 161 dispose = this.dispose;
157 162
...@@ -346,7 +351,52 @@ ...@@ -346,7 +351,52 @@
346 * @param update {object} the updated media playlist object 351 * @param update {object} the updated media playlist object
347 */ 352 */
348 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { 353 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
354 var outdated, i, segment;
355
356 outdated = this.media_;
349 this.media_ = this.master.playlists[update.uri]; 357 this.media_ = this.master.playlists[update.uri];
358
359 if (!outdated) {
360 return;
361 }
362
363 // try using precise timing from first segment of the updated
364 // playlist
365 if (update.segments.length) {
366 if (update.segments[0].start !== undefined) {
367 this.expired_ = update.segments[0].start;
368 return;
369 } else if (update.segments[0].end !== undefined) {
370 this.expired_ = update.segments[0].end - update.segments[0].duration;
371 return;
372 }
373 }
374
375 // calculate expired by walking the outdated playlist
376 i = update.mediaSequence - outdated.mediaSequence - 1;
377
378 for (; i >= 0; i--) {
379 segment = outdated.segments[i];
380
381 if (!segment) {
382 // we missed information on this segment completely between
383 // playlist updates so we'll have to take an educated guess
384 // once we begin buffering again, any error we introduce can
385 // be corrected
386 this.expired_ += outdated.targetDuration || 10;
387 continue;
388 }
389
390 if (segment.end !== undefined) {
391 this.expired_ = segment.end;
392 return;
393 }
394 if (segment.start !== undefined) {
395 this.expired_ = segment.start + segment.duration;
396 return;
397 }
398 this.expired_ += segment.duration;
399 }
350 }; 400 };
351 401
352 /** 402 /**
...@@ -457,8 +507,8 @@ ...@@ -457,8 +507,8 @@
457 507
458 if (i === endIndex) { 508 if (i === endIndex) {
459 // We haven't found a segment but we did hit a known end point 509 // We haven't found a segment but we did hit a known end point
460 // so fallback to "Algorithm Jon" - try to interpolate the segment 510 // so fallback to interpolating between the segment index
461 // index based on the known span of the timeline we are dealing with 511 // based on the known span of the timeline we are dealing with
462 // and the number of segments inside that span 512 // and the number of segments inside that span
463 return startIndex + Math.floor( 513 return startIndex + Math.floor(
464 ((originalTime - knownStart) / (knownEnd - knownStart)) * 514 ((originalTime - knownStart) / (knownEnd - knownStart)) *
...@@ -481,9 +531,13 @@ ...@@ -481,9 +531,13 @@
481 // We haven't found a segment so load the first one 531 // We haven't found a segment so load the first one
482 return 0; 532 return 0;
483 } else { 533 } else {
484 // We known nothing so use "Algorithm A" - walk from the front 534 // We known nothing so walk from the front of the playlist,
485 // of the playlist naively subtracking durations until we find 535 // subtracting durations until we find a segment that contains
486 // a segment that contains time and return it 536 // time and return it
537 time = time - this.expired_;
538 if (time < 0) {
539 return -1;
540 }
487 for (i = 0; i < numSegments; i++) { 541 for (i = 0; i < numSegments; i++) {
488 segment = this.media_.segments[i]; 542 segment = this.media_.segments[i];
489 time -= segment.duration || targetDuration; 543 time -= segment.duration || targetDuration;
......
...@@ -187,7 +187,7 @@ videojs.HlsHandler.prototype.src = function(src) { ...@@ -187,7 +187,7 @@ videojs.HlsHandler.prototype.src = function(src) {
187 }.bind(this)); 187 }.bind(this));
188 188
189 this.playlists.on('loadedplaylist', function() { 189 this.playlists.on('loadedplaylist', function() {
190 var updatedPlaylist = this.playlists.media(); 190 var updatedPlaylist = this.playlists.media(), seekable;
191 191
192 if (!updatedPlaylist) { 192 if (!updatedPlaylist) {
193 // select the initial variant 193 // select the initial variant
...@@ -196,6 +196,14 @@ videojs.HlsHandler.prototype.src = function(src) { ...@@ -196,6 +196,14 @@ videojs.HlsHandler.prototype.src = function(src) {
196 } 196 }
197 197
198 this.updateDuration(this.playlists.media()); 198 this.updateDuration(this.playlists.media());
199
200 // update seekable
201 seekable = this.seekable();
202 if (this.duration() === Infinity &&
203 seekable.length !== 0) {
204 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
205 }
206
199 oldMediaPlaylist = updatedPlaylist; 207 oldMediaPlaylist = updatedPlaylist;
200 }.bind(this)); 208 }.bind(this));
201 209
...@@ -291,7 +299,6 @@ videojs.Hls.bufferedAdditions_ = function(original, update) { ...@@ -291,7 +299,6 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
291 return result; 299 return result;
292 }; 300 };
293 301
294
295 var parseCodecs = function(codecs) { 302 var parseCodecs = function(codecs) {
296 var result = { 303 var result = {
297 codecCount: 0, 304 codecCount: 0,
...@@ -312,6 +319,7 @@ var parseCodecs = function(codecs) { ...@@ -312,6 +319,7 @@ var parseCodecs = function(codecs) {
312 319
313 return result; 320 return result;
314 }; 321 };
322
315 /** 323 /**
316 * Blacklist playlists that are known to be codec or 324 * Blacklist playlists that are known to be codec or
317 * stream-incompatible with the SourceBuffer configuration. For 325 * stream-incompatible with the SourceBuffer configuration. For
...@@ -445,15 +453,15 @@ videojs.HlsHandler.prototype.play = function() { ...@@ -445,15 +453,15 @@ videojs.HlsHandler.prototype.play = function() {
445 // if the viewer has paused and we fell out of the live window, 453 // if the viewer has paused and we fell out of the live window,
446 // seek forward to the earliest available position 454 // seek forward to the earliest available position
447 if (this.duration() === Infinity) { 455 if (this.duration() === Infinity) {
448 if (this.tech_.currentTime() < this.tech_.seekable().start(0)) { 456 if (this.tech_.currentTime() < this.seekable().start(0)) {
449 this.tech_.setCurrentTime(this.tech_.seekable().start(0)); 457 this.tech_.setCurrentTime(this.seekable().start(0));
450 } 458 }
451 } 459 }
452 }; 460 };
453 461
454 videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { 462 videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) {
455 var 463 var
456 buffered = this.findCurrentBuffered_(); 464 buffered = this.findBufferedRange_();
457 465
458 if (!(this.playlists && this.playlists.media())) { 466 if (!(this.playlists && this.playlists.media())) {
459 // return immediately if the metadata is not ready yet 467 // return immediately if the metadata is not ready yet
...@@ -501,7 +509,7 @@ videojs.HlsHandler.prototype.duration = function() { ...@@ -501,7 +509,7 @@ videojs.HlsHandler.prototype.duration = function() {
501 }; 509 };
502 510
503 videojs.HlsHandler.prototype.seekable = function() { 511 videojs.HlsHandler.prototype.seekable = function() {
504 var media; 512 var media, seekable;
505 513
506 if (!this.playlists) { 514 if (!this.playlists) {
507 return videojs.createTimeRanges(); 515 return videojs.createTimeRanges();
...@@ -511,7 +519,25 @@ videojs.HlsHandler.prototype.seekable = function() { ...@@ -511,7 +519,25 @@ videojs.HlsHandler.prototype.seekable = function() {
511 return videojs.createTimeRanges(); 519 return videojs.createTimeRanges();
512 } 520 }
513 521
514 return videojs.Hls.Playlist.seekable(media); 522 seekable = videojs.Hls.Playlist.seekable(media);
523 if (seekable.length === 0) {
524 return seekable;
525 }
526
527 // if the seekable start is zero, it may be because the player has
528 // been paused for a long time and stopped buffering. in that case,
529 // fall back to the playlist loader's running estimate of expired
530 // time
531 if (seekable.start(0) === 0) {
532 return videojs.createTimeRanges([[
533 this.playlists.expired_,
534 this.playlists.expired_ + seekable.end(0)
535 ]]);
536 }
537
538 // seekable has been calculated based on buffering video data so it
539 // can be returned directly
540 return seekable;
515 }; 541 };
516 542
517 /** 543 /**
...@@ -522,15 +548,10 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) { ...@@ -522,15 +548,10 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) {
522 newDuration = videojs.Hls.Playlist.duration(playlist), 548 newDuration = videojs.Hls.Playlist.duration(playlist),
523 setDuration = function() { 549 setDuration = function() {
524 this.mediaSource.duration = newDuration; 550 this.mediaSource.duration = newDuration;
525 // update seekable
526 if (seekable.length !== 0 && newDuration === Infinity) {
527 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
528 }
529 this.tech_.trigger('durationchange'); 551 this.tech_.trigger('durationchange');
530 552
531 this.mediaSource.removeEventListener('sourceopen', setDuration); 553 this.mediaSource.removeEventListener('sourceopen', setDuration);
532 }.bind(this), 554 }.bind(this);
533 seekable = this.seekable();
534 555
535 // if the duration has changed, invalidate the cached value 556 // if the duration has changed, invalidate the cached value
536 if (oldDuration !== newDuration) { 557 if (oldDuration !== newDuration) {
...@@ -539,10 +560,6 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) { ...@@ -539,10 +560,6 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) {
539 this.mediaSource.addEventListener('sourceopen', setDuration); 560 this.mediaSource.addEventListener('sourceopen', setDuration);
540 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) { 561 } else if (!this.sourceBuffer || !this.sourceBuffer.updating) {
541 this.mediaSource.duration = newDuration; 562 this.mediaSource.duration = newDuration;
542 // update seekable
543 if (seekable.length !== 0 && newDuration === Infinity) {
544 this.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
545 }
546 this.tech_.trigger('durationchange'); 563 this.tech_.trigger('durationchange');
547 } 564 }
548 } 565 }
...@@ -745,43 +762,63 @@ videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() { ...@@ -745,43 +762,63 @@ videojs.HlsHandler.prototype.stopCheckingBuffer_ = function() {
745 this.tech_.off('waiting', this.drainBuffer); 762 this.tech_.off('waiting', this.drainBuffer);
746 }; 763 };
747 764
748 /** 765 var filterBufferedRanges = function(predicate) {
749 * Attempts to find the buffered TimeRange where playback is currently 766 return function(time) {
750 * happening. Returns a new TimeRange with one or zero ranges.
751 */
752 videojs.HlsHandler.prototype.findCurrentBuffered_ = function() {
753 var 767 var
754 ranges,
755 i, 768 i,
769 ranges = [],
756 tech = this.tech_, 770 tech = this.tech_,
757 // !!The order of the next two lines is important!! 771 // !!The order of the next two assignments is important!!
758 // `currentTime` must be equal-to or greater-than the start of the 772 // `currentTime` must be equal-to or greater-than the start of the
759 // buffered range. Flash executes out-of-process so, every value can 773 // buffered range. Flash executes out-of-process so, every value can
760 // change behind the scenes from line-to-line. By reading `currentTime` 774 // change behind the scenes from line-to-line. By reading `currentTime`
761 // after `buffered`, we ensure that it is always a current or later 775 // after `buffered`, we ensure that it is always a current or later
762 // value during playback. 776 // value during playback.
763 buffered = tech.buffered(), 777 buffered = tech.buffered();
764 currentTime = tech.currentTime(); 778
779
780 if (time === undefined) {
781 time = tech.currentTime();
782 }
765 783
766 if (buffered && buffered.length) { 784 if (buffered && buffered.length) {
767 // Search for a range containing the play-head 785 // Search for a range containing the play-head
768 for (i = 0; i < buffered.length; i++) { 786 for (i = 0; i < buffered.length; i++) {
769 if (buffered.start(i) - TIME_FUDGE_FACTOR <= currentTime && 787 if (predicate(buffered.start(i), buffered.end(i), time)) {
770 buffered.end(i) + TIME_FUDGE_FACTOR >= currentTime) { 788 ranges.push([buffered.start(i), buffered.end(i)]);
771 ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i));
772 ranges.indexOf = i;
773 return ranges;
774 } 789 }
775 } 790 }
776 } 791 }
777 792
778 // Return an empty range if no ranges exist 793 return videojs.createTimeRanges(ranges);
779 ranges = videojs.createTimeRanges(); 794 };
780 ranges.indexOf = -1;
781 return ranges;
782 }; 795 };
783 796
784 /** 797 /**
798 * Attempts to find the buffered TimeRange that contains the specified
799 * time, or where playback is currently happening if no specific time
800 * is specified.
801 * @param time (optional) {number} the time to filter on. Defaults to
802 * currentTime.
803 * @return a new TimeRanges object.
804 */
805 videojs.HlsHandler.prototype.findBufferedRange_ = filterBufferedRanges(function(start, end, time) {
806 return start - TIME_FUDGE_FACTOR <= time &&
807 end + TIME_FUDGE_FACTOR >= time;
808 });
809
810 /**
811 * Returns the TimeRanges that begin at or later than the specified
812 * time.
813 * @param time (optional) {number} the time to filter on. Defaults to
814 * currentTime.
815 * @return a new TimeRanges object.
816 */
817 videojs.HlsHandler.prototype.findNextBufferedRange_ = filterBufferedRanges(function(start, end, time) {
818 return start - TIME_FUDGE_FACTOR >= time;
819 });
820
821 /**
785 * Determines whether there is enough video data currently in the buffer 822 * Determines whether there is enough video data currently in the buffer
786 * and downloads a new segment if the buffered time is less than the goal. 823 * and downloads a new segment if the buffered time is less than the goal.
787 * @param seekToTime (optional) {number} the offset into the downloaded segment 824 * @param seekToTime (optional) {number} the offset into the downloaded segment
...@@ -791,7 +828,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) { ...@@ -791,7 +828,7 @@ videojs.HlsHandler.prototype.fillBuffer = function(mediaIndex) {
791 var 828 var
792 tech = this.tech_, 829 tech = this.tech_,
793 currentTime = tech.currentTime(), 830 currentTime = tech.currentTime(),
794 currentBuffered = this.findCurrentBuffered_(), 831 currentBuffered = this.findBufferedRange_(),
795 currentBufferedEnd = 0, 832 currentBufferedEnd = 0,
796 bufferedTime = 0, 833 bufferedTime = 0,
797 segment, 834 segment,
...@@ -1025,7 +1062,7 @@ videojs.HlsHandler.prototype.drainBuffer = function(event) { ...@@ -1025,7 +1062,7 @@ videojs.HlsHandler.prototype.drainBuffer = function(event) {
1025 segIv, 1062 segIv,
1026 segmentTimestampOffset = 0, 1063 segmentTimestampOffset = 0,
1027 hasBufferedContent = (this.tech_.buffered().length !== 0), 1064 hasBufferedContent = (this.tech_.buffered().length !== 0),
1028 currentBuffered = this.findCurrentBuffered_(), 1065 currentBuffered = this.findBufferedRange_(),
1029 outsideBufferedRanges = !(currentBuffered && currentBuffered.length); 1066 outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
1030 1067
1031 // if the buffer is empty or the source buffer hasn't been created 1068 // if the buffer is empty or the source buffer hasn't been created
...@@ -1132,6 +1169,7 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { ...@@ -1132,6 +1169,7 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
1132 playlist, 1169 playlist,
1133 currentMediaIndex, 1170 currentMediaIndex,
1134 currentBuffered, 1171 currentBuffered,
1172 seekable,
1135 timelineUpdates; 1173 timelineUpdates;
1136 1174
1137 this.pendingSegment_ = null; 1175 this.pendingSegment_ = null;
...@@ -1144,7 +1182,7 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { ...@@ -1144,7 +1182,7 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
1144 playlist = this.playlists.media(); 1182 playlist = this.playlists.media();
1145 segments = playlist.segments; 1183 segments = playlist.segments;
1146 currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence); 1184 currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence);
1147 currentBuffered = this.findCurrentBuffered_(); 1185 currentBuffered = this.findBufferedRange_();
1148 1186
1149 // if we switched renditions don't try to add segment timeline 1187 // if we switched renditions don't try to add segment timeline
1150 // information to the playlist 1188 // information to the playlist
...@@ -1156,9 +1194,25 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { ...@@ -1156,9 +1194,25 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () {
1156 // added by the media processing 1194 // added by the media processing
1157 segment = playlist.segments[currentMediaIndex]; 1195 segment = playlist.segments[currentMediaIndex];
1158 1196
1197 // when seeking to the beginning of the seekable range, it's
1198 // possible that imprecise timing information may cause the seek to
1199 // end up earlier than the start of the range
1200 // in that case, seek again
1201 seekable = this.seekable();
1202 if (this.tech_.seeking() &&
1203 currentBuffered.length === 0) {
1204 if (seekable.length &&
1205 this.tech_.currentTime() < seekable.start(0)) {
1206 var next = this.findNextBufferedRange_();
1207 if (next.length) {
1208 videojs.log('tried seeking to', this.tech_.currentTime(), 'but that was too early, retrying at', next.start(0));
1209 this.tech_.setCurrentTime(next.start(0));
1210 }
1211 }
1212 }
1213
1159 timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered, 1214 timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered,
1160 this.tech_.buffered()); 1215 this.tech_.buffered());
1161
1162 timelineUpdates.forEach(function (update) { 1216 timelineUpdates.forEach(function (update) {
1163 if (segment) { 1217 if (segment) {
1164 if (update.end !== undefined) { 1218 if (update.end !== undefined) {
......
...@@ -53,6 +53,17 @@ ...@@ -53,6 +53,17 @@
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_,
63 0,
64 'zero seconds expired');
65 });
66
56 test('requests the initial playlist immediately', function() { 67 test('requests the initial playlist immediately', function() {
57 new videojs.Hls.PlaylistLoader('master.m3u8'); 68 new videojs.Hls.PlaylistLoader('master.m3u8');
58 strictEqual(requests.length, 1, 'made a request'); 69 strictEqual(requests.length, 1, 'made a request');
...@@ -166,6 +177,125 @@ ...@@ -166,6 +177,125 @@
166 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 177 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
167 }); 178 });
168 179
180 test('increments expired seconds after a segment is removed', function() {
181 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
182 requests.pop().respond(200, null,
183 '#EXTM3U\n' +
184 '#EXT-X-MEDIA-SEQUENCE:0\n' +
185 '#EXTINF:10,\n' +
186 '0.ts\n' +
187 '#EXTINF:10,\n' +
188 '1.ts\n' +
189 '#EXTINF:10,\n' +
190 '2.ts\n' +
191 '#EXTINF:10,\n' +
192 '3.ts\n');
193 clock.tick(10 * 1000); // 10s, one target duration
194 requests.pop().respond(200, null,
195 '#EXTM3U\n' +
196 '#EXT-X-MEDIA-SEQUENCE:1\n' +
197 '#EXTINF:10,\n' +
198 '1.ts\n' +
199 '#EXTINF:10,\n' +
200 '2.ts\n' +
201 '#EXTINF:10,\n' +
202 '3.ts\n' +
203 '#EXTINF:10,\n' +
204 '4.ts\n');
205 equal(loader.expired_, 10, 'expired one segment');
206 });
207
208 test('increments expired seconds after a discontinuity', function() {
209 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
210 requests.pop().respond(200, null,
211 '#EXTM3U\n' +
212 '#EXT-X-MEDIA-SEQUENCE:0\n' +
213 '#EXTINF:10,\n' +
214 '0.ts\n' +
215 '#EXTINF:3,\n' +
216 '1.ts\n' +
217 '#EXT-X-DISCONTINUITY\n' +
218 '#EXTINF:4,\n' +
219 '2.ts\n');
220 clock.tick(10 * 1000); // 10s, one target duration
221 requests.pop().respond(200, null,
222 '#EXTM3U\n' +
223 '#EXT-X-MEDIA-SEQUENCE:1\n' +
224 '#EXTINF:3,\n' +
225 '1.ts\n' +
226 '#EXT-X-DISCONTINUITY\n' +
227 '#EXTINF:4,\n' +
228 '2.ts\n');
229 equal(loader.expired_, 10, 'expired one segment');
230
231 clock.tick(10 * 1000); // 10s, one target duration
232 requests.pop().respond(200, null,
233 '#EXTM3U\n' +
234 '#EXT-X-MEDIA-SEQUENCE:2\n' +
235 '#EXT-X-DISCONTINUITY\n' +
236 '#EXTINF:4,\n' +
237 '2.ts\n');
238 equal(loader.expired_, 13, 'no expirations after the discontinuity yet');
239
240 clock.tick(10 * 1000); // 10s, one target duration
241 requests.pop().respond(200, null,
242 '#EXTM3U\n' +
243 '#EXT-X-MEDIA-SEQUENCE:3\n' +
244 '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
245 '#EXTINF:10,\n' +
246 '3.ts\n');
247 equal(loader.expired_, 17, 'tracked expiration across the discontinuity');
248 });
249
250 test('tracks expired seconds properly when two discontinuities expire at once', function() {
251 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
252 requests.pop().respond(200, null,
253 '#EXTM3U\n' +
254 '#EXT-X-MEDIA-SEQUENCE:0\n' +
255 '#EXTINF:4,\n' +
256 '0.ts\n' +
257 '#EXT-X-DISCONTINUITY\n' +
258 '#EXTINF:5,\n' +
259 '1.ts\n' +
260 '#EXT-X-DISCONTINUITY\n' +
261 '#EXTINF:6,\n' +
262 '2.ts\n' +
263 '#EXTINF:7,\n' +
264 '3.ts\n');
265 clock.tick(10 * 1000);
266 requests.pop().respond(200, null,
267 '#EXTM3U\n' +
268 '#EXT-X-MEDIA-SEQUENCE:3\n' +
269 '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
270 '#EXTINF:7,\n' +
271 '3.ts\n');
272 equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities');
273 });
274
275 test('estimates expired if an entire window elapses between live playlist updates', function() {
276 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
277 requests.pop().respond(200, null,
278 '#EXTM3U\n' +
279 '#EXT-X-MEDIA-SEQUENCE:0\n' +
280 '#EXTINF:4,\n' +
281 '0.ts\n' +
282 '#EXTINF:5,\n' +
283 '1.ts\n');
284
285 clock.tick(10 * 1000);
286 requests.pop().respond(200, null,
287 '#EXTM3U\n' +
288 '#EXT-X-MEDIA-SEQUENCE:4\n' +
289 '#EXTINF:6,\n' +
290 '4.ts\n' +
291 '#EXTINF:7,\n' +
292 '5.ts\n');
293
294 equal(loader.expired_,
295 4 + 5 + (2 * 10),
296 'made a very rough estimate of expired time');
297 });
298
169 test('emits an error when an initial playlist request fails', function() { 299 test('emits an error when an initial playlist request fails', function() {
170 var 300 var
171 errors = [], 301 errors = [],
...@@ -672,7 +802,7 @@ ...@@ -672,7 +802,7 @@
672 equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); 802 equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5');
673 }); 803 });
674 804
675 test('accounts for expired time when calculating media index', function() { 805 test('accounts for non-zero starting segment time when calculating media index', function() {
676 var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); 806 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
677 requests.shift().respond(200, null, 807 requests.shift().respond(200, null,
678 '#EXTM3U\n' + 808 '#EXTM3U\n' +
...@@ -693,6 +823,53 @@ ...@@ -693,6 +823,53 @@
693 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); 823 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
694 }); 824 });
695 825
826 test('prefers precise segment timing when tracking expired time', function() {
827 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
828 requests.shift().respond(200, null,
829 '#EXTM3U\n' +
830 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
831 '#EXTINF:4,\n' +
832 '1001.ts\n' +
833 '#EXTINF:5,\n' +
834 '1002.ts\n');
835 // setup the loader with an "imprecise" value as if it had been
836 // accumulating segment durations as they expire
837 loader.expired_ = 160;
838 // annotate the first segment with a start time
839 // this number would be coming from the Source Buffer in practice
840 loader.media().segments[0].start = 150;
841
842 equal(loader.getMediaIndexForTime_(151), 0, 'prefers the value on the first segment');
843
844 clock.tick(10 * 1000); // trigger a playlist refresh
845 requests.shift().respond(200, null,
846 '#EXTM3U\n' +
847 '#EXT-X-MEDIA-SEQUENCE:1002\n' +
848 '#EXTINF:5,\n' +
849 '1002.ts\n');
850 equal(loader.getMediaIndexForTime_(150 + 4 + 1), 0, 'tracks precise expired times');
851 });
852
853 test('accounts for expired time when calculating media index', function() {
854 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
855 requests.shift().respond(200, null,
856 '#EXTM3U\n' +
857 '#EXT-X-MEDIA-SEQUENCE:1001\n' +
858 '#EXTINF:4,\n' +
859 '1001.ts\n' +
860 '#EXTINF:5,\n' +
861 '1002.ts\n');
862 loader.expired_ = 150;
863
864 equal(loader.getMediaIndexForTime_(0), -1, 'expired content returns a negative index');
865 equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns a negative index');
866 equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position');
867 equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment');
868 equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment');
869 equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment');
870 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
871 });
872
696 test('does not misintrepret playlists missing newlines at the end', function() { 873 test('does not misintrepret playlists missing newlines at the end', function() {
697 var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); 874 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
698 requests.shift().respond(200, null, 875 requests.shift().respond(200, null,
......
...@@ -549,11 +549,11 @@ test('finds the correct buffered region based on currentTime', function() { ...@@ -549,11 +549,11 @@ test('finds the correct buffered region based on currentTime', function() {
549 standardXHRResponse(requests[1]); 549 standardXHRResponse(requests[1]);
550 player.currentTime(3); 550 player.currentTime(3);
551 clock.tick(1); 551 clock.tick(1);
552 equal(player.tech_.hls.findCurrentBuffered_().end(0), 552 equal(player.tech_.hls.findBufferedRange_().end(0),
553 5, 'inside the first buffered region'); 553 5, 'inside the first buffered region');
554 player.currentTime(6); 554 player.currentTime(6);
555 clock.tick(1); 555 clock.tick(1);
556 equal(player.tech_.hls.findCurrentBuffered_().end(0), 556 equal(player.tech_.hls.findBufferedRange_().end(0),
557 12, 'inside the second buffered region'); 557 12, 'inside the second buffered region');
558 }); 558 });
559 559
...@@ -1636,6 +1636,41 @@ test('live playlist starts with correct currentTime value', function() { ...@@ -1636,6 +1636,41 @@ test('live playlist starts with correct currentTime value', function() {
1636 'currentTime is updated at playback'); 1636 'currentTime is updated at playback');
1637 }); 1637 });
1638 1638
1639 test('adjusts the seekable start based on the amount of expired live content', function() {
1640 player.src({
1641 src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
1642 type: 'application/vnd.apple.mpegurl'
1643 });
1644 openMediaSource(player);
1645
1646 standardXHRResponse(requests.shift());
1647
1648 // add timeline info to the playlist
1649 player.tech_.hls.playlists.media().segments[1].end = 29.5;
1650 // expired_ should be ignored if there is timeline information on
1651 // the playlist
1652 player.tech_.hls.playlists.expired_ = 172;
1653
1654 equal(player.seekable().start(0),
1655 29.5 - 29,
1656 'offset the seekable start');
1657 });
1658
1659 test('estimates seekable ranges for live streams that have been paused for a long time', function() {
1660 player.src({
1661 src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
1662 type: 'application/vnd.apple.mpegurl'
1663 });
1664 openMediaSource(player);
1665
1666 standardXHRResponse(requests.shift());
1667 player.tech_.hls.playlists.expired_ = 172;
1668
1669 equal(player.seekable().start(0),
1670 player.tech_.hls.playlists.expired_,
1671 'offset the seekable start');
1672 });
1673
1639 test('resets the time to a seekable position when resuming a live stream ' + 1674 test('resets the time to a seekable position when resuming a live stream ' +
1640 'after a long break', function() { 1675 'after a long break', function() {
1641 var seekTarget; 1676 var seekTarget;
......