Skip to content

Commit 95786a8

Browse files
committed
Merge branch 'patch-5684' of https://github.com/mjd/SmartTube into mjd-patch-5684
2 parents ccef5ab + 7371a8b commit 95786a8

2 files changed

Lines changed: 199 additions & 3 deletions

File tree

exoplayer-amzn-2.10.6/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import androidx.annotation.CheckResult;
2929
import androidx.annotation.IntDef;
3030
import androidx.annotation.Nullable;
31+
import androidx.annotation.VisibleForTesting;
3132
import com.google.android.exoplayer2.BaseRenderer;
3233
import com.google.android.exoplayer2.C;
3334
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -342,6 +343,16 @@ private static String buildCustomDiagnosticInfo(int errorCode) {
342343
@DrainAction private int codecDrainAction;
343344
private boolean codecReceivedBuffers;
344345
private boolean codecReceivedEos;
346+
// SmartTube fix: transient MediaCodec error recovery.
347+
// A transient codec IllegalStateException is recovered in place (codec re-init) instead of
348+
// surfacing as an unrecoverable TYPE_UNEXPECTED player error (full engine restart + error toast).
349+
// A short per-window attempt budget prevents a permanently wedged codec from re-initializing in a
350+
// tight loop; once exhausted we surface the error (with a real message) and let the app restart the
351+
// engine, as it did before this fix.
352+
private static final int MAX_CODEC_RECOVERY_ATTEMPTS = 3;
353+
private static final long CODEC_RECOVERY_WINDOW_MS = 5_000;
354+
private int codecRecoveryAttempts;
355+
private long lastCodecRecoveryMs = C.TIME_UNSET;
345356
private long lastBufferInStreamPresentationTimeUs;
346357
private long largestQueuedPresentationTimeUs;
347358
private boolean inputStreamEnded;
@@ -667,9 +678,20 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx
667678
if (codec != null) {
668679
long drainStartTimeMs = SystemClock.elapsedRealtime();
669680
TraceUtil.beginSection("drainAndFeed");
670-
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
671-
while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {}
672-
TraceUtil.endSection();
681+
try {
682+
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
683+
while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {}
684+
} catch (IllegalStateException e) {
685+
// SmartTube fix: a native MediaCodec error would otherwise propagate as
686+
// TYPE_UNEXPECTED. Try to recover in place; only surface a (typed) error if that fails.
687+
if (isMediaCodecException(e)) {
688+
maybeRecoverFromCodecError(e);
689+
} else {
690+
throw e;
691+
}
692+
} finally {
693+
TraceUtil.endSection();
694+
}
673695
} else {
674696
decoderCounters.skippedInputBufferCount += skipSource(positionUs);
675697
// We need to read any format changes despite not having a codec so that drmSession can be
@@ -681,6 +703,92 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx
681703
decoderCounters.ensureUpdated();
682704
}
683705

706+
/**
707+
* SmartTube fix: recover from a transient MediaCodec
708+
* {@link IllegalStateException} by re-initializing the codec in place, instead of letting it
709+
* surface as an unrecoverable player error (which forces a full engine restart and an "Unexpected
710+
* playback error" toast).
711+
*
712+
* <p>Repeated failures within {@link #CODEC_RECOVERY_WINDOW_MS} (a permanently wedged codec) exhaust
713+
* {@link #MAX_CODEC_RECOVERY_ATTEMPTS} and fall through to {@link #createDecoderException}, so the
714+
* old restart-based recovery still acts as a backstop.
715+
*/
716+
private void maybeRecoverFromCodecError(IllegalStateException error) throws ExoPlaybackException {
717+
long nowMs = SystemClock.elapsedRealtime();
718+
if (lastCodecRecoveryMs == C.TIME_UNSET || nowMs - lastCodecRecoveryMs > CODEC_RECOVERY_WINDOW_MS) {
719+
codecRecoveryAttempts = 0;
720+
}
721+
lastCodecRecoveryMs = nowMs;
722+
723+
if (codecRecoveryAttempts >= MAX_CODEC_RECOVERY_ATTEMPTS) {
724+
throw createDecoderException(error);
725+
}
726+
727+
codecRecoveryAttempts++;
728+
Log.w(TAG, "Recovering from MediaCodec error, re-initializing decoder (attempt "
729+
+ codecRecoveryAttempts + "/" + MAX_CODEC_RECOVERY_ATTEMPTS + ")");
730+
try {
731+
// A wedged codec's stop() can throw inside releaseCodec(), but releaseCodec()'s own finally has
732+
// already nulled the codec by then, so a fresh init can still proceed. Don't let that abort the
733+
// recovery.
734+
try {
735+
releaseCodec();
736+
} catch (Throwable ignored) {
737+
// Codec already released; continue to re-init.
738+
}
739+
maybeInitCodec();
740+
} catch (ExoPlaybackException reinitError) {
741+
throw reinitError;
742+
} catch (Throwable reinitError) {
743+
throw createDecoderException(error);
744+
}
745+
}
746+
747+
/**
748+
* Wraps a MediaCodec {@link IllegalStateException} (which normally carries a {@code null} message)
749+
* with the decoder name and, when available, the codec's diagnostic info, and reports it as an
750+
* {@code UNEXPECTED} error.
751+
*
752+
* <p>We deliberately do NOT use {@link ExoPlaybackException#createForRenderer} here. A
753+
* {@code TYPE_RENDERER} video error routes into the app's ErrorFixerController, which reacts by
754+
* persisting a fallback format ({@code VIDEO_FHD_AVC_30}) — wiping the user's saved video preset.
755+
* Keeping {@code TYPE_UNEXPECTED} preserves the (non-destructive) pre-fix restart path for an
756+
* unrecoverable transient error, while still replacing the bare {@code null} message with a real
757+
* one so it's no longer reported as "Unexpected playback error null".
758+
*/
759+
private ExoPlaybackException createDecoderException(IllegalStateException error) {
760+
String diagnosticInfo = getCodecDiagnosticInfo(error);
761+
String message = "MediaCodec decoder error (" + codecName + ")"
762+
+ (diagnosticInfo != null ? ": " + diagnosticInfo : "");
763+
return ExoPlaybackException.createForUnexpected(new IllegalStateException(message, error));
764+
}
765+
766+
private static String getCodecDiagnosticInfo(IllegalStateException error) {
767+
if (Util.SDK_INT >= 21) {
768+
return getCodecDiagnosticInfoV21(error);
769+
}
770+
return null;
771+
}
772+
773+
@TargetApi(21)
774+
private static String getCodecDiagnosticInfoV21(IllegalStateException error) {
775+
return error instanceof CodecException ? ((CodecException) error).getDiagnosticInfo() : null;
776+
}
777+
778+
@VisibleForTesting
779+
/* package */ static boolean isMediaCodecException(IllegalStateException error) {
780+
if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) {
781+
return true;
782+
}
783+
StackTraceElement[] stackTrace = error.getStackTrace();
784+
return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec");
785+
}
786+
787+
@TargetApi(21)
788+
private static boolean isMediaCodecExceptionV21(IllegalStateException error) {
789+
return error instanceof CodecException;
790+
}
791+
684792
/**
685793
* Flushes the codec. If flushing is not possible, the codec will be released and re-instantiated.
686794
* This method is a no-op if the codec is {@code null}.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (C) 2020 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.android.exoplayer2.mediacodec;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
20+
import org.junit.Test;
21+
22+
/**
23+
* Unit test for the transient-MediaCodec-error classification used by the in-place codec recovery
24+
* (SmartTube fix). {@link MediaCodecRenderer#isMediaCodecException} is the gate that decides
25+
* whether a render-loop {@link IllegalStateException} is treated as a recoverable decoder error, so
26+
* these tests pin the two real-world crash signatures we observed (a native {@code MediaCodec} frame
27+
* at the top of the stack) and guard against unrelated exceptions being swallowed.
28+
*
29+
* <p>Deliberately a plain JUnit test (no Robolectric): it exercises only stack-trace inspection, and
30+
* the 2019-era Robolectric bundled with this ExoPlayer fork cannot run under the JDK 17 required by
31+
* the app's Android Gradle Plugin. Under a plain JVM {@code Util.SDK_INT} reads as 0, so this
32+
* exercises the stack-frame branch — which is exactly the path that fired on the real crashes (both
33+
* were plain {@link IllegalStateException}s, not {@code MediaCodec.CodecException}s).
34+
*/
35+
public class MediaCodecRendererTest {
36+
37+
@Test
38+
public void isMediaCodecException_withNativeDequeueInputBufferFrame_returnsTrue() {
39+
// Observed crash: MediaCodec.native_dequeueInputBuffer on VP9 1080p60.
40+
IllegalStateException error =
41+
illegalStateExceptionWithTopFrame(
42+
"android.media.MediaCodec", "native_dequeueInputBuffer");
43+
44+
assertThat(MediaCodecRenderer.isMediaCodecException(error)).isTrue();
45+
}
46+
47+
@Test
48+
public void isMediaCodecException_withReleaseOutputBufferFrame_returnsTrue() {
49+
// Observed crash: MediaCodec.releaseOutputBuffer on VP9 1920x960.
50+
IllegalStateException error =
51+
illegalStateExceptionWithTopFrame("android.media.MediaCodec", "releaseOutputBuffer");
52+
53+
assertThat(MediaCodecRenderer.isMediaCodecException(error)).isTrue();
54+
}
55+
56+
@Test
57+
public void isMediaCodecException_withUnrelatedFrame_returnsFalse() {
58+
// A logic-bug ISE originating elsewhere must still propagate untouched.
59+
IllegalStateException error =
60+
illegalStateExceptionWithTopFrame(
61+
"com.google.android.exoplayer2.SomeOtherComponent", "doWork");
62+
63+
assertThat(MediaCodecRenderer.isMediaCodecException(error)).isFalse();
64+
}
65+
66+
@Test
67+
public void isMediaCodecException_withEmptyStackTrace_returnsFalse() {
68+
IllegalStateException error = new IllegalStateException();
69+
error.setStackTrace(new StackTraceElement[0]);
70+
71+
assertThat(MediaCodecRenderer.isMediaCodecException(error)).isFalse();
72+
}
73+
74+
private static IllegalStateException illegalStateExceptionWithTopFrame(
75+
String className, String methodName) {
76+
IllegalStateException error = new IllegalStateException();
77+
error.setStackTrace(
78+
new StackTraceElement[] {
79+
new StackTraceElement(className, methodName, /* fileName= */ null, /* lineNumber= */ -2),
80+
new StackTraceElement(
81+
"com.google.android.exoplayer2.mediacodec.MediaCodecRenderer",
82+
"feedInputBuffer",
83+
"MediaCodecRenderer.java",
84+
994)
85+
});
86+
return error;
87+
}
88+
}

0 commit comments

Comments
 (0)