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_
Showing
2 changed files
with
176 additions
and
61 deletions
... | @@ -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; |
495 | } | 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_(); | ||
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 && |
838 | currentMediaIndex, | 833 | segmentInfo && |
839 | timelineUpdate); | 834 | segmentInfo.playlist.uri === this.playlist_.uri) { |
835 | segmentEnd = Ranges.findSoleUncommonTimeRangesEnd(segmentInfo.buffered, | ||
836 | this.sourceUpdater_.buffered()); | ||
837 | timelineUpdated = updateSegmentMetadata(playlist, | ||
838 | currentMediaIndex, | ||
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 | }); | ... | ... |
-
Please register or sign in to post a comment