From 3c61e0d39e1ffd6a540104fe60d3bba226dad194 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 16 May 2026 21:29:07 +0530 Subject: [PATCH] fix: decoder fallback handling in ExoPlayer to try app decoders if a decoder failure occurs --- .../features/player/PlayerEngine.android.kt | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt index ebdcfd92..62ebd521 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt @@ -98,10 +98,28 @@ actual fun PlatformPlayerSurface( val libassRenderType = runCatching { LibassRenderType.valueOf(playerSettings.libassRenderType) }.getOrDefault(LibassRenderType.CUES) + val playerSourceKey = listOf( + sourceUrl, + sourceAudioUrl.orEmpty(), + sanitizedSourceHeaders, + sanitizedSourceResponseHeaders, + useYoutubeChunkedPlayback, + ) + var decoderPriorityOverride by remember(playerSourceKey) { mutableStateOf(null) } + var fallbackStartPositionMs by remember(playerSourceKey) { mutableStateOf(null) } + val effectiveDecoderPriority = decoderPriorityOverride ?: playerSettings.decoderPriority - val exoPlayer = remember(sourceUrl, sourceAudioUrl, sanitizedSourceHeaders, sanitizedSourceResponseHeaders) { + val exoPlayer = remember( + sourceUrl, + sourceAudioUrl, + sanitizedSourceHeaders, + sanitizedSourceResponseHeaders, + useYoutubeChunkedPlayback, + effectiveDecoderPriority, + ) { val renderersFactory = DefaultRenderersFactory(context) - .setExtensionRendererMode(playerSettings.decoderPriority) + .setExtensionRendererMode(effectiveDecoderPriority) + .setEnableDecoderFallback(true) .setMapDV7ToHevc(playerSettings.mapDV7ToHevc) val trackSelector = DefaultTrackSelector(context).apply { @@ -169,6 +187,7 @@ actual fun PlatformPlayerSurface( } else { setMediaItem(MediaItem.fromUri(sourceUrl)) } + fallbackStartPositionMs?.let { seekTo(it.coerceAtLeast(0L)) } prepare() this.playWhenReady = playWhenReady } @@ -191,6 +210,21 @@ actual fun PlatformPlayerSurface( val listener = object : Player.Listener { override fun onPlayerError(error: PlaybackException) { syncPlayerViewKeepScreenOn() + if ( + playerSettings.decoderPriority == DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON && + effectiveDecoderPriority != DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER && + error.isDecoderFailure() + ) { + Log.w( + TAG, + "Decoder failure (${error.errorCodeName}); retrying with app decoders", + error, + ) + fallbackStartPositionMs = exoPlayer.currentPosition.coerceAtLeast(0L) + decoderPriorityOverride = DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + latestOnError.value(null) + return + } latestOnError.value(error.localizedMessage ?: runBlocking { getString(Res.string.player_unable_to_play_stream) }) } @@ -204,6 +238,7 @@ actual fun PlatformPlayerSurface( } Log.d(TAG, "onPlaybackStateChanged: $stateName") if (playbackState == Player.STATE_READY) { + fallbackStartPositionMs = null latestOnError.value(null) exoPlayer.logCurrentTracks("STATE_READY") } @@ -484,6 +519,16 @@ private fun ExoPlayer.shouldKeepPlayerScreenOn(): Boolean = playWhenReady && playbackState in setOf(Player.STATE_BUFFERING, Player.STATE_READY) +private fun PlaybackException.isDecoderFailure(): Boolean = + errorCode in setOf( + PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, + PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED, + PlaybackException.ERROR_CODE_DECODING_FAILED, + PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES, + PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, + PlaybackException.ERROR_CODE_DECODING_RESOURCES_RECLAIMED, + ) + private fun PlayerResizeMode.toExoResizeMode(): Int = when (this) { PlayerResizeMode.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT