fix(android): force codec re-init on TV surface changes to recover video after screensaver

close #1481
This commit is contained in:
edde746
2026-07-04 19:25:56 +02:00
parent 50261398bf
commit dd09e421bf
3 changed files with 69 additions and 0 deletions
@@ -0,0 +1,24 @@
package com.edde746.plezy
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
/**
* Native mirror of MainActivity.getAndroidTvDetection(): any TV signal counts.
* Kept in sync with the Dart-facing detection so native gating matches
* PlatformDetector.isTV().
*/
object TvDetection {
fun isTv(context: Context): Boolean {
val pm = context.packageManager
val uiModeType = context.resources.configuration.uiMode and Configuration.UI_MODE_TYPE_MASK
@Suppress("DEPRECATION")
return uiModeType == Configuration.UI_MODE_TYPE_TELEVISION ||
pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
pm.hasSystemFeature("amazon.hardware.fire_tv") ||
!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
}
}
@@ -25,6 +25,7 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
import androidx.media3.exoplayer.video.VideoRendererEventListener
import com.edde746.plezy.TvDetection
import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.abs
@@ -32,6 +33,13 @@ import kotlin.math.abs
@OptIn(UnstableApi::class)
class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context) {
/**
* Android TV: MediaCodec.setOutputSurface is broken on many TV SoCs (Sony Bravia
* MediaTek etc., ExoPlayer #5119/#8329) — force full codec re-init on surface
* changes instead (#1481). TV-only: phones keep the fast path for PiP churn.
*/
private val forceSetOutputSurfaceWorkaround: Boolean = TvDetection.isTv(context)
/** Audio delay in microseconds. Shared with PositionFixAudioSink for live updates. */
val audioDelayUs = AtomicLong(0L)
@@ -79,6 +87,7 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
.setEventHandler(eventHandler)
.setEventListener(eventListener)
.setMaxDroppedFramesToNotify(MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY),
forceSetOutputSurfaceWorkaround,
videoDiagnosticsLogger
)
}
@@ -341,6 +350,7 @@ internal class SubtitleDelayRenderer(
@OptIn(UnstableApi::class)
internal class DvSanitizingVideoRenderer(
builder: Builder,
private val forceSetOutputSurfaceWorkaround: Boolean,
private val log: ((String, String, String) -> Unit)?
) : MediaCodecVideoRenderer(builder) {
@@ -348,6 +358,26 @@ internal class DvSanitizingVideoRenderer(
private var stripHdr10PlusSei = false
private var stripDvRpu = false
private var loggedSurfaceWorkaround = false
// On TV SoCs MediaCodec.setOutputSurface() silently yields a black picture after
// the screensaver/background destroys and recreates the surface (#1481). Returning
// true makes media3 release + re-init the codec instead. media3's built-in device
// list doesn't cover 2019+ Bravias; consulted once per codec init.
override fun codecNeedsSetOutputSurfaceWorkaround(name: String): Boolean {
if (forceSetOutputSurfaceWorkaround) {
if (!loggedSurfaceWorkaround) {
loggedSurfaceWorkaround = true
log?.invoke(
"info",
"video",
"TV setOutputSurface workaround active: codec will fully re-init on surface changes (codec=$name)"
)
}
return true
}
return super.codecNeedsSetOutputSurfaceWorkaround(name)
}
// Sanitize diagnostics — playback-thread only, cumulative for the renderer's lifetime.
private var sanitizedSampleCount = 0L
@@ -143,6 +143,21 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
_recordLifecycleState('resumed', action: 'render_restored');
}
// TV never hides the render layer on background (_handleAppHidden returns
// early without setting _hiddenForBackground), but the screensaver can
// still destroy the surface. Kick the video output so a missed surface
// callback can't leave the picture black: mpv re-attaches via
// refreshVideoOutput, ExoPlayer just reapplies sizing/z-order.
if (!_hiddenForBackground &&
Platform.isAndroid &&
PlatformDetector.isTV() &&
currentPlayer != null &&
_isPlayerInitialized) {
await currentPlayer.updateFrame();
if (!mounted || currentPlayer != player) return;
_recordLifecycleState('resumed', action: 'tv_video_output_kick');
}
// Restore media controls and wakelock when app is resumed.
if (_isPlayerInitialized && mounted) {
_resumeMediaControlsAfterTvBackground('app_resumed');