8da2b070 by Jon-Carlos Rivera

Fix time correction (#706)

* Fix how and when timeCorrection_ was being applied so that it works for Flash
* Always apply timeCorrection_ when we haven't learned anything even if playlists have changed
* Apply timeCorrection_ directly to the currentTime when calling getMediaIndexForTime so that we can work around bad segment.end metadata in the playlist
* Emit an error if we have been stuck in a "timeCorrection" loop for more than 5 fetch attempts

* Make sure to clear the timeout before setting another in monitorBuffer_

* Clearer naming of variables

* Use timeCorrection_ when a chosen segment is more than 90% buffered
* Break out timeCorrection_ functionality into a reusable piece

* Added tests of the new timeCorrection_ functionality
* Test that timeCorrection is applied even when the segment.end data is misleading
* Verify that an error is emitted when we have failed to make progress after 5 time-corrections
* Ensure that we only have 1 timer if monitorBuffer_ is called multiple times

* Moved percent-buffered check out of checkBuffer_ to fillBuffer_
1 parent 1f7cc98f
...@@ -303,6 +303,11 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -303,6 +303,11 @@ export default class SegmentLoader extends videojs.EventTarget {
303 if (this.state === 'READY') { 303 if (this.state === 'READY') {
304 this.fillBuffer_(); 304 this.fillBuffer_();
305 } 305 }
306
307 if (this.checkBufferTimeout_) {
308 window.clearTimeout(this.checkBufferTimeout_);
309 }
310
306 this.checkBufferTimeout_ = window.setTimeout(this.monitorBuffer_.bind(this), 311 this.checkBufferTimeout_ = window.setTimeout(this.monitorBuffer_.bind(this),
307 CHECK_BUFFER_DELAY); 312 CHECK_BUFFER_DELAY);
308 } 313 }
...@@ -364,8 +369,8 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -364,8 +369,8 @@ export default class SegmentLoader extends videojs.EventTarget {
364 if (currentBuffered.length === 0) { 369 if (currentBuffered.length === 0) {
365 // find the segment containing currentTime 370 // find the segment containing currentTime
366 mediaIndex = getMediaIndexForTime(playlist, 371 mediaIndex = getMediaIndexForTime(playlist,
367 currentTime, 372 currentTime + this.timeCorrection_,
368 this.expired_ + this.timeCorrection_); 373 this.expired_);
369 } else { 374 } else {
370 // find the segment adjacent to the end of the current 375 // find the segment adjacent to the end of the current
371 // buffered region 376 // buffered region
...@@ -384,42 +389,14 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -384,42 +389,14 @@ export default class SegmentLoader extends videojs.EventTarget {
384 return null; 389 return null;
385 } 390 }
386 mediaIndex = getMediaIndexForTime(playlist, 391 mediaIndex = getMediaIndexForTime(playlist,
387 currentBufferedEnd, 392 currentBufferedEnd + this.timeCorrection_,
388 this.expired_ + this.timeCorrection_); 393 this.expired_);
389 } 394 }
390 395
391 if (mediaIndex < 0 || mediaIndex === playlist.segments.length) { 396 if (mediaIndex < 0 || mediaIndex === playlist.segments.length) {
392 return null; 397 return null;
393 } 398 }
394 399
395 // Sanity check the segment-index determining logic above but calcuating
396 // the percentage of the chosen segment that is buffered. If more than 90%
397 // of the segment is buffered then fetching it will likely not help in any
398 // way
399 let percentBuffered = this.getSegmentBufferedPercent_(playlist,
400 mediaIndex,
401 currentTime,
402 buffered);
403
404 if (percentBuffered >= 90) {
405 // Retry the buffered calculation with the next segment if there is another
406 // segment after the currently selected segment
407 if (mediaIndex + 1 < playlist.segments.length) {
408 percentBuffered = this.getSegmentBufferedPercent_(playlist,
409 mediaIndex + 1,
410 currentTime,
411 buffered);
412 }
413
414 // If both checks failed return and don't load anything
415 if (percentBuffered >= 90) {
416 return;
417 }
418
419 // Otherwise, continue with the next segment
420 mediaIndex += 1;
421 }
422
423 segment = playlist.segments[mediaIndex]; 400 segment = playlist.segments[mediaIndex];
424 let startOfSegment = duration(playlist, 401 let startOfSegment = duration(playlist,
425 playlist.mediaSequence + mediaIndex, 402 playlist.mediaSequence + mediaIndex,
...@@ -490,9 +467,33 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -490,9 +467,33 @@ export default class SegmentLoader extends videojs.EventTarget {
490 this.currentTime_(), 467 this.currentTime_(),
491 this.timestampOffset_); 468 this.timestampOffset_);
492 469
493 if (request) { 470 if (!request) {
494 this.loadSegment_(request); 471 return;
472 }
473
474 // Sanity check the segment-index determining logic by calcuating the
475 // percentage of the chosen segment that is buffered. If more than 90%
476 // of the segment is buffered then fetching it will likely not help in
477 // any way
478 let percentBuffered = this.getSegmentBufferedPercent_(this.playlist_,
479 request.mediaIndex,
480 this.currentTime_(),
481 this.sourceUpdater_.buffered());
482
483 if (percentBuffered >= 90) {
484 // Increment the timeCorrection_ variable to push the fetcher forward
485 // in time and hopefully skip any gaps or flaws in our understanding
486 // of the media
487 this.incrementTimeCorrection_(this.playlist_.targetDuration);
488
489 if (!this.paused()) {
490 this.fillBuffer_();
495 } 491 }
492
493 return;
494 }
495
496 this.loadSegment_(request);
496 } 497 }
497 498
498 /** 499 /**
...@@ -757,12 +758,11 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -757,12 +758,11 @@ export default class SegmentLoader extends videojs.EventTarget {
757 let currentTime = this.currentTime_(); 758 let currentTime = this.currentTime_();
758 759
759 this.pendingSegment_ = null; 760 this.pendingSegment_ = null;
760 // add segment timeline information if we're still using the 761
761 // same playlist 762 // add segment metadata if it we have gained information during the
762 if (segmentInfo && segmentInfo.playlist.uri === this.playlist_.uri) { 763 // last append
763 this.updateTimeline_(segmentInfo); 764 this.updateTimeline_(segmentInfo);
764 this.trigger('progress'); 765 this.trigger('progress');
765 }
766 766
767 let currentMediaIndex = segmentInfo.mediaIndex; 767 let currentMediaIndex = segmentInfo.mediaIndex;
768 768
...@@ -819,24 +819,26 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -819,24 +819,26 @@ export default class SegmentLoader extends videojs.EventTarget {
819 */ 819 */
820 updateTimeline_(segmentInfo) { 820 updateTimeline_(segmentInfo) {
821 let segment; 821 let segment;
822 let timelineUpdate; 822 let segmentEnd;
823 let timelineUpdated;
824 let segmentLength = this.playlist_.targetDuration;
823 let playlist = segmentInfo.playlist; 825 let playlist = segmentInfo.playlist;
824 let currentMediaIndex = segmentInfo.mediaIndex; 826 let currentMediaIndex = segmentInfo.mediaIndex;
825 827
826 currentMediaIndex += playlist.mediaSequence - this.playlist_.mediaSequence; 828 currentMediaIndex += playlist.mediaSequence - this.playlist_.mediaSequence;
827 segment = playlist.segments[currentMediaIndex]; 829 segment = playlist.segments[currentMediaIndex];
828 830
829 if (!segment) {
830 return;
831 }
832
833 timelineUpdate = Ranges.findSoleUncommonTimeRangesEnd(segmentInfo.buffered,
834 this.sourceUpdater_.buffered());
835
836 // Update segment meta-data (duration and end-point) based on timeline 831 // Update segment meta-data (duration and end-point) based on timeline
837 let timelineUpdated = updateSegmentMetadata(playlist, 832 if (segment &&
833 segmentInfo &&
834 segmentInfo.playlist.uri === this.playlist_.uri) {
835 segmentEnd = Ranges.findSoleUncommonTimeRangesEnd(segmentInfo.buffered,
836 this.sourceUpdater_.buffered());
837 timelineUpdated = updateSegmentMetadata(playlist,
838 currentMediaIndex, 838 currentMediaIndex,
839 timelineUpdate); 839 segmentEnd);
840 segmentLength = segment.duration;
841 }
840 842
841 // the last segment append must have been entirely in the 843 // the last segment append must have been entirely in the
842 // already buffered time ranges. adjust the timeCorrection 844 // already buffered time ranges. adjust the timeCorrection
...@@ -844,9 +846,33 @@ export default class SegmentLoader extends videojs.EventTarget { ...@@ -844,9 +846,33 @@ export default class SegmentLoader extends videojs.EventTarget {
844 // to the buffered time ranges and improves subsequent media 846 // to the buffered time ranges and improves subsequent media
845 // index calculations. 847 // index calculations.
846 if (!timelineUpdated) { 848 if (!timelineUpdated) {
847 this.timeCorrection_ -= segment.duration; 849 this.incrementTimeCorrection_(segmentLength);
848 } else { 850 } else {
849 this.timeCorrection_ = 0; 851 this.timeCorrection_ = 0;
850 } 852 }
851 } 853 }
854
855 /**
856 * add a number of seconds to the currentTime when determining which
857 * segment to fetch in order to force the fetcher to advance in cases
858 * where it may get stuck on the same segment due to buffer gaps or
859 * missing segment annotation after a rendition switch (especially
860 * during a live stream)
861 *
862 * @private
863 * @param {Number} secondsToIncrement number of seconds to add to the
864 * timeCorrection_ variable
865 */
866 incrementTimeCorrection_(secondsToIncrement) {
867 // If we have already incremented timeCorrection_ beyond the limit,
868 // then stop trying to find a segment, pause fetching, and emit an
869 // error event
870 if (this.timeCorrection_ >= this.playlist_.targetDuration * 5) {
871 this.timeCorrection_ = 0;
872 this.pause();
873 return this.trigger('error');
874 }
875
876 this.timeCorrection_ += secondsToIncrement;
877 }
852 } 878 }
......
...@@ -6,6 +6,7 @@ import { useFakeEnvironment, useFakeMediaSource } from './test-helpers.js'; ...@@ -6,6 +6,7 @@ import { useFakeEnvironment, useFakeMediaSource } from './test-helpers.js';
6 6
7 const playlistWithDuration = function(time, conf) { 7 const playlistWithDuration = function(time, conf) {
8 let result = { 8 let result = {
9 targetDuration: 10,
9 mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0, 10 mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
10 discontinuityStarts: [], 11 discontinuityStarts: [],
11 segments: [], 12 segments: [],
...@@ -350,14 +351,7 @@ QUnit.test('never attempt to load a segment that ' + ...@@ -350,14 +351,7 @@ QUnit.test('never attempt to load a segment that ' +
350 sourceBuffer.buffered = videojs.createTimeRanges([[0, 9.2]]); 351 sourceBuffer.buffered = videojs.createTimeRanges([[0, 9.2]]);
351 sourceBuffer.trigger('updateend'); 352 sourceBuffer.trigger('updateend');
352 353
353 // the next segment doesn't increase the buffer at all 354 // the loader should move on to the next segment
354 QUnit.equal(this.requests[0].url, '1.ts', 'requested the next segment');
355 this.clock.tick(1);
356 this.requests[0].response = new Uint8Array(10).buffer;
357 this.requests.shift().respond(200, null, '');
358 sourceBuffer.trigger('updateend');
359
360 // so the loader should try the next segment
361 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment'); 355 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
362 }); 356 });
363 357
...@@ -398,6 +392,79 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct ...@@ -398,6 +392,79 @@ QUnit.test('adjusts the playlist offset if no buffering progress is made', funct
398 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment'); 392 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
399 }); 393 });
400 394
395 QUnit.test('adjusts the playlist offset even when segment.end is set if no' +
396 ' buffering progress is made', function() {
397 let sourceBuffer;
398 let playlist;
399
400 playlist = playlistWithDuration(40);
401 playlist.endList = false;
402 loader.playlist(playlist);
403 loader.mimeType(this.mimeType);
404 loader.load();
405 sourceBuffer = mediaSource.sourceBuffers[0];
406
407 // buffer some content and switch playlists on progress
408 this.clock.tick(1);
409 this.requests[0].response = new Uint8Array(10).buffer;
410 this.requests.shift().respond(200, null, '');
411 sourceBuffer.buffered = videojs.createTimeRanges([[0, 5]]);
412 loader.one('progress', function f() {
413 QUnit.equal(playlist.segments[0].end, 5, 'segment.end was set based on the buffer');
414 playlist.segments[0].end = 10;
415 });
416
417 sourceBuffer.trigger('updateend');
418
419 // the next segment doesn't increase the buffer at all
420 QUnit.equal(this.requests[0].url, '0.ts', 'requested the same segment');
421 this.clock.tick(1);
422 this.requests[0].response = new Uint8Array(10).buffer;
423 this.requests.shift().respond(200, null, '');
424 sourceBuffer.trigger('updateend');
425
426 // so the loader should try the next segment
427 QUnit.equal(this.requests[0].url, '1.ts', 'moved ahead a segment');
428 });
429
430 QUnit.test('adjusts the playlist offset if no buffering progress is made after' +
431 ' five consecutive attempts', function() {
432 let sourceBuffer;
433 let playlist;
434 let errors = 0;
435
436 loader.on('error', () => {
437 errors++;
438 });
439
440 playlist = playlistWithDuration(120);
441 playlist.endList = false;
442 loader.playlist(playlist);
443 loader.mimeType(this.mimeType);
444 loader.load();
445 sourceBuffer = mediaSource.sourceBuffers[0];
446
447 // buffer some content
448 this.clock.tick(1);
449 this.requests[0].response = new Uint8Array(10).buffer;
450 this.requests.shift().respond(200, null, '');
451 sourceBuffer.buffered = videojs.createTimeRanges([[0, 10]]);
452 sourceBuffer.trigger('updateend');
453
454 for (let i = 1; i <= 6; i++) {
455 // the next segment doesn't increase the buffer at all
456 QUnit.equal(this.requests[0].url, (i + '.ts'), 'requested the next segment');
457 this.clock.tick(1);
458 this.requests[0].response = new Uint8Array(10).buffer;
459 this.requests.shift().respond(200, null, '');
460 sourceBuffer.trigger('updateend');
461 }
462
463 // so the loader should try the next segment
464 QUnit.equal(errors, 1, 'emitted error');
465 QUnit.ok(loader.paused(), 'loader is paused');
466 });
467
401 QUnit.test('cancels outstanding requests on abort', function() { 468 QUnit.test('cancels outstanding requests on abort', function() {
402 loader.playlist(playlistWithDuration(20)); 469 loader.playlist(playlistWithDuration(20));
403 loader.mimeType(this.mimeType); 470 loader.mimeType(this.mimeType);
...@@ -855,6 +922,7 @@ QUnit.module('Segment Loading Calculation', { ...@@ -855,6 +922,7 @@ QUnit.module('Segment Loading Calculation', {
855 this.env = useFakeEnvironment(); 922 this.env = useFakeEnvironment();
856 this.mse = useFakeMediaSource(); 923 this.mse = useFakeMediaSource();
857 this.hasPlayed = true; 924 this.hasPlayed = true;
925 this.clock = this.env.clock;
858 926
859 currentTime = 0; 927 currentTime = 0;
860 loader = new SegmentLoader({ 928 loader = new SegmentLoader({
...@@ -911,11 +979,14 @@ QUnit.test('does not download the next segment if the buffer is full', function( ...@@ -911,11 +979,14 @@ QUnit.test('does not download the next segment if the buffer is full', function(
911 QUnit.test('downloads the next segment if the buffer is getting low', function() { 979 QUnit.test('downloads the next segment if the buffer is getting low', function() {
912 let buffered; 980 let buffered;
913 let segmentInfo; 981 let segmentInfo;
982 let playlist = playlistWithDuration(30);
914 983
915 loader.mimeType(this.mimeType); 984 loader.mimeType(this.mimeType);
985 loader.playlist(playlist);
916 986
987 playlist.segments[1].end = 19.999;
917 buffered = videojs.createTimeRanges([[0, 19.999]]); 988 buffered = videojs.createTimeRanges([[0, 19.999]]);
918 segmentInfo = loader.checkBuffer_(buffered, playlistWithDuration(30), 15); 989 segmentInfo = loader.checkBuffer_(buffered, playlist, 15);
919 990
920 QUnit.ok(segmentInfo, 'made a request'); 991 QUnit.ok(segmentInfo, 'made a request');
921 QUnit.equal(segmentInfo.uri, '2.ts', 'requested the third segment'); 992 QUnit.equal(segmentInfo.uri, '2.ts', 'requested the third segment');
...@@ -1001,3 +1072,21 @@ QUnit.test('adjusts calculations based on expired time', function() { ...@@ -1001,3 +1072,21 @@ QUnit.test('adjusts calculations based on expired time', function() {
1001 QUnit.ok(segmentInfo, 'fetched a segment'); 1072 QUnit.ok(segmentInfo, 'fetched a segment');
1002 QUnit.equal(segmentInfo.uri, '2.ts', 'accounted for expired time'); 1073 QUnit.equal(segmentInfo.uri, '2.ts', 'accounted for expired time');
1003 }); 1074 });
1075
1076 QUnit.test('doesn\'t allow more than one monitor buffer timer to be set', function() {
1077 let timeoutCount = this.clock.methods.length;
1078
1079 loader.mimeType(this.mimeType);
1080 loader.monitorBuffer_();
1081
1082 QUnit.equal(this.clock.methods.length, timeoutCount, 'timeout count remains the same');
1083
1084 loader.monitorBuffer_();
1085
1086 QUnit.equal(this.clock.methods.length, timeoutCount, 'timeout count remains the same');
1087
1088 loader.monitorBuffer_();
1089 loader.monitorBuffer_();
1090
1091 QUnit.equal(this.clock.methods.length, timeoutCount, 'timeout count remains the same');
1092 });
......