Skip to content

Commit 79b688e

Browse files
toniheimicrokatz
authored andcommitted
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
1 parent 08342ea commit 79b688e

File tree

2 files changed

+130
-17
lines changed

2 files changed

+130
-17
lines changed

library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,23 @@ public boolean handleMessage(Message msg) {
591591
e = pendingRecoverableRendererError;
592592
}
593593
Log.e(TAG, "Playback error", e);
594+
if (e.type == ExoPlaybackException.TYPE_RENDERER
595+
&& queue.getPlayingPeriod() != queue.getReadingPeriod()) {
596+
// We encountered a renderer error while reading ahead. Force-update the playback position
597+
// to the failing item to ensure the user-visible error is reported after the transition.
598+
while (queue.getPlayingPeriod() != queue.getReadingPeriod()) {
599+
queue.advancePlayingPeriod();
600+
}
601+
MediaPeriodHolder newPlayingPeriodHolder = checkNotNull(queue.getPlayingPeriod());
602+
playbackInfo =
603+
handlePositionDiscontinuity(
604+
newPlayingPeriodHolder.info.id,
605+
newPlayingPeriodHolder.info.startPositionUs,
606+
newPlayingPeriodHolder.info.requestedContentPositionUs,
607+
/* discontinuityStartPositionUs= */ newPlayingPeriodHolder.info.startPositionUs,
608+
/* reportDiscontinuity= */ true,
609+
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
610+
}
594611
stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false);
595612
playbackInfo = playbackInfo.copyWithPlaybackError(e);
596613
}

library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
5151
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition;
5252
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
53+
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilError;
5354
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
5455
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
5556
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
@@ -88,6 +89,7 @@
8889
import android.graphics.SurfaceTexture;
8990
import android.media.AudioManager;
9091
import android.net.Uri;
92+
import android.os.Handler;
9193
import android.os.Looper;
9294
import android.view.Surface;
9395
import androidx.annotation.Nullable;
@@ -99,9 +101,11 @@
99101
import com.google.android.exoplayer2.Timeline.Window;
100102
import com.google.android.exoplayer2.analytics.AnalyticsListener;
101103
import com.google.android.exoplayer2.audio.AudioAttributes;
104+
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
102105
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
103106
import com.google.android.exoplayer2.drm.DrmSessionManager;
104107
import com.google.android.exoplayer2.metadata.Metadata;
108+
import com.google.android.exoplayer2.metadata.MetadataOutput;
105109
import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
106110
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
107111
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
@@ -126,6 +130,7 @@
126130
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
127131
import com.google.android.exoplayer2.testutil.FakeAdaptiveDataSet;
128132
import com.google.android.exoplayer2.testutil.FakeAdaptiveMediaSource;
133+
import com.google.android.exoplayer2.testutil.FakeAudioRenderer;
129134
import com.google.android.exoplayer2.testutil.FakeChunkSource;
130135
import com.google.android.exoplayer2.testutil.FakeClock;
131136
import com.google.android.exoplayer2.testutil.FakeDataSource;
@@ -143,16 +148,19 @@
143148
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
144149
import com.google.android.exoplayer2.testutil.FakeVideoRenderer;
145150
import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder;
151+
import com.google.android.exoplayer2.text.TextOutput;
146152
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
147153
import com.google.android.exoplayer2.upstream.Allocation;
148154
import com.google.android.exoplayer2.upstream.Allocator;
149155
import com.google.android.exoplayer2.upstream.Loader;
150156
import com.google.android.exoplayer2.upstream.TransferListener;
151157
import com.google.android.exoplayer2.util.Assertions;
152158
import com.google.android.exoplayer2.util.Clock;
159+
import com.google.android.exoplayer2.util.HandlerWrapper;
153160
import com.google.android.exoplayer2.util.MimeTypes;
154161
import com.google.android.exoplayer2.util.SystemClock;
155162
import com.google.android.exoplayer2.util.Util;
163+
import com.google.android.exoplayer2.video.VideoRendererEventListener;
156164
import com.google.common.collect.ImmutableList;
157165
import com.google.common.collect.Iterables;
158166
import com.google.common.collect.Range;
@@ -9732,24 +9740,30 @@ public void rendererError_isReportedWithReadingMediaPeriodId() throws Exception
97329740
FakeMediaSource source1 =
97339741
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT);
97349742
RenderersFactory renderersFactory =
9735-
(eventHandler, videoListener, audioListener, textOutput, metadataOutput) ->
9736-
new Renderer[] {
9737-
new FakeRenderer(C.TRACK_TYPE_VIDEO),
9738-
new FakeRenderer(C.TRACK_TYPE_AUDIO) {
9739-
@Override
9740-
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
9741-
throws ExoPlaybackException {
9742-
// Fail when enabling the renderer. This will happen during the period
9743-
// transition while the reading and playing period are different.
9744-
throw createRendererException(
9745-
new IllegalStateException(),
9746-
ExoPlayerTestRunner.AUDIO_FORMAT,
9747-
PlaybackException.ERROR_CODE_UNSPECIFIED);
9748-
}
9743+
(eventHandler, videoListener, audioListener, textOutput, metadataOutput) -> {
9744+
HandlerWrapper handler =
9745+
SystemClock.DEFAULT.createHandler(eventHandler.getLooper(), /* callback= */ null);
9746+
return new Renderer[] {
9747+
new FakeVideoRenderer(handler, videoListener),
9748+
new FakeAudioRenderer(handler, audioListener) {
9749+
@Override
9750+
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
9751+
throws ExoPlaybackException {
9752+
super.onEnabled(joining, mayRenderStartOfStream);
9753+
// Fail when enabling the renderer. This will happen during the period
9754+
// transition while the reading and playing period are different.
9755+
throw createRendererException(
9756+
new IllegalStateException(),
9757+
ExoPlayerTestRunner.AUDIO_FORMAT,
9758+
PlaybackException.ERROR_CODE_UNSPECIFIED);
97499759
}
9750-
};
9760+
}
9761+
};
9762+
};
97519763
ExoPlayer player =
97529764
new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build();
9765+
AnalyticsListener mockListener = mock(AnalyticsListener.class);
9766+
player.addAnalyticsListener(mockListener);
97539767
player.setMediaSources(ImmutableList.of(source0, source1));
97549768
player.prepare();
97559769
player.play();
@@ -9762,8 +9776,12 @@ protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
97629776
.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true)
97639777
.uid;
97649778
assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid);
9765-
// Verify test setup by checking that playing period was indeed different.
9766-
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
9779+
// Verify test setup by checking that enabling the renderer happened before the transition.
9780+
InOrder inOrderEvents = inOrder(mockListener);
9781+
inOrderEvents.verify(mockListener).onAudioEnabled(any(), any());
9782+
inOrderEvents
9783+
.verify(mockListener)
9784+
.onMediaItemTransition(any(), any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
97679785
}
97689786

97699787
@Test
@@ -12219,6 +12237,84 @@ public void loadControlBackBuffer_withInsufficientMemoryLimits_stillContinuesPla
1221912237
// Assert that playing works without getting stuck due to the memory used by the back buffer.
1222012238
}
1222112239

12240+
@Test
12241+
public void rendererError_whileReadingAhead_isReportedAfterMediaItemTransition()
12242+
throws Exception {
12243+
// Throw an exception as soon as we try to process a buffer for the second item. This happens
12244+
// while the player is still playing the first item.
12245+
ExoPlayer player =
12246+
new TestExoPlayerBuilder(context)
12247+
.setRenderersFactory(
12248+
new RenderersFactory() {
12249+
@Override
12250+
public Renderer[] createRenderers(
12251+
Handler handler,
12252+
VideoRendererEventListener videoListener,
12253+
AudioRendererEventListener audioListener,
12254+
TextOutput textOutput,
12255+
MetadataOutput metadataOutput) {
12256+
return new Renderer[] {
12257+
new FakeVideoRenderer(
12258+
SystemClock.DEFAULT.createHandler(
12259+
handler.getLooper(), /* callback= */ null),
12260+
videoListener) {
12261+
int streamChangeCount = 0;
12262+
12263+
@Override
12264+
protected void onStreamChanged(
12265+
Format[] formats, long startPositionUs, long offsetUs)
12266+
throws ExoPlaybackException {
12267+
super.onStreamChanged(formats, startPositionUs, offsetUs);
12268+
streamChangeCount++;
12269+
}
12270+
12271+
@Override
12272+
protected boolean shouldProcessBuffer(
12273+
long bufferTimeUs, long playbackPositionUs) {
12274+
boolean shouldProcess =
12275+
super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
12276+
if (streamChangeCount == 2 && shouldProcess) {
12277+
Util.sneakyThrow(
12278+
createRendererException(
12279+
new IllegalStateException(),
12280+
/* format= */ null,
12281+
PlaybackException.ERROR_CODE_DECODING_FAILED));
12282+
}
12283+
return shouldProcess;
12284+
}
12285+
}
12286+
};
12287+
}
12288+
})
12289+
.build();
12290+
AnalyticsListener mockListener = mock(AnalyticsListener.class);
12291+
player.addAnalyticsListener(mockListener);
12292+
12293+
player.setMediaSources(
12294+
ImmutableList.of(
12295+
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
12296+
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
12297+
player.prepare();
12298+
player.play();
12299+
runUntilError(player);
12300+
int mediaItemIndexAfterError = player.getCurrentMediaItemIndex();
12301+
player.release();
12302+
12303+
assertThat(mediaItemIndexAfterError).isEqualTo(1);
12304+
InOrder eventsInOrder = inOrder(mockListener);
12305+
// Verify the test setup by checking that the renderer format change happened before the
12306+
// position discontinuity.
12307+
eventsInOrder.verify(mockListener, times(2)).onDownstreamFormatChanged(any(), any());
12308+
eventsInOrder
12309+
.verify(mockListener)
12310+
.onPositionDiscontinuity(
12311+
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
12312+
eventsInOrder
12313+
.verify(mockListener)
12314+
.onMediaItemTransition(any(), any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
12315+
eventsInOrder.verify(mockListener).onPlayerError(any(), any());
12316+
}
12317+
1222212318
// Internal methods.
1222312319

1222412320
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {

0 commit comments

Comments
 (0)