mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Implement error handling support for pre-warming renderers
PiperOrigin-RevId: 704408379
This commit is contained in:
parent
be63e156bb
commit
6689fee2b2
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user