fix: mpv destroy/create ANR
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user