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 PreloadConfiguration preloadConfiguration;
|
||||||
private Timeline lastPreloadPoolInvalidationTimeline;
|
private Timeline lastPreloadPoolInvalidationTimeline;
|
||||||
private long prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
|
private long prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
|
||||||
|
private boolean isPrewarmingDisabledUntilNextTransition;
|
||||||
|
|
||||||
public ExoPlayerImplInternal(
|
public ExoPlayerImplInternal(
|
||||||
Renderer[] renderers,
|
Renderer[] renderers,
|
||||||
@ -298,7 +299,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
rendererCapabilities[i].setListener(rendererCapabilitiesListener);
|
rendererCapabilities[i].setListener(rendererCapabilitiesListener);
|
||||||
}
|
}
|
||||||
if (secondaryRenderers[i] != null) {
|
if (secondaryRenderers[i] != null) {
|
||||||
secondaryRenderers[i].init(/* index= */ i, playerId, clock);
|
secondaryRenderers[i].init(/* index= */ i + renderers.length, playerId, clock);
|
||||||
hasSecondaryRenderers = true;
|
hasSecondaryRenderers = true;
|
||||||
}
|
}
|
||||||
this.renderers[i] = new RendererHolder(renderers[i], secondaryRenderers[i], /* index= */ i);
|
this.renderers[i] = new RendererHolder(renderers[i], secondaryRenderers[i], /* index= */ i);
|
||||||
@ -678,7 +679,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
if (readingPeriod != null) {
|
if (readingPeriod != null) {
|
||||||
// We can assume that all renderer errors happen in the context of the reading period. See
|
// 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.
|
// [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
|
if (e.isRecoverable
|
||||||
@ -699,6 +706,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
// recovered or the player stopped before any other message is handled.
|
// recovered or the player stopped before any other message is handled.
|
||||||
handler.sendMessageAtFrontOfQueue(
|
handler.sendMessageAtFrontOfQueue(
|
||||||
handler.obtainMessage(MSG_ATTEMPT_RENDERER_ERROR_RECOVERY, e));
|
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 {
|
} else {
|
||||||
if (pendingRecoverableRendererError != null) {
|
if (pendingRecoverableRendererError != null) {
|
||||||
pendingRecoverableRendererError.addSuppressed(e);
|
pendingRecoverableRendererError.addSuppressed(e);
|
||||||
@ -2336,7 +2362,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
|
|
||||||
private void maybeUpdatePrewarmingPeriod() throws ExoPlaybackException {
|
private void maybeUpdatePrewarmingPeriod() throws ExoPlaybackException {
|
||||||
// TODO: Add limit as to not enable waiting renderer too early
|
// TODO: Add limit as to not enable waiting renderer too early
|
||||||
if (pendingPauseAtEndOfPeriod || !hasSecondaryRenderers || areRenderersPrewarming()) {
|
if (pendingPauseAtEndOfPeriod
|
||||||
|
|| !hasSecondaryRenderers
|
||||||
|
|| isPrewarmingDisabledUntilNextTransition
|
||||||
|
|| areRenderersPrewarming()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@Nullable MediaPeriodHolder prewarmingPeriodHolder = queue.getPrewarmingPeriod();
|
@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 new period starts with a discontinuity, so unless a pre-warming renderer is handling
|
||||||
// the discontinuity, the renderers will play out all data, then
|
// the discontinuity, the renderers will play out all data, then
|
||||||
// be disabled and re-enabled when they start playing the next period.
|
// be disabled and re-enabled when they start playing the next period.
|
||||||
boolean arePrewarmingRenderersHandlingDiscontinuity = hasSecondaryRenderers;
|
boolean arePrewarmingRenderersHandlingDiscontinuity =
|
||||||
|
hasSecondaryRenderers && !isPrewarmingDisabledUntilNextTransition;
|
||||||
if (arePrewarmingRenderersHandlingDiscontinuity) {
|
if (arePrewarmingRenderersHandlingDiscontinuity) {
|
||||||
for (int i = 0; i < renderers.length; i++) {
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
if (!newTrackSelectorResult.isRendererEnabled(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.
|
// If we advance more than one period at a time, notify listeners after each update.
|
||||||
maybeNotifyPlaybackInfoChanged();
|
maybeNotifyPlaybackInfoChanged();
|
||||||
}
|
}
|
||||||
|
isPrewarmingDisabledUntilNextTransition = false;
|
||||||
MediaPeriodHolder newPlayingPeriodHolder = checkNotNull(queue.advancePlayingPeriod());
|
MediaPeriodHolder newPlayingPeriodHolder = checkNotNull(queue.advancePlayingPeriod());
|
||||||
boolean isCancelledSSAIAdTransition =
|
boolean isCancelledSSAIAdTransition =
|
||||||
playbackInfo.periodId.periodUid.equals(newPlayingPeriodHolder.info.id.periodUid)
|
playbackInfo.periodId.periodUid.equals(newPlayingPeriodHolder.info.id.periodUid)
|
||||||
|
@ -80,6 +80,12 @@ import java.util.Objects;
|
|||||||
return isPrimaryRendererPrewarming() || isSecondaryRendererPrewarming();
|
return isPrimaryRendererPrewarming() || isSecondaryRendererPrewarming();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRendererPrewarming(int id) {
|
||||||
|
boolean isPrewarmingPrimaryRenderer = isPrimaryRendererPrewarming() && id == index;
|
||||||
|
boolean isPrewarmingSecondaryRenderer = isSecondaryRendererPrewarming() && id != index;
|
||||||
|
return isPrewarmingPrimaryRenderer || isPrewarmingSecondaryRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isPrimaryRendererPrewarming() {
|
private boolean isPrimaryRendererPrewarming() {
|
||||||
return prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY
|
return prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY
|
||||||
|| prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_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.END_OF_STREAM_ITEM;
|
||||||
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
|
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.run;
|
||||||
|
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
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.content.Context;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@ -27,6 +33,7 @@ import android.util.Pair;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.common.util.Clock;
|
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.audio.AudioRendererEventListener;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
|
||||||
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
||||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||||
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
||||||
@ -60,6 +68,7 @@ import androidx.test.core.app.ApplicationProvider;
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@ -1048,6 +1057,417 @@ public class ExoPlayerWithPrewarmingRenderersTest {
|
|||||||
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_STARTED);
|
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. */
|
/** {@link FakeMediaSource} that prevents any reading of samples off the sample queue. */
|
||||||
private static final class FakeBlockingMediaSource extends FakeMediaSource {
|
private static final class FakeBlockingMediaSource extends FakeMediaSource {
|
||||||
|
|
||||||
@ -1145,4 +1565,49 @@ public class ExoPlayerWithPrewarmingRenderersTest {
|
|||||||
return null;
|
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