diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index 6675b08d..5bd490ac 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -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() + 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() - 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}")) 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 86f6a8b6..158af551 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 @@ -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() } } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt index a92e7d3b..37eacdac 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt @@ -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)