Implement error handling support for pre-warming renderers

PiperOrigin-RevId: 704408379
This commit is contained in:
michaelkatz 2024-12-09 14:00:52 -08:00 committed by Copybara-Service
parent be63e156bb
commit 6689fee2b2
3 changed files with 506 additions and 4 deletions

View File

@ -235,6 +235,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private PreloadConfiguration preloadConfiguration;
private Timeline lastPreloadPoolInvalidationTimeline;
private long prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
private boolean isPrewarmingDisabledUntilNextTransition;
public ExoPlayerImplInternal(
Renderer[] renderers,
@ -298,7 +299,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
rendererCapabilities[i].setListener(rendererCapabilitiesListener);
}
if (secondaryRenderers[i] != null) {
secondaryRenderers[i].init(/* index= */ i, playerId, clock);
secondaryRenderers[i].init(/* index= */ i + renderers.length, playerId, clock);
hasSecondaryRenderers = true;
}
this.renderers[i] = new RendererHolder(renderers[i], secondaryRenderers[i], /* index= */ i);
@ -678,7 +679,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (readingPeriod != null) {
// We can assume that all renderer errors happen in the context of the reading period. See
// [internal: b/150584930#comment4] for exceptions that aren't covered by this assumption.
e = e.copyWithMediaPeriodId(readingPeriod.info.id);
e =
e.copyWithMediaPeriodId(
(renderers[e.rendererIndex % renderers.length].isRendererPrewarming(
e.rendererIndex)
&& readingPeriod.getNext() != null)
? readingPeriod.getNext().info.id
: readingPeriod.info.id);
}
}
if (e.isRecoverable
@ -699,6 +706,25 @@ 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);
@ -2336,7 +2362,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void maybeUpdatePrewarmingPeriod() throws ExoPlaybackException {
// TODO: Add limit as to not enable waiting renderer too early
if (pendingPauseAtEndOfPeriod || !hasSecondaryRenderers || areRenderersPrewarming()) {
if (pendingPauseAtEndOfPeriod
|| !hasSecondaryRenderers
|| isPrewarmingDisabledUntilNextTransition
|| areRenderersPrewarming()) {
return;
}
@Nullable MediaPeriodHolder prewarmingPeriodHolder = queue.getPrewarmingPeriod();
@ -2446,7 +2475,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
// The new period starts with a discontinuity, so unless a pre-warming renderer is handling
// the discontinuity, the renderers will play out all data, then
// be disabled and re-enabled when they start playing the next period.
boolean arePrewarmingRenderersHandlingDiscontinuity = hasSecondaryRenderers;
boolean arePrewarmingRenderersHandlingDiscontinuity =
hasSecondaryRenderers && !isPrewarmingDisabledUntilNextTransition;
if (arePrewarmingRenderersHandlingDiscontinuity) {
for (int i = 0; i < renderers.length; i++) {
if (!newTrackSelectorResult.isRendererEnabled(i)) {
@ -2580,6 +2610,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// If we advance more than one period at a time, notify listeners after each update.
maybeNotifyPlaybackInfoChanged();
}
isPrewarmingDisabledUntilNextTransition = false;
MediaPeriodHolder newPlayingPeriodHolder = checkNotNull(queue.advancePlayingPeriod());
boolean isCancelledSSAIAdTransition =
playbackInfo.periodId.periodUid.equals(newPlayingPeriodHolder.info.id.periodUid)

View File

@ -80,6 +80,12 @@ import java.util.Objects;
return isPrimaryRendererPrewarming() || isSecondaryRendererPrewarming();
}
public boolean isRendererPrewarming(int id) {
boolean isPrewarmingPrimaryRenderer = isPrimaryRendererPrewarming() && id == index;
boolean isPrewarmingSecondaryRenderer = isSecondaryRendererPrewarming() && id != index;
return isPrewarmingPrimaryRenderer || isPrewarmingSecondaryRenderer;
}
private boolean isPrimaryRendererPrewarming() {
return prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY
|| prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY;

View File

@ -19,7 +19,13 @@ import static androidx.media3.common.Player.REPEAT_MODE_ONE;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.os.Handler;
@ -27,6 +33,7 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Clock;
@ -36,6 +43,7 @@ import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
@ -60,6 +68,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Rule;
@ -1048,6 +1057,417 @@ public class ExoPlayerWithPrewarmingRenderersTest {
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_STARTED);
}
@Test
public void
play_errorByPrewarmingSecondaryRendererBeforeAdvancingReadingPeriod_doesNotResetPrimaryRenderer()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean attemptedRenderWithSecondaryRenderer = new AtomicBoolean(false);
AtomicBoolean shouldSecondaryRendererThrow = new AtomicBoolean(true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows(
fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow))
.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(
// Use FakeBlockingMediaSource so that reading period is not advanced when pre-warming.
new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
// Play a bit until the second renderer is enabled and throws errors.
run(player).untilState(Player.STATE_READY);
player.play();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState = videoRenderer.getState();
@Renderer.State int secondaryVideoState = secondaryVideoRenderer.getState();
player.release();
assertThat(attemptedRenderWithSecondaryRenderer.get()).isTrue();
verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(videoState).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState).isEqualTo(Renderer.STATE_DISABLED);
}
@Test
public void
play_errorByPrewarmingSecondaryRendererAfterAdvancingReadingPeriod_doesNotResetPrimaryRenderer()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean attemptedRenderWithSecondaryRenderer = new AtomicBoolean(false);
AtomicBoolean shouldSecondaryRendererThrow = new AtomicBoolean(true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows(
fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow))
.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 FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
// Play a bit until the second renderer is enabled and throws error.
run(player).untilState(Player.STATE_READY);
player.play();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
assertThat(attemptedRenderWithSecondaryRenderer.get()).isTrue();
attemptedRenderWithSecondaryRenderer.set(false);
// Play a bit so that primary renderer is enabled on second media item.
run(player).untilStartOfMediaItem(/* mediaItemIndex= */ 1);
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
player.release();
verify(listener).onPositionDiscontinuity(any(), any(), anyInt());
// Secondary renderer will not be used subsequently after failure.
assertThat(attemptedRenderWithSecondaryRenderer.get()).isFalse();
assertThat(videoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_DISABLED);
assertThat(videoState2).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_DISABLED);
}
@Test
public void play_errorByPrewarmingSecondaryRenderer_primaryRendererIsUsedOnSubsequentMediaItem()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean attemptedRenderWithSecondaryRenderer = new AtomicBoolean(false);
AtomicBoolean shouldSecondaryRendererThrow = new AtomicBoolean(true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows(
fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow))
.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 FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
// Play a bit until the second renderer is enabled and throws error.
run(player).untilState(Player.STATE_READY);
player.play();
run(player).untilBackgroundThreadCondition(attemptedRenderWithSecondaryRenderer::get);
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
assertThat(attemptedRenderWithSecondaryRenderer.get()).isTrue();
shouldSecondaryRendererThrow.set(false);
run(player).untilStartOfMediaItem(/* mediaItemIndex= */ 1);
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
player.release();
verify(listener).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(videoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_DISABLED);
assertThat(videoState2).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_DISABLED);
}
@Test
public void
play_withSecondaryRendererNonRecoverableErrorForMultipleMediaItems_primaryRendererIsUsed()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean attemptedRenderWithSecondaryRenderer = new AtomicBoolean(false);
AtomicBoolean shouldSecondaryRendererThrow = new AtomicBoolean(true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows(
fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow))
.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 FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
// Play a bit until the second renderer is started.
run(player).untilState(Player.STATE_READY);
player.play();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
assertThat(attemptedRenderWithSecondaryRenderer.get()).isTrue();
run(player).untilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 500);
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
shouldSecondaryRendererThrow.set(false);
run(player).untilPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 500);
run(player).untilPendingCommandsAreFullyHandled();
@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_DISABLED);
assertThat(videoState2).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_DISABLED);
assertThat(videoState3).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_ENABLED);
}
@Test
public void play_errorWithPrimaryRendererDuringPrewarming_doesNotResetSecondaryRenderer()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean shouldPrimaryRendererThrow = 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 (!shouldPrimaryRendererThrow.get()) {
super.render(positionUs, elapsedRealtimeUs);
} else {
throw createRendererException(
new MediaCodecRenderer.DecoderInitializationException(
new Format.Builder().build(),
new IllegalArgumentException(),
false,
0),
this.getFormatHolder().format,
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();
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
run(player)
.untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
shouldPrimaryRendererThrow.set(true);
run(player)
.untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_DISABLED);
@Renderer.State int videoState3 = videoRenderer.getState();
@Renderer.State int secondaryVideoState3 = secondaryVideoRenderer.getState();
player.release();
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);
}
@Test
public void
play_errorWithPrimaryWhilePrewarmingSecondaryPriorToAdvancingReadingPeriod_restartingPlaybackWillUseSecondaryRenderer()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean shouldPrimaryRendererThrow = 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 (!shouldPrimaryRendererThrow.get()) {
super.render(positionUs, elapsedRealtimeUs);
} else {
throw createRendererException(
new MediaCodecRenderer.DecoderInitializationException(
new Format.Builder().build(),
new IllegalArgumentException(),
false,
0),
this.getFormatHolder().format,
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(
// Use FakeBlockingMediaSource so that reading period is not advanced when pre-warming.
new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
// Play a bit until the second renderer is enabled.
run(player).untilState(Player.STATE_READY);
player.play();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
// Force primary renderer to error, killing playback.
shouldPrimaryRendererThrow.set(true);
run(player).untilPlayerError();
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
// Restart playback with primary renderer functioning properly.
shouldPrimaryRendererThrow.set(false);
player.prepare();
player.play();
run(player).untilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 500);
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState3 = videoRenderer.getState();
@Renderer.State int secondaryVideoState3 = secondaryVideoRenderer.getState();
player.release();
verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(videoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_ENABLED);
assertThat(videoState2).isEqualTo(Renderer.STATE_DISABLED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_DISABLED);
assertThat(videoState3).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_ENABLED);
}
@Test
public void
play_errorWithSecondaryWhilePrewarmingPrimaryPriorToAdvancingReadingPeriod_restartingPlaybackWillPrewarmSecondaryRenderer()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
Player.Listener listener = mock(Player.Listener.class);
AtomicBoolean attemptedRenderWithSecondaryRenderer = new AtomicBoolean(false);
AtomicBoolean shouldSecondaryRendererThrow = new AtomicBoolean(false);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows(
fakeClock, attemptedRenderWithSecondaryRenderer, shouldSecondaryRendererThrow))
.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),
// Use FakeBlockingMediaSource so that reading period is not advanced when pre-warming.
new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
// Play a bit until on second media item and the primary renderer is pre-warming.
run(player).untilStartOfMediaItem(/* mediaItemIndex= */ 1);
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
// Force secondary renderer to error, killing playback.
shouldSecondaryRendererThrow.set(true);
runUntilError(player);
// Restart playback with secondary renderer functioning properly.
shouldSecondaryRendererThrow.set(false);
player.prepare();
// Play until secondary renderer is pre-warming.
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
player.release();
verify(listener).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(videoState1).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(videoState2).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_ENABLED);
}
/** {@link FakeMediaSource} that prevents any reading of samples off the sample queue. */
private static final class FakeBlockingMediaSource extends FakeMediaSource {
@ -1145,4 +1565,49 @@ public class ExoPlayerWithPrewarmingRenderersTest {
return null;
}
}
private static final class FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows
extends FakeRenderersFactorySupportingSecondaryVideoRenderer {
private final AtomicBoolean attemptedRenderWithSecondaryRenderer;
private final AtomicBoolean shouldSecondaryRendererThrow;
public FakeRenderersFactorySupportingSecondaryVideoRendererThatThrows(
Clock clock,
AtomicBoolean attemptedRenderWithSecondaryRenderer,
AtomicBoolean shouldSecondaryRendererThrow) {
super(clock);
this.attemptedRenderWithSecondaryRenderer = attemptedRenderWithSecondaryRenderer;
this.shouldSecondaryRendererThrow = shouldSecondaryRendererThrow;
}
@Override
public Renderer createSecondaryRenderer(
Renderer renderer,
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
if (renderer instanceof FakeVideoRenderer) {
return new FakeVideoRenderer(
clock.createHandler(eventHandler.getLooper(), /* callback= */ null),
videoRendererEventListener) {
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
attemptedRenderWithSecondaryRenderer.set(true);
if (!shouldSecondaryRendererThrow.get()) {
super.render(positionUs, elapsedRealtimeUs);
} else {
throw createRendererException(
new MediaCodecRenderer.DecoderInitializationException(
new Format.Builder().build(), new IllegalArgumentException(), false, 0),
this.getFormatHolder().format,
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
}
}
};
}
return null;
}
}
}