From 6e510c26df0d354312abe480b238afa47abedd3d 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 --- .../exoplayer/ExoPlayerImplInternal.java | 38 ++++----- .../ExoPlayerWithPrewarmingRenderersTest.java | 85 +++++++++++++++++++ 2 files changed, 104 insertions(+), 19 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 f197a9503e..2e11d35acd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -741,25 +741,7 @@ import java.util.concurrent.atomic.AtomicBoolean; e = e.copyWithMediaPeriodId(readingPeriod.info.id); } } - 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)) { - // If pendingRecoverableRendererError != null and error was - // ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED then upon retry, renderer will attempt with - // offload disabled. - Log.w(TAG, "Recoverable renderer error", e); - if (pendingRecoverableRendererError != null) { - pendingRecoverableRendererError.addSuppressed(e); - e = pendingRecoverableRendererError; - } else { - pendingRecoverableRendererError = e; - } - // Given that the player is now in an unhandled exception state, the error needs to be - // 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 + if (e.type == ExoPlaybackException.TYPE_RENDERER && e.mediaPeriodId != null && isRendererPrewarmingMediaPeriod(e.rendererIndex, e.mediaPeriodId)) { // TODO(b/380273486): Investigate recovery for pre-warming renderer errors @@ -778,6 +760,24 @@ import java.util.concurrent.atomic.AtomicBoolean; 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)) { + // If pendingRecoverableRendererError != null and error was + // ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED then upon retry, renderer will attempt with + // offload disabled. + Log.w(TAG, "Recoverable renderer error", e); + if (pendingRecoverableRendererError != null) { + pendingRecoverableRendererError.addSuppressed(e); + e = pendingRecoverableRendererError; + } else { + pendingRecoverableRendererError = e; + } + // Given that the player is now in an unhandled exception state, the error needs to be + // recovered or the player stopped before any other message is handled. + handler.sendMessageAtFrontOfQueue( + handler.obtainMessage(MSG_ATTEMPT_RENDERER_ERROR_RECOVERY, e)); } 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 {