Skip to content

Commit aea0637

Browse files
microkatztonihei
authored andcommitted
Fix media period mismatch during recoverable renderer error processing
If a recoverable renderer error occurred just before playing period transition(aka media item transition) then the player may enter a continuous loop of retrying to play the previous media item. This was most easily reproduced in an audio offload scenario where init in offload mode always fails. In initializing the following media, the process would fail with recoverable error to try in non-offload mode. The player would try to recover with playing the previous media item. Most times it would skip to the next track but not always. Issue: #2229 PiperOrigin-RevId: 741213293
1 parent f533f55 commit aea0637

File tree

2 files changed

+100
-22
lines changed

2 files changed

+100
-22
lines changed

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -769,34 +769,17 @@ public boolean handleMessage(Message msg) {
769769
maybeContinueLoading();
770770
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
771771
}
772-
} else if (e.isRecoverable
773-
&& (pendingRecoverableRendererError == null
774-
|| e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED
775-
|| e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED)) {
776-
// If pendingRecoverableRendererError != null and error was
777-
// ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED then upon retry, renderer will attempt with
778-
// offload disabled.
779-
Log.w(TAG, "Recoverable renderer error", e);
780-
if (pendingRecoverableRendererError != null) {
781-
pendingRecoverableRendererError.addSuppressed(e);
782-
e = pendingRecoverableRendererError;
783-
} else {
784-
pendingRecoverableRendererError = e;
785-
}
786-
// Given that the player is now in an unhandled exception state, the error needs to be
787-
// recovered or the player stopped before any other message is handled.
788-
handler.sendMessageAtFrontOfQueue(
789-
handler.obtainMessage(MSG_ATTEMPT_RENDERER_ERROR_RECOVERY, e));
790772
} else {
791773
if (pendingRecoverableRendererError != null) {
792774
pendingRecoverableRendererError.addSuppressed(e);
793775
e = pendingRecoverableRendererError;
794776
}
795-
Log.e(TAG, "Playback error", e);
777+
796778
if (e.type == ExoPlaybackException.TYPE_RENDERER
797779
&& queue.getPlayingPeriod() != queue.getReadingPeriod()) {
798780
// We encountered a renderer error while reading ahead. Force-update the playback position
799-
// to the failing item to ensure the user-visible error is reported after the transition.
781+
// to the failing item to ensure correct retry or that the user-visible error is reported
782+
// after the transition.
800783
while (queue.getPlayingPeriod() != queue.getReadingPeriod()) {
801784
queue.advancePlayingPeriod();
802785
}
@@ -812,8 +795,24 @@ public boolean handleMessage(Message msg) {
812795
/* reportDiscontinuity= */ true,
813796
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
814797
}
815-
stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false);
816-
playbackInfo = playbackInfo.copyWithPlaybackError(e);
798+
799+
if (e.isRecoverable
800+
&& (pendingRecoverableRendererError == null
801+
|| e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED
802+
|| e.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED)) {
803+
// Given that the player is now in an unhandled exception state, the error needs to be
804+
// recovered or the player stopped before any other message is handled.
805+
Log.w(TAG, "Recoverable renderer error", e);
806+
if (pendingRecoverableRendererError == null) {
807+
pendingRecoverableRendererError = e;
808+
}
809+
handler.sendMessageAtFrontOfQueue(
810+
handler.obtainMessage(MSG_ATTEMPT_RENDERER_ERROR_RECOVERY, e));
811+
} else {
812+
Log.e(TAG, "Playback error", e);
813+
stopInternal(/* forceResetRenderers= */ true, /* acknowledgeStop= */ false);
814+
playbackInfo = playbackInfo.copyWithPlaybackError(e);
815+
}
817816
}
818817
} catch (DrmSession.DrmSessionException e) {
819818
handleIoException(e, e.errorCode);

libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
148148
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
149149
import androidx.media3.exoplayer.drm.DrmSessionManager;
150+
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
150151
import androidx.media3.exoplayer.metadata.MetadataOutput;
151152
import androidx.media3.exoplayer.source.ClippingMediaSource;
152153
import androidx.media3.exoplayer.source.ConcatenatingMediaSource;
@@ -16748,6 +16749,84 @@ public void handleMessage(
1674816749
assertThat(videoScalingSetOnSecondaryVideoRenderer.get()).isTrue();
1674916750
}
1675016751

16752+
@Test
16753+
public void
16754+
play_withRecoverableErrorAfterAdvancingReadingPeriod_advancesPlayingPeriodWhileErrorHandling()
16755+
throws Exception {
16756+
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
16757+
AtomicBoolean shouldRendererThrowRecoverableError = new AtomicBoolean(false);
16758+
AtomicInteger onStreamChangedCount = new AtomicInteger(0);
16759+
ExoPlayer player =
16760+
new TestExoPlayerBuilder(context)
16761+
.setClock(fakeClock)
16762+
.setRenderersFactory(
16763+
new RenderersFactory() {
16764+
@Override
16765+
public Renderer[] createRenderers(
16766+
Handler eventHandler,
16767+
VideoRendererEventListener videoRendererEventListener,
16768+
AudioRendererEventListener audioRendererEventListener,
16769+
TextOutput textRendererOutput,
16770+
MetadataOutput metadataRendererOutput) {
16771+
return new Renderer[] {
16772+
new FakeVideoRenderer(
16773+
SystemClock.DEFAULT.createHandler(
16774+
eventHandler.getLooper(), /* callback= */ null),
16775+
videoRendererEventListener) {
16776+
@Override
16777+
protected void onStreamChanged(
16778+
Format[] formats,
16779+
long startPositionUs,
16780+
long offsetUs,
16781+
MediaSource.MediaPeriodId mediaPeriodId)
16782+
throws ExoPlaybackException {
16783+
super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId);
16784+
onStreamChangedCount.getAndIncrement();
16785+
}
16786+
16787+
@Override
16788+
public void render(long positionUs, long elapsedRealtimeUs)
16789+
throws ExoPlaybackException {
16790+
if (!shouldRendererThrowRecoverableError.get()) {
16791+
super.render(positionUs, elapsedRealtimeUs);
16792+
} else {
16793+
shouldRendererThrowRecoverableError.set(false);
16794+
throw createRendererException(
16795+
new MediaCodecRenderer.DecoderInitializationException(
16796+
new Format.Builder().build(),
16797+
new IllegalArgumentException(),
16798+
false,
16799+
0),
16800+
this.getFormatHolder().format,
16801+
true,
16802+
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
16803+
}
16804+
}
16805+
}
16806+
};
16807+
}
16808+
})
16809+
.build();
16810+
player.setMediaSources(
16811+
ImmutableList.of(
16812+
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
16813+
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT)));
16814+
player.prepare();
16815+
16816+
// Play a bit until the reading period has advanced.
16817+
player.play();
16818+
advance(player).untilBackgroundThreadCondition(() -> onStreamChangedCount.get() == 2);
16819+
shouldRendererThrowRecoverableError.set(true);
16820+
runUntilPlaybackState(player, Player.STATE_ENDED);
16821+
16822+
player.release();
16823+
16824+
// onStreamChanged should occur thrice;
16825+
// 1 during first enable, 2 during replace stream, 3 during error recovery
16826+
assertThat(onStreamChangedCount.get()).isEqualTo(3);
16827+
assertThat(shouldRendererThrowRecoverableError.get()).isFalse();
16828+
}
16829+
1675116830
// Internal methods.
1675216831

1675316832
private void addWatchAsSystemFeature() {

0 commit comments

Comments
 (0)