From 6916b386eb515ffe5703f9fb055d2f2d5bb35c1a Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 23 Feb 2026 05:27:36 +0100 Subject: [PATCH] fix(android): SurfaceControl crash on player dispose --- .../com/edde746/plezy/exoplayer/ExoPlayerCore.kt | 14 ++++++++++---- .../kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt | 12 +++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 3609300a..31d30a27 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -146,6 +146,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { private fun ensureFlutterOverlayOnTop() { val contentView = activity.findViewById(android.R.id.content) contentView.post { + if (!isInitialized) return@post var flutterContainer: ViewGroup? = null for (i in 0 until contentView.childCount) { @@ -1275,14 +1276,19 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { val contentView = activity.findViewById(android.R.id.content) contentView.viewTreeObserver.removeOnGlobalLayoutListener(listener) } + overlayLayoutListener = null - // Post view removal to next frame to avoid SurfaceControl race on render thread - val contentView = activity.findViewById(android.R.id.content) + // Remove SurfaceView from container synchronously — triggers + // onDetachedFromWindow which properly unregisters PositionUpdateListener + // and releases SurfaceControl before the render thread can access it. val container = surfaceContainer - val subtitle = subtitleView + surfaceView?.let { container?.removeView(it) } + subtitleView?.let { container?.removeView(it) } + + // Defer empty container removal (plain FrameLayout, no SurfaceControl) + val contentView = activity.findViewById(android.R.id.content) contentView.post { container?.let { contentView.removeView(it) } - subtitle?.let { contentView.removeView(it) } } surfaceContainer = null surfaceView = null diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt index 9d913701..86f6a8b6 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt @@ -135,6 +135,7 @@ class MpvPlayerCore(private val activity: Activity) : private fun ensureFlutterOverlayOnTop() { val contentView = activity.findViewById(android.R.id.content) contentView.post { + if (!isInitialized) return@post var flutterContainer: ViewGroup? = null // First pass: look for FlutterView by name (debug builds) @@ -717,15 +718,20 @@ class MpvPlayerCore(private val activity: Activity) : MPVLib.removeObserver(this) MPVLib.removeLogObserver(this) - surfaceView?.holder?.removeCallback(this) overlayLayoutListener?.let { listener -> val contentView = activity.findViewById(android.R.id.content) contentView.viewTreeObserver.removeOnGlobalLayoutListener(listener) } + overlayLayoutListener = null - // Post view removal to next frame to avoid SurfaceControl race on render thread - val contentView = activity.findViewById(android.R.id.content) + // Remove SurfaceView synchronously (keep callback so surfaceDestroyed + // fires → MPVLib.detachSurface()), then remove callback after. val container = surfaceContainer + surfaceView?.let { container?.removeView(it) } + surfaceView?.holder?.removeCallback(this) + + // Defer empty container removal (plain FrameLayout, no SurfaceControl) + val contentView = activity.findViewById(android.R.id.content) contentView.post { container?.let { contentView.removeView(it) } }