Commit 79b688ef by tonihei Committed by microkatz

Advance position across transition for readahead renderer error

If a renderer error happens while processing readahead data for the
next item in the playlist, we currently throw the error immediately
and only set the item id in the error details. This makes it harder
to associate the error to the right item. For example, the user
facing UI is likely not updated to show the failing item when the
error is reported.

This can be improved slighly by force setting the position to the
failing item. The playback still fails immediately, but this can't
be avoided because the renderer itself went into an error state.

PiperOrigin-RevId: 507808635
parent 08342ea9
...@@ -591,6 +591,23 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -591,6 +591,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
e = pendingRecoverableRendererError; e = pendingRecoverableRendererError;
} }
Log.e(TAG, "Playback error", e); Log.e(TAG, "Playback error", e);
if (e.type == ExoPlaybackException.TYPE_RENDERER
&& queue.getPlayingPeriod() != queue.getReadingPeriod()) {
// We encountered a renderer error while reading ahead. Force-update the playback position
// to the failing item to ensure the user-visible error is reported after the transition.
while (queue.getPlayingPeriod() != queue.getReadingPeriod()) {
queue.advancePlayingPeriod();
}
MediaPeriodHolder newPlayingPeriodHolder = checkNotNull(queue.getPlayingPeriod());
playbackInfo =
handlePositionDiscontinuity(
newPlayingPeriodHolder.info.id,
newPlayingPeriodHolder.info.startPositionUs,
newPlayingPeriodHolder.info.requestedContentPositionUs,
/* discontinuityStartPositionUs= */ newPlayingPeriodHolder.info.startPositionUs,
/* reportDiscontinuity= */ true,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
}
stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false); stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false);
playbackInfo = playbackInfo.copyWithPlaybackError(e); playbackInfo = playbackInfo.copyWithPlaybackError(e);
} }
......
...@@ -50,6 +50,7 @@ import static com.google.android.exoplayer2.Player.STATE_ENDED; ...@@ -50,6 +50,7 @@ import static com.google.android.exoplayer2.Player.STATE_ENDED;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilError;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
...@@ -88,6 +89,7 @@ import android.content.Intent; ...@@ -88,6 +89,7 @@ import android.content.Intent;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -99,9 +101,11 @@ import com.google.android.exoplayer2.Player.PositionInfo; ...@@ -99,9 +101,11 @@ import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
...@@ -126,6 +130,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; ...@@ -126,6 +130,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
import com.google.android.exoplayer2.testutil.FakeAdaptiveDataSet; import com.google.android.exoplayer2.testutil.FakeAdaptiveDataSet;
import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource; import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource;
import com.google.android.exoplayer2.testutil.FakeAudioRenderer;
import com.google.android.exoplayer2.testutil.FakeChunkSource; import com.google.android.exoplayer2.testutil.FakeChunkSource;
import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource;
...@@ -143,6 +148,7 @@ import com.google.android.exoplayer2.testutil.FakeTrackSelection; ...@@ -143,6 +148,7 @@ import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.testutil.FakeTrackSelector;
import com.google.android.exoplayer2.testutil.FakeVideoRenderer; import com.google.android.exoplayer2.testutil.FakeVideoRenderer;
import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder; import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocation;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
...@@ -150,9 +156,11 @@ import com.google.android.exoplayer2.upstream.Loader; ...@@ -150,9 +156,11 @@ import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.SystemClock; import com.google.android.exoplayer2.util.SystemClock;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Range; import com.google.common.collect.Range;
...@@ -9732,13 +9740,16 @@ public final class ExoPlayerTest { ...@@ -9732,13 +9740,16 @@ public final class ExoPlayerTest {
FakeMediaSource source1 = FakeMediaSource source1 =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT); new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT);
RenderersFactory renderersFactory = RenderersFactory renderersFactory =
(eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> (eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> {
new Renderer[] { HandlerWrapper handler =
new FakeRenderer(C.TRACK_TYPE_VIDEO), SystemClock.DEFAULT.createHandler(eventHandler.getLooper(), /* callback= */ null);
new FakeRenderer(C.TRACK_TYPE_AUDIO) { return new Renderer[] {
new FakeVideoRenderer(handler, videoListener),
new FakeAudioRenderer(handler, audioListener) {
@Override @Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException { throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
// Fail when enabling the renderer. This will happen during the period // Fail when enabling the renderer. This will happen during the period
// transition while the reading and playing period are different. // transition while the reading and playing period are different.
throw createRendererException( throw createRendererException(
...@@ -9748,8 +9759,11 @@ public final class ExoPlayerTest { ...@@ -9748,8 +9759,11 @@ public final class ExoPlayerTest {
} }
} }
}; };
};
ExoPlayer player = ExoPlayer player =
new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build(); new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build();
AnalyticsListener mockListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockListener);
player.setMediaSources(ImmutableList.of(source0, source1)); player.setMediaSources(ImmutableList.of(source0, source1));
player.prepare(); player.prepare();
player.play(); player.play();
...@@ -9762,8 +9776,12 @@ public final class ExoPlayerTest { ...@@ -9762,8 +9776,12 @@ public final class ExoPlayerTest {
.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true) .getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true)
.uid; .uid;
assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid); assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid);
// Verify test setup by checking that playing period was indeed different. // Verify test setup by checking that enabling the renderer happened before the transition.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); InOrder inOrderEvents = inOrder(mockListener);
inOrderEvents.verify(mockListener).onAudioEnabled(any(), any());
inOrderEvents
.verify(mockListener)
.onMediaItemTransition(any(), any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
} }
@Test @Test
...@@ -12219,6 +12237,84 @@ public final class ExoPlayerTest { ...@@ -12219,6 +12237,84 @@ public final class ExoPlayerTest {
// Assert that playing works without getting stuck due to the memory used by the back buffer. // Assert that playing works without getting stuck due to the memory used by the back buffer.
} }
@Test
public void rendererError_whileReadingAhead_isReportedAfterMediaItemTransition()
throws Exception {
// Throw an exception as soon as we try to process a buffer for the second item. This happens
// while the player is still playing the first item.
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderersFactory(
new RenderersFactory() {
@Override
public Renderer[] createRenderers(
Handler handler,
VideoRendererEventListener videoListener,
AudioRendererEventListener audioListener,
TextOutput textOutput,
MetadataOutput metadataOutput) {
return new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener) {
int streamChangeCount = 0;
@Override
protected void onStreamChanged(
Format[] formats, long startPositionUs, long offsetUs)
throws ExoPlaybackException {
super.onStreamChanged(formats, startPositionUs, offsetUs);
streamChangeCount++;
}
@Override
protected boolean shouldProcessBuffer(
long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess =
super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
if (streamChangeCount == 2 && shouldProcess) {
Util.sneakyThrow(
createRendererException(
new IllegalStateException(),
/* format= */ null,
PlaybackException.ERROR_CODE_DECODING_FAILED));
}
return shouldProcess;
}
}
};
}
})
.build();
AnalyticsListener mockListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockListener);
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
player.prepare();
player.play();
runUntilError(player);
int mediaItemIndexAfterError = player.getCurrentMediaItemIndex();
player.release();
assertThat(mediaItemIndexAfterError).isEqualTo(1);
InOrder eventsInOrder = inOrder(mockListener);
// Verify the test setup by checking that the renderer format change happened before the
// position discontinuity.
eventsInOrder.verify(mockListener, times(2)).onDownstreamFormatChanged(any(), any());
eventsInOrder
.verify(mockListener)
.onPositionDiscontinuity(
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
eventsInOrder
.verify(mockListener)
.onMediaItemTransition(any(), any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
eventsInOrder.verify(mockListener).onPlayerError(any(), any());
}
// Internal methods. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment