fix: mpv destroy/create ANR

This commit is contained in:
edde746
2026-02-26 12:56:02 +01:00
parent a56ef0e3ca
commit 5cf5b2f0a0
3 changed files with 100 additions and 84 deletions
@@ -585,63 +585,63 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
mpvCore = MpvPlayerCore(currentActivity).apply {
delegate = this@ExoPlayerPlugin
}
val success = mpvCore?.initialize() ?: false
if (!success) {
Log.e(TAG, "Failed to initialize MPV fallback")
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
return@runOnUiThread
}
usingMpvFallback = true
// Configure basic MPV properties for Plex playback
mpvCore?.setProperty("hwdec", "auto")
mpvCore?.setProperty("vo", "gpu")
mpvCore?.setProperty("ao", "audiotrack")
// Forward user's buffer config to MPV fallback
configuredBufferSizeBytes?.let { bytes ->
if (bytes > 0) {
mpvCore?.setProperty("demuxer-max-bytes", bytes.toString())
mpvCore?.initialize { success ->
if (!success) {
Log.e(TAG, "Failed to initialize MPV fallback")
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: $errorMessage"))
return@initialize
}
usingMpvFallback = true
// Configure basic MPV properties for Plex playback
mpvCore?.setProperty("hwdec", "auto")
mpvCore?.setProperty("vo", "gpu")
mpvCore?.setProperty("ao", "audiotrack")
// Forward user's buffer config to MPV fallback
configuredBufferSizeBytes?.let { bytes ->
if (bytes > 0) {
mpvCore?.setProperty("demuxer-max-bytes", bytes.toString())
}
}
// Setup property observers
mpvCore?.observeProperty("time-pos", "double")
mpvCore?.observeProperty("duration", "double")
mpvCore?.observeProperty("pause", "flag")
mpvCore?.observeProperty("paused-for-cache", "flag")
mpvCore?.observeProperty("demuxer-cache-time", "double")
mpvCore?.observeProperty("eof-reached", "flag")
mpvCore?.observeProperty("track-list", "string")
mpvCore?.observeProperty("aid", "string")
mpvCore?.observeProperty("sid", "string")
mpvCore?.observeProperty("volume", "double")
mpvCore?.observeProperty("speed", "double")
// Show the MPV surface
mpvCore?.setVisible(true)
// Load media at the same position
val startSeconds = positionMs / 1000.0
val options = mutableListOf<String>()
options.add("start=$startSeconds")
headers?.forEach { (key, value) ->
options.add("http-header-fields-append=$key: $value")
}
val optionsStr = options.joinToString(",")
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
// Request audio focus
mpvCore?.requestAudioFocus()
// Emit backend-switched event so Flutter can show notification
onEvent("backend-switched", null)
Log.i(TAG, "Successfully switched to MPV fallback")
}
// Setup property observers
mpvCore?.observeProperty("time-pos", "double")
mpvCore?.observeProperty("duration", "double")
mpvCore?.observeProperty("pause", "flag")
mpvCore?.observeProperty("paused-for-cache", "flag")
mpvCore?.observeProperty("demuxer-cache-time", "double")
mpvCore?.observeProperty("eof-reached", "flag")
mpvCore?.observeProperty("track-list", "string")
mpvCore?.observeProperty("aid", "string")
mpvCore?.observeProperty("sid", "string")
mpvCore?.observeProperty("volume", "double")
mpvCore?.observeProperty("speed", "double")
// Show the MPV surface
mpvCore?.setVisible(true)
// Load media at the same position
val startSeconds = positionMs / 1000.0
val options = mutableListOf<String>()
options.add("start=$startSeconds")
headers?.forEach { (key, value) ->
options.add("http-header-fields-append=$key: $value")
}
val optionsStr = options.joinToString(",")
// Convert content:// URIs to fdclose:// for MPV (SAF SD card downloads)
val mpvUri = openContentFd(uri)?.let { "fdclose://$it" } ?: uri
mpvCore?.command(arrayOf("loadfile", mpvUri, "replace", "-1", optionsStr))
// Request audio focus
mpvCore?.requestAudioFocus()
// Emit backend-switched event so Flutter can show notification
onEvent("backend-switched", null)
Log.i(TAG, "Successfully switched to MPV fallback")
} catch (e: Exception) {
Log.e(TAG, "Failed to switch to MPV fallback", e)
onEvent("end-file", mapOf("reason" to "error", "message" to "Fallback failed: ${e.message}"))
@@ -40,6 +40,9 @@ class MpvPlayerCore(private val activity: Activity) :
companion object {
private const val TAG = "MpvPlayerCore"
private const val SHORT_VIDEO_LENGTH_MS = 300000L // 5 minutes
// Guards MPVLib.create/destroy which share global native state
private val mpvLock = Object()
}
private var surfaceView: SurfaceView? = null
@@ -178,10 +181,11 @@ class MpvPlayerCore(private val activity: Activity) :
}
}
fun initialize(): Boolean {
fun initialize(onResult: (Boolean) -> Unit) {
if (isInitialized) {
Log.d(TAG, "Already initialized")
return true
onResult(true)
return
}
try {
@@ -250,25 +254,31 @@ class MpvPlayerCore(private val activity: Activity) :
Log.d(TAG, "SurfaceView added to content view")
// Initialize MPVLib
MPVLib.create(activity.applicationContext)
// Configure MPV defaults
setupMpvDefaults()
// Initialize MPV
MPVLib.init()
// Register event and log observers
MPVLib.addObserver(this)
MPVLib.addLogObserver(this)
isInitialized = true
Log.d(TAG, "Initialized successfully")
return true
// Native MPVLib init on background thread — waits for any
// in-flight destroy to finish without blocking the UI thread.
val ctx = activity.applicationContext
Thread {
try {
synchronized(mpvLock) {
MPVLib.create(ctx)
setupMpvDefaults()
MPVLib.init()
}
handler.post {
MPVLib.addObserver(this)
MPVLib.addLogObserver(this)
isInitialized = true
Log.d(TAG, "Initialized successfully")
onResult(true)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize native: ${e.message}", e)
handler.post { onResult(false) }
}
}.start()
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize: ${e.message}", e)
return false
onResult(false)
}
}
@@ -737,10 +747,16 @@ class MpvPlayerCore(private val activity: Activity) :
}
surfaceContainer = null
surfaceView = null
MPVLib.destroy()
isInitialized = false
Log.d(TAG, "Disposed")
// Run native destroy on background thread to avoid ANR —
// MPVLib.destroy() blocks on pthread_cond_wait while mpv's
// internal threads (lua, demux, vo) shut down.
Thread {
synchronized(mpvLock) {
MPVLib.destroy()
}
Log.d(TAG, "Disposed (native)")
}.start()
}
}
@@ -125,14 +125,14 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
playerCore = MpvPlayerCore(currentActivity).apply {
delegate = this@MpvPlayerPlugin
}
val success = playerCore?.initialize() ?: false
// Start hidden - now safe because setVisible operates on the container,
// not the SurfaceView directly (matching ExoPlayer's approach)
playerCore?.setVisible(false)
Log.d(TAG, "Initialized: $success")
result.success(success)
playerCore?.initialize { success ->
// Start hidden - now safe because setVisible operates on the container,
// not the SurfaceView directly (matching ExoPlayer's approach)
playerCore?.setVisible(false)
Log.d(TAG, "Initialized: $success")
result.success(success)
} ?: result.success(false)
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize: ${e.message}", e)
result.error("INIT_FAILED", e.message, null)