fix(android): force codec re-init on TV surface changes to recover video after screensaver
close #1481
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user