2828import androidx .annotation .CheckResult ;
2929import androidx .annotation .IntDef ;
3030import androidx .annotation .Nullable ;
31+ import androidx .annotation .VisibleForTesting ;
3132import com .google .android .exoplayer2 .BaseRenderer ;
3233import com .google .android .exoplayer2 .C ;
3334import 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}.
0 commit comments