From 9705639f4514d8d2dbfe5d81a31388f99e6be507 Mon Sep 17 00:00:00 2001 From: Patrick Cruikshank Date: Fri, 14 Jan 2022 04:53:42 -0500 Subject: [PATCH] fix: Clear buffer on seek if mediaState is updating (#3795) Previously, we only cleared the buffer if the media state had something buffered. This ensured that we did not pointlessly clear the buffer when nothing was there. However, this lead to problems if a seek was performed early during the loading process, before the source buffer is initialized. During that time, nothing is buffered, so we would not clear the buffer on a seek. This lead to us accidentally fetching content from the beginning of the presentation, rather than starting from the new clock time. This changes the streaming engine to also clear the buffer if mediaState.performingUpdate is true, to avoid that situation. Closes #3299 --- lib/media/streaming_engine.js | 2 +- test/media/streaming_engine_unit.js | 64 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index d6048b9cc5..a501370c52 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -628,7 +628,7 @@ shaka.media.StreamingEngine = class { // Don't clear the buffer unless something is buffered. This extra // check prevents extra, useless calls to clear the buffer. - if (somethingBuffered) { + if (somethingBuffered || mediaState.performingUpdate) { this.forceClearBuffer_(mediaState); streamCleared = true; } diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index 23edba3b85..b88d58b34a 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -97,6 +97,9 @@ describe('StreamingEngine', () => { /** @type {!shaka.media.StreamingEngine} */ let streamingEngine; + /** @type {function(function(), number)} */ + let realSetTimeout; + /** * Runs the fake event loop. * @param {function()=} callback An optional callback that is executed @@ -116,6 +119,7 @@ describe('StreamingEngine', () => { } beforeAll(() => { + realSetTimeout = window.setTimeout; jasmine.clock().install(); jasmine.clock().mockDate(); }); @@ -1438,6 +1442,66 @@ describe('StreamingEngine', () => { }); }); + it('into unbuffered regions when nothing is buffered ' + + 'and mediaState is performing an update', async () => { + // Here we go! + streamingEngine.switchVariant(variant); + streamingEngine.switchTextStream(textStream); + // ensure init source buffer promise does not resolve before seeked() + // so mediaState remains in "performingUpdate" state + const initSourceBufferPromise = new shaka.util.PublicPromise(); + mediaSourceEngine.setStreamProperties.and + .returnValue(initSourceBufferPromise); + await streamingEngine.start(); + playing = true; + + // tick to trigger mediaState updates + jasmine.clock().tick(1); + // give a chance for fetchAndAppend_ to be invoked + // by async onUpdate_ callback + await Util.shortDelay(realSetTimeout); + + // Nothing is buffered yet. + expect(mediaSourceEngine.segments).toEqual({ + audio: [false, false, false, false], + video: [false, false, false, false], + text: [false, false, false, false], + }); + + // Seek forward to an unbuffered region in the first Period. + presentationTimeInSeconds = 15; + streamingEngine.seeked(); + + // resolve initSourceBufferPromise after seeked(), waitingToClearBuffer + // should have been set, so this will now trigger the actual flush of + // the buffer + initSourceBufferPromise.resolve(); + mediaSourceEngine.setStreamProperties.and + .returnValue(Promise.resolve()); + + // allow mediaState update to resolve + await Util.shortDelay(realSetTimeout); + + onTick.and.callFake(() => { + expect(mediaSourceEngine.clear).toHaveBeenCalled(); + onTick.and.stub(); + }); + + await runTest(Util.spyFunc(onTick)); + + // Verify buffers. + expect(mediaSourceEngine.initSegments).toEqual({ + audio: [false, true], + video: [false, true], + text: [], + }); + expect(mediaSourceEngine.segments).toEqual({ + audio: [false, true, true, true], + video: [false, true, true, true], + text: [false, true, true, true], + }); + }); + it('into unbuffered regions near segment start', async () => { // Here we go! streamingEngine.switchVariant(variant);