fix(player): fetch Android mpv stats off main thread

This commit is contained in:
edde746
2026-06-15 12:04:58 +02:00
parent 7195702242
commit 6292685b5d
4 changed files with 185 additions and 72 deletions
@@ -446,11 +446,18 @@ class ExoPlayerPlugin :
activity?.runOnUiThread {
if (usingMpvFallback) {
val selectFlag = if (select) "select" else "auto"
mpvCore?.command(arrayOf("sub-add", uri, selectFlag, title ?: "External"))
val core = mpvCore
if (core == null) {
result.success(null)
} else {
core.command(arrayOf("sub-add", uri, selectFlag, title ?: "External")) {
result.success(null)
}
}
} else {
playerCore?.addSubtitleTrack(uri, title, language, mimeType, select)
result.success(null)
}
result.success(null)
} ?: result.success(null)
}
@@ -690,57 +697,7 @@ class ExoPlayerPlugin :
* compatible with the performance overlay.
*/
private fun getMpvStats(): Map<String, Any?> {
val mpv = mpvCore ?: return mapOf("playerType" to "mpv")
val hasVideo = mpv.getProperty("video-params/w") != null
val stats = mutableMapOf<String, Any?>(
"playerType" to "mpv",
// Video metrics
"video-codec" to mpv.getProperty("video-codec"),
"video-params/w" to mpv.getProperty("video-params/w"),
"video-params/h" to mpv.getProperty("video-params/h"),
"videoWidth" to mpv.getProperty("dwidth"),
"videoHeight" to mpv.getProperty("dheight"),
"container-fps" to mpv.getProperty("container-fps"),
"estimated-vf-fps" to mpv.getProperty("estimated-vf-fps"),
"video-bitrate" to mpv.getProperty("video-bitrate"),
"hwdec-current" to mpv.getProperty("hwdec-current"),
// Audio metrics
"audio-codec-name" to mpv.getProperty("audio-codec-name"),
"audio-params/samplerate" to mpv.getProperty("audio-params/samplerate"),
"audio-params/hr-channels" to mpv.getProperty("audio-params/hr-channels"),
"audio-bitrate" to mpv.getProperty("audio-bitrate"),
// Performance metrics
"total-avsync-change" to mpv.getProperty("total-avsync-change"),
"cache-used" to mpv.getProperty("cache-used"),
"demuxer-max-bytes" to mpv.getProperty("demuxer-max-bytes"),
"cache-speed" to mpv.getProperty("cache-speed"),
"frame-drop-count" to mpv.getProperty("frame-drop-count"),
"decoder-frame-drop-count" to mpv.getProperty("decoder-frame-drop-count"),
"demuxer-cache-duration" to mpv.getProperty("demuxer-cache-duration")
)
// Only query properties that require an active video track
if (hasVideo) {
stats["display-fps"] = mpv.getProperty("display-fps")
// Color/Format properties
stats["video-params/pixelformat"] = mpv.getProperty("video-params/pixelformat")
stats["video-params/hw-pixelformat"] = mpv.getProperty("video-params/hw-pixelformat")
stats["video-params/colormatrix"] = mpv.getProperty("video-params/colormatrix")
stats["video-params/primaries"] = mpv.getProperty("video-params/primaries")
stats["video-params/gamma"] = mpv.getProperty("video-params/gamma")
// HDR metadata
stats["video-params/max-luma"] = mpv.getProperty("video-params/max-luma")
stats["video-params/min-luma"] = mpv.getProperty("video-params/min-luma")
stats["video-params/max-cll"] = mpv.getProperty("video-params/max-cll")
stats["video-params/max-fall"] = mpv.getProperty("video-params/max-fall")
// Other
stats["video-params/aspect-name"] = mpv.getProperty("video-params/aspect-name")
stats["video-params/rotate"] = mpv.getProperty("video-params/rotate")
}
return stats
return mpvCore?.getStats() ?: mapOf("playerType" to "mpv")
}
// PiP Mode handling
@@ -76,6 +76,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
@Volatile private var videoOutputEpoch: Long = 0L
private val videoOutputMutex = Mutex()
private val commandMutex = Mutex()
private var pendingVideoOutputDisableJob: Job? = null
private var pendingVideoOutputRefreshJob: Job? = null
@@ -111,20 +112,29 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
return refreshRate.toString()
}
private fun updateDisplayFpsOverride(p: MpvPlayer, reason: String) {
private fun updateDisplayFpsOverride(p: MpvPlayer, reason: String, onComplete: () -> Unit = {}) {
val fps = currentDisplayFpsOverride()
if (fps == null) {
Log.d(TAG, "Skipping display-fps-override update ($reason): no display rate")
onComplete()
return
}
if (!scope.isActive) {
onComplete()
return
}
try {
runBlocking(Dispatchers.IO) {
scope.launch(Dispatchers.IO) {
try {
p.setProperty("display-fps-override", fps)
Log.d(TAG, "Updated display-fps-override=$fps ($reason)")
} catch (e: Exception) {
Log.w(TAG, "Failed to update display-fps-override ($reason)", e)
} finally {
withContext(NonCancellable + Dispatchers.Main) {
onComplete()
}
}
Log.d(TAG, "Updated display-fps-override=$fps ($reason)")
} catch (e: Exception) {
Log.w(TAG, "Failed to update display-fps-override ($reason)", e)
}
}
@@ -649,8 +659,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
// Public API
fun setProperty(name: String, value: String) {
if (!isInitialized || disposing) return
fun setProperty(name: String, value: String, onComplete: ((Boolean) -> Unit)? = null) {
if (!isInitialized || disposing || !scope.isActive) {
onComplete?.invoke(false)
return
}
if (name == "pause") {
val paused = normalizePauseValue(value)
if (paused == true) {
@@ -664,6 +677,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
if (!hasReadyVideoOutput()) {
deferredResumeRequested = true
Log.d(TAG, "Deferring public resume until video output is ready")
onComplete?.invoke(true)
return
}
cachedPaused = false
@@ -671,16 +685,30 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
Log.d(TAG, "Public pause state updated: paused=false")
}
}
scope.launch {
scope.launch(Dispatchers.IO) {
var success = false
try {
player?.setProperty(name, value)
success = true
} catch (e: Exception) {
Log.w(TAG, "setProperty($name) failed", e)
} finally {
withContext(NonCancellable + Dispatchers.Main) {
onComplete?.invoke(success)
}
}
}
}
fun getProperty(name: String): String? {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "Refusing synchronous getProperty($name) on the main thread")
return null
}
return getPropertyBlocking(name)
}
private fun getPropertyBlocking(name: String): String? {
if (!isInitialized || disposing) return null
return try {
runBlocking(Dispatchers.IO) { player?.getString(name) }
@@ -689,6 +717,75 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
}
}
fun getPropertyAsync(name: String, onResult: (String?) -> Unit) {
if (!isInitialized || disposing) {
onResult(null)
return
}
Thread {
val value = getPropertyBlocking(name)
activity.runOnUiThread {
onResult(if (!disposing && isInitialized) value else null)
}
}.start()
}
/**
* Returns MPV stats in the same key format used by the performance overlay.
* This method performs synchronous native property reads and must not be
* called on Android's main thread.
*/
fun getStats(): Map<String, Any?> {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "Refusing synchronous getStats() on the main thread")
return mapOf("playerType" to "mpv")
}
val hasVideo = getProperty("video-params/w") != null
val stats = mutableMapOf<String, Any?>(
"playerType" to "mpv",
"video-codec" to getProperty("video-codec"),
"video-params/w" to getProperty("video-params/w"),
"video-params/h" to getProperty("video-params/h"),
"videoWidth" to getProperty("dwidth"),
"videoHeight" to getProperty("dheight"),
"container-fps" to getProperty("container-fps"),
"estimated-vf-fps" to getProperty("estimated-vf-fps"),
"video-bitrate" to getProperty("video-bitrate"),
"hwdec-current" to getProperty("hwdec-current"),
"audio-codec-name" to getProperty("audio-codec-name"),
"audio-params/samplerate" to getProperty("audio-params/samplerate"),
"audio-params/hr-channels" to getProperty("audio-params/hr-channels"),
"audio-bitrate" to getProperty("audio-bitrate"),
"total-avsync-change" to getProperty("total-avsync-change"),
"cache-used" to getProperty("cache-used"),
"demuxer-max-bytes" to getProperty("demuxer-max-bytes"),
"cache-speed" to getProperty("cache-speed"),
"frame-drop-count" to getProperty("frame-drop-count"),
"decoder-frame-drop-count" to getProperty("decoder-frame-drop-count"),
"demuxer-cache-duration" to getProperty("demuxer-cache-duration")
)
if (hasVideo) {
stats["display-fps"] = getProperty("display-fps")
stats["video-params/pixelformat"] = getProperty("video-params/pixelformat")
stats["video-params/hw-pixelformat"] = getProperty("video-params/hw-pixelformat")
stats["video-params/colormatrix"] = getProperty("video-params/colormatrix")
stats["video-params/primaries"] = getProperty("video-params/primaries")
stats["video-params/gamma"] = getProperty("video-params/gamma")
stats["video-params/max-luma"] = getProperty("video-params/max-luma")
stats["video-params/min-luma"] = getProperty("video-params/min-luma")
stats["video-params/max-cll"] = getProperty("video-params/max-cll")
stats["video-params/max-fall"] = getProperty("video-params/max-fall")
stats["video-params/aspect-name"] = getProperty("video-params/aspect-name")
stats["video-params/rotate"] = getProperty("video-params/rotate")
}
return stats
}
fun observeProperty(name: String, format: String) {
val p = player ?: return
if (!isInitialized) return
@@ -706,15 +803,19 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
onComplete?.invoke(false)
return
}
scope.launch {
scope.launch(Dispatchers.IO) {
var success = false
try {
player?.command(*args)
success = true
commandMutex.withLock {
player?.command(*args)
success = true
}
} catch (e: Exception) {
Log.w(TAG, "command failed", e)
} finally {
onComplete?.invoke(success)
withContext(NonCancellable + Dispatchers.Main) {
onComplete?.invoke(success)
}
}
}
}
@@ -793,8 +894,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
return
}
mgr.setVideoFrameRate(fps, videoDurationMs, extraDelayMs) { switched ->
player?.let { updateDisplayFpsOverride(it, "frame rate switch, switched=$switched") }
onComplete(switched)
player?.let {
updateDisplayFpsOverride(it, "frame rate switch, switched=$switched") {
onComplete(switched)
}
} ?: onComplete(switched)
}
}
@@ -110,6 +110,7 @@ class MpvPlayerPlugin :
"dispose" -> handleDispose(result)
"setProperty" -> handleSetProperty(call, result)
"getProperty" -> handleGetProperty(call, result)
"getStats" -> handleGetStats(result)
"observeProperty" -> handleObserveProperty(call, result)
"command" -> handleCommand(call, result)
"setVisible" -> handleSetVisible(call, result)
@@ -232,8 +233,15 @@ class MpvPlayerPlugin :
return
}
playerCore?.setProperty(name, value)
result.success(null)
val core = playerCore
if (core == null) {
result.success(null)
return
}
core.setProperty(name, value) {
result.success(null)
}
}
private fun handleGetProperty(call: MethodCall, result: MethodChannel.Result) {
@@ -244,8 +252,41 @@ class MpvPlayerPlugin :
return
}
val value = playerCore?.getProperty(name)
result.success(value)
val core = playerCore
if (core == null) {
result.success(null)
return
}
val gen = sessionGeneration
core.getPropertyAsync(name) { value ->
if (gen != sessionGeneration || playerCore !== core) {
result.success(null)
} else {
result.success(value)
}
}
}
private fun handleGetStats(result: MethodChannel.Result) {
val currentActivity = activity
val core = playerCore
if (currentActivity == null || core == null) {
result.success(mapOf("playerType" to "mpv"))
return
}
val gen = sessionGeneration
Thread {
val stats = core.getStats()
currentActivity.runOnUiThread {
if (gen != sessionGeneration || playerCore !== core) {
result.success(mapOf("playerType" to "mpv"))
} else {
result.success(stats)
}
}
}.start()
}
private fun handleObserveProperty(call: MethodCall, result: MethodChannel.Result) {