From 1cc99c1241c89b5fb5a989dd52ff0b9a9753b65f Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Mon, 10 Jan 2022 14:13:49 -0800 Subject: [PATCH] fix: Fix playback failure due to rounding errors If a rounding error in the DASH parser created an extra "phantom" segment that extended just beyond the presentation duration, this was causing a playback failure. Now StreamingEngine will tolerate these small offsets and end playback at the correct time. Fixes #3717 Change-Id: I00780a664aff4148b6b1046d8322a68da745de40 --- lib/media/streaming_engine.js | 5 +- test/media/streaming_engine_unit.js | 81 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 863ca9c872..d6048b9cc5 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -988,7 +988,10 @@ shaka.media.StreamingEngine = class { unscaledBufferingGoal * this.bufferingGoalScale_; // Check if we've buffered to the end of the presentation. - if (timeNeeded >= this.manifest_.presentationTimeline.getDuration()) { + const timeUntilEnd = + this.manifest_.presentationTimeline.getDuration() - timeNeeded; + const oneMicrosecond = 1e-6; + if (timeUntilEnd < oneMicrosecond) { // We shouldn't rebuffer if the playhead is close to the end of the // presentation. shaka.log.debug(logPrefix, 'buffered to end of presentation'); diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index 3f17b8f300..23edba3b85 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -824,6 +824,53 @@ describe('StreamingEngine', () => { .toHaveBeenCalledWith('video', 0, lt20, gt40); }); + // Regression test for https://github.com/google/shaka-player/issues/3717 + it('applies fudge factors for the duration', async () => { + setupVod(); + + // In #3717, the duration was just barely large enough to encompass an + // additional segment, but that segment didn't exist, so playback never + // completed. Here, we set the duration to just beyond the 3rd segment, and + // we make the 4th segment fail when requested. + const duration = 30.000000005; + timeline.getDuration.and.returnValue(duration); + + const targetUri = '1_video_3'; // The URI of the 4th video segment. + failRequestsForTarget(netEngine, targetUri); + + mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); + createStreamingEngine(); + + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + await streamingEngine.start(); + playing = true; + await runTest(); + + // The end of the stream should have been reached, and the 4th segment from + // each type should never have been requested. + expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); + + const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT; + + netEngine.expectRequest('0_audio_0', segmentType); + netEngine.expectRequest('0_video_0', segmentType); + netEngine.expectRequest('0_text_0', segmentType); + + netEngine.expectRequest('0_audio_1', segmentType); + netEngine.expectRequest('0_video_1', segmentType); + netEngine.expectRequest('0_text_1', segmentType); + + netEngine.expectRequest('1_audio_2', segmentType); + netEngine.expectRequest('1_video_2', segmentType); + netEngine.expectRequest('1_text_2', segmentType); + + netEngine.expectNoRequest('1_audio_3', segmentType); + netEngine.expectNoRequest('1_video_3', segmentType); + netEngine.expectNoRequest('1_text_3', segmentType); + }); + it('does not buffer one media type ahead of another', async () => { setupVod(); mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); @@ -3390,6 +3437,40 @@ describe('StreamingEngine', () => { segmentAvailability.end++; } + /** + * @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine + * look-alike. + * @param {string} targetUri + * @param {shaka.util.Error.Code=} errorCode + */ + function failRequestsForTarget( + netEngine, targetUri, errorCode=shaka.util.Error.Code.BAD_HTTP_STATUS) { + // eslint-disable-next-line no-restricted-syntax + const originalNetEngineRequest = netEngine.request.bind(netEngine); + + netEngine.request = jasmine.createSpy('request').and.callFake( + (requestType, request) => { + if (request.uris[0] == targetUri) { + const data = [targetUri]; + + if (errorCode == shaka.util.Error.Code.BAD_HTTP_STATUS) { + data.push(404); + data.push(''); + } + + // The compiler still sees the error code parameter as potentially + // undefined, even though we gave it a default value. + goog.asserts.assert(errorCode != undefined, 'Undefined error code'); + + return shaka.util.AbortableOperation.failed(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + errorCode, data)); + } + return originalNetEngineRequest(requestType, request); + }); + } + /** * @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine * look-alike.