From ef8f72d684c5cca44d21adb90fd3907364f948f0 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Tue, 25 Mar 2025 08:00:47 -0700 Subject: [PATCH] Set that any error during pre-warming disables and resets pre-warming For now, even if a recoverable error occurs during pre-warming, the current process will be that pre-warming is disabled until subsequent media item transition. PiperOrigin-RevId: 740349517 (cherry picked from commit 6e510c26df0d354312abe480b238afa47abedd3d) --- .../exoplayer/ExoPlayerImplInternal.java | 40 ++++----- .../ExoPlayerWithPrewarmingRenderersTest.java | 85 +++++++++++++++++++ 2 files changed, 105 insertions(+), 20 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 64ace4e059..e0038cb0ee 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -750,7 +750,26 @@ import java.util.concurrent.atomic.AtomicBoolean; : readingPeriod.info.id); } } - if (e.isRecoverable + if (e.type == ExoPlaybackException.TYPE_RENDERER + && e.mediaPeriodId != null + && isRendererPrewarmingMediaPeriod(e.rendererIndex, e.mediaPeriodId)) { + // TODO(b/380273486): Investigate recovery for pre-warming renderer errors + isPrewarmingDisabledUntilNextTransition = true; + disableAndResetPrewarmingRenderers(); + // Remove periods from the queue starting at the pre-warming period. + MediaPeriodHolder prewarmingPeriod = queue.getPrewarmingPeriod(); + MediaPeriodHolder periodToRemoveAfter = queue.getPlayingPeriod(); + if (queue.getPlayingPeriod() != prewarmingPeriod) { + while (periodToRemoveAfter != null && periodToRemoveAfter.getNext() != prewarmingPeriod) { + periodToRemoveAfter = periodToRemoveAfter.getNext(); + } + } + queue.removeAfter(periodToRemoveAfter); + if (playbackInfo.playbackState != Player.STATE_ENDED) { + maybeContinueLoading(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } else if (e.isRecoverable && (pendingRecoverableRendererError == null || e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED || e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED)) { @@ -768,25 +787,6 @@ import java.util.concurrent.atomic.AtomicBoolean; // recovered or the player stopped before any other message is handled. handler.sendMessageAtFrontOfQueue( handler.obtainMessage(MSG_ATTEMPT_RENDERER_ERROR_RECOVERY, e)); - } else if (e.type == ExoPlaybackException.TYPE_RENDERER - && renderers[e.rendererIndex % renderers.length].isRendererPrewarming( - /* id= */ e.rendererIndex)) { - // TODO(b/380273486): Investigate recovery for pre-warming renderer errors - isPrewarmingDisabledUntilNextTransition = true; - disableAndResetPrewarmingRenderers(); - // Remove periods from the queue starting at the pre-warming period. - MediaPeriodHolder prewarmingPeriod = queue.getPrewarmingPeriod(); - MediaPeriodHolder periodToRemoveAfter = queue.getPlayingPeriod(); - if (queue.getPlayingPeriod() != prewarmingPeriod) { - while (periodToRemoveAfter != null && periodToRemoveAfter.getNext() != prewarmingPeriod) { - periodToRemoveAfter = periodToRemoveAfter.getNext(); - } - } - queue.removeAfter(periodToRemoveAfter); - if (playbackInfo.playbackState != Player.STATE_ENDED) { - maybeContinueLoading(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } } else { if (pendingRecoverableRendererError != null) { pendingRecoverableRendererError.addSuppressed(e); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java index 52e794cb27..f6b7c7060e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java @@ -1524,6 +1524,91 @@ public class ExoPlayerWithPrewarmingRenderersTest { assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_ENABLED); } + @Test + public void + play_recoverableErrorWithPrimaryRendererDuringPrewarming_doesNotResetSecondaryRenderer() + throws Exception { + Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); + Player.Listener listener = mock(Player.Listener.class); + AtomicBoolean shouldPrimaryRendererThrowRecoverable = new AtomicBoolean(false); + ExoPlayer player = + new TestExoPlayerBuilder(context) + .setClock(fakeClock) + .setRenderersFactory( + new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock) { + @Override + public Renderer[] createRenderers( + Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + HandlerWrapper clockAwareHandler = + clock.createHandler(eventHandler.getLooper(), /* callback= */ null); + return new Renderer[] { + new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener) { + @Override + public void render(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + if (!shouldPrimaryRendererThrowRecoverable.get()) { + super.render(positionUs, elapsedRealtimeUs); + } else { + shouldPrimaryRendererThrowRecoverable.set(false); + throw createRendererException( + new MediaCodecRenderer.DecoderInitializationException( + new Format.Builder().build(), + new IllegalArgumentException(), + false, + 0), + this.getFormatHolder().format, + true, + PlaybackException.ERROR_CODE_DECODER_INIT_FAILED); + } + } + }, + new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener) + }; + } + }) + .build(); + player.addListener(listener); + Renderer videoRenderer = player.getRenderer(/* index= */ 0); + Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0); + // Set a playlist that allows a new renderer to be enabled early. + player.setMediaSources( + ImmutableList.of( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT))); + player.prepare(); + + // Play a bit until the second renderer is pre-warming. + player.play(); + advance(player) + .untilBackgroundThreadCondition( + () -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED); + @Renderer.State int videoState1 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState(); + advance(player) + .untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_ENABLED); + @Renderer.State int videoState2 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState(); + shouldPrimaryRendererThrowRecoverable.set(true); + advance(player) + .untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_DISABLED); + @Renderer.State int videoState3 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState3 = secondaryVideoRenderer.getState(); + player.release(); + + verify(listener).onPositionDiscontinuity(any(), any(), anyInt()); + assertThat(videoState1).isEqualTo(Renderer.STATE_STARTED); + assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_ENABLED); + assertThat(videoState2).isEqualTo(Renderer.STATE_ENABLED); + assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_STARTED); + assertThat(videoState3).isEqualTo(Renderer.STATE_DISABLED); + assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_STARTED); + } + /** {@link FakeMediaSource} that prevents any reading of samples off the sample queue. */ private static final class FakeBlockingMediaSource extends FakeMediaSource {