Merge pull request #459 from dmlap/long-term-seekable-start
Long term seekable start
Showing
6 changed files
with
437 additions
and
50 deletions
... | @@ -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) + TIME_FUDGE_FACTOR); | ||
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, | ... | ... |
... | @@ -45,6 +45,12 @@ | ... | @@ -45,6 +45,12 @@ |
45 | } | 45 | } |
46 | </style> | 46 | </style> |
47 | 47 | ||
48 | <script> | ||
49 | if (window.location.search === '?flash') { | ||
50 | videojs.options.techOrder = ['flash']; | ||
51 | } | ||
52 | </script> | ||
53 | |||
48 | </head> | 54 | </head> |
49 | <body> | 55 | <body> |
50 | <div class="info"> | 56 | <div class="info"> |
... | @@ -64,6 +70,7 @@ | ... | @@ -64,6 +70,7 @@ |
64 | type="application/x-mpegURL"> | 70 | type="application/x-mpegURL"> |
65 | </video> | 71 | </video> |
66 | <section class="stats"> | 72 | <section class="stats"> |
73 | <div class="player-stats"> | ||
67 | <h2>Player Stats</h2> | 74 | <h2>Player Stats</h2> |
68 | <dl> | 75 | <dl> |
69 | <dt>Current Time:</dt> | 76 | <dt>Current Time:</dt> |
... | @@ -77,7 +84,21 @@ | ... | @@ -77,7 +84,21 @@ |
77 | <dt>Measured Bitrate:</dt> | 84 | <dt>Measured Bitrate:</dt> |
78 | <dd class="measured-bitrate-stat">0 kbps</dd> | 85 | <dd class="measured-bitrate-stat">0 kbps</dd> |
79 | </dl> | 86 | </dl> |
80 | <h3>Bitrate Switching</h3> | 87 | </div> |
88 | <div class="event-counts"> | ||
89 | <h2>Event Counts</h2> | ||
90 | <dl> | ||
91 | <dt>Play:</dt> | ||
92 | <dd class="play-count">0</dd> | ||
93 | <dt>Playing:</dt> | ||
94 | <dd class="playing-count">0</dd> | ||
95 | <dt>Seeking:</dt> | ||
96 | <dd class="seeking-count">0</dd> | ||
97 | <dt>Seeked:</dt> | ||
98 | <dd class="seeked-count">0</dd> | ||
99 | </dl> | ||
100 | </div> | ||
101 | <h3 class="bitrate-switching">Bitrate Switching</h3> | ||
81 | <div class="switching-stats"> | 102 | <div class="switching-stats"> |
82 | Once the player begins loading, you'll see information about the | 103 | Once the player begins loading, you'll see information about the |
83 | operation of the adaptive quality switching here. | 104 | operation of the adaptive quality switching here. |
... | @@ -90,7 +111,7 @@ | ... | @@ -90,7 +111,7 @@ |
90 | <script> | 111 | <script> |
91 | videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf'; | 112 | videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf'; |
92 | // initialize the player | 113 | // initialize the player |
93 | var player = videojs('video'); | 114 | var player = videojs('video').ready(function() { |
94 | 115 | ||
95 | // ------------ | 116 | // ------------ |
96 | // Player Stats | 117 | // Player Stats |
... | @@ -107,7 +128,7 @@ | ... | @@ -107,7 +128,7 @@ |
107 | currentTimeStat.textContent = player.currentTime().toFixed(1); | 128 | currentTimeStat.textContent = player.currentTime().toFixed(1); |
108 | }); | 129 | }); |
109 | 130 | ||
110 | player.on('progress', function() { | 131 | window.setInterval(function() { |
111 | var bufferedText = '', oldStart, oldEnd, i; | 132 | var bufferedText = '', oldStart, oldEnd, i; |
112 | 133 | ||
113 | // buffered | 134 | // buffered |
... | @@ -146,10 +167,40 @@ | ... | @@ -146,10 +167,40 @@ |
146 | maximumFractionDigits: 1 | 167 | maximumFractionDigits: 1 |
147 | }) + ' kbps'; | 168 | }) + ' kbps'; |
148 | } | 169 | } |
170 | }, 1000); | ||
171 | |||
172 | var trackEventCount = function(eventName, selector) { | ||
173 | var count = 0, element = document.querySelector(selector); | ||
174 | player.on(eventName, function() { | ||
175 | count++; | ||
176 | element.innerHTML = count; | ||
149 | }); | 177 | }); |
178 | }; | ||
179 | trackEventCount('play', '.play-count'); | ||
180 | trackEventCount('playing', '.playing-count'); | ||
181 | trackEventCount('seeking', '.seeking-count'); | ||
182 | trackEventCount('seeked', '.seeked-count'); | ||
150 | 183 | ||
151 | videojs.Hls.displayStats(document.querySelector('.switching-stats'), player); | 184 | videojs.Hls.displayStats(document.querySelector('.switching-stats'), player); |
152 | videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player); | 185 | videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player); |
186 | }); | ||
187 | |||
188 | // ----------- | ||
189 | // Tech Switch | ||
190 | // ----------- | ||
191 | |||
192 | var techSwitch = document.createElement('a'); | ||
193 | techSwitch.className = 'tech-switch'; | ||
194 | if (player.el().querySelector('video')) { | ||
195 | techSwitch.href = window.location.origin + window.location.pathname + '?flash'; | ||
196 | techSwitch.appendChild(document.createTextNode('Switch to the Flash tech')); | ||
197 | } else { | ||
198 | techSwitch.href = window.location.origin + window.location.pathname; | ||
199 | techSwitch.appendChild(document.createTextNode('Stop forcing Flash')); | ||
200 | } | ||
201 | |||
202 | document.body.insertBefore(techSwitch, document.querySelector('.stats')); | ||
203 | |||
153 | </script> | 204 | </script> |
154 | </body> | 205 | </body> |
155 | </html> | 206 | </html> | ... | ... |
1 | .tech-switch { | ||
2 | padding: 8px 0 0; | ||
3 | display: block; | ||
4 | } | ||
5 | |||
6 | .player-stats, .event-counts { | ||
7 | box-sizing: border-box; | ||
8 | padding: 8px; | ||
9 | float: left; | ||
10 | width: 50%; | ||
11 | } | ||
12 | |||
13 | h3.bitrate-switching { | ||
14 | clear: both; | ||
15 | } | ||
16 | |||
1 | .axis text, | 17 | .axis text, |
2 | .cue text { | 18 | .cue text { |
3 | font: 12px sans-serif; | 19 | font: 12px sans-serif; | ... | ... |
... | @@ -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; | ... | ... |
-
Please register or sign in to post a comment