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 735fecd2..00bd5288 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 @@ -431,6 +431,80 @@ class MpvPlayerCore(private val activity: Activity) : MPVLib.observeProperty(name, mpvFormat) } + /** + * Get an MPV property asynchronously off the UI thread. + * This prevents ANR when mpv_get_property blocks waiting for internal locks. + */ + fun getPropertyAsync(name: String, result: MethodChannel.Result) { + if (!isInitialized) { + result.success(null) + return + } + commandExecutor.execute { + try { + val value = MPVLib.getPropertyString(name) + activity.runOnUiThread { result.success(value) } + } catch (e: Exception) { + activity.runOnUiThread { result.success(null) } + } + } + } + + /** + * Set an MPV property asynchronously off the UI thread. + * This prevents ANR when mpv_set_property blocks waiting for internal locks. + */ + fun setPropertyAsync(name: String, value: String, result: MethodChannel.Result) { + if (!isInitialized) { + result.success(null) + return + } + commandExecutor.execute { + try { + MPVLib.setPropertyString(name, value) + activity.runOnUiThread { result.success(null) } + } catch (e: Exception) { + Log.e(TAG, "Async setProperty failed: ${e.message}", e) + activity.runOnUiThread { result.success(null) } + } + } + } + + /** + * Observe an MPV property asynchronously off the UI thread. + * This prevents ANR when mpv_observe_property blocks waiting for internal locks. + */ + fun observePropertyAsync(name: String, format: String, result: MethodChannel.Result) { + if (!isInitialized) { + result.success(null) + return + } + val mpvFormat = when (format) { + "double" -> MPVLib.MPV_FORMAT_DOUBLE + "flag" -> MPVLib.MPV_FORMAT_FLAG + "string" -> MPVLib.MPV_FORMAT_STRING + "node" -> MPVLib.MPV_FORMAT_NODE + else -> MPVLib.MPV_FORMAT_NONE + } + commandExecutor.execute { + try { + MPVLib.observeProperty(name, mpvFormat) + activity.runOnUiThread { result.success(null) } + } catch (e: Exception) { + Log.e(TAG, "Async observeProperty failed: ${e.message}", e) + activity.runOnUiThread { result.success(null) } + } + } + } + + /** + * Run a block on the command executor (background thread). + * Used by the plugin for I/O operations that shouldn't block the main thread. + */ + fun runOnExecutor(block: () -> Unit) { + commandExecutor.execute(block) + } + fun command(args: Array) { if (!isInitialized || args.isEmpty()) return MPVLib.command(args) 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 29a0f2fc..935e8452 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 @@ -162,8 +162,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, return } - playerCore?.setProperty(name, value) - result.success(null) + playerCore?.setPropertyAsync(name, value, result) + ?: result.success(null) } private fun handleGetProperty(call: MethodCall, result: MethodChannel.Result) { @@ -174,8 +174,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, return } - val value = playerCore?.getProperty(name) - result.success(value) + playerCore?.getPropertyAsync(name, result) + ?: result.success(null) } private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) { @@ -189,8 +189,8 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, } nameToId[name] = id - playerCore?.observeProperty(name, format) - result.success(null) + playerCore?.observePropertyAsync(name, format, result) + ?: result.success(null) } private fun handleCommand(call: MethodCall, result: MethodChannel.Result) { @@ -253,28 +253,32 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, return } - try { - val uri = Uri.parse(uriString) - val contentResolver = activity?.contentResolver - if (contentResolver == null) { - result.error("NO_ACTIVITY", "Activity not available", null) - return - } - - val pfd = contentResolver.openFileDescriptor(uri, "r") - if (pfd == null) { - result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null) - return - } - - // detachFd() transfers ownership of the FD to the caller (MPV via fdclose://) - val fd = pfd.detachFd() - Log.d(TAG, "Opened content FD $fd for $uriString") - result.success(fd) - } catch (e: Exception) { - Log.e(TAG, "Failed to open content FD: ${e.message}", e) - result.error("OPEN_FAILED", e.message, null) + val contentResolver = activity?.contentResolver + if (contentResolver == null) { + result.error("NO_ACTIVITY", "Activity not available", null) + return } + + // Open file descriptor off UI thread to prevent ANR on slow storage + playerCore?.runOnExecutor { + try { + val uri = Uri.parse(uriString) + val pfd = contentResolver.openFileDescriptor(uri, "r") + if (pfd == null) { + activity?.runOnUiThread { + result.error("OPEN_FAILED", "Failed to open file descriptor for $uriString", null) + } + return@runOnExecutor + } + + val fd = pfd.detachFd() + Log.d(TAG, "Opened content FD $fd for $uriString") + activity?.runOnUiThread { result.success(fd) } + } catch (e: Exception) { + Log.e(TAG, "Failed to open content FD: ${e.message}", e) + activity?.runOnUiThread { result.error("OPEN_FAILED", e.message, null) } + } + } ?: result.error("NO_PLAYER", "Player not initialized", null) } // MpvPlayerDelegate