fix(player): fetch Android mpv stats off main thread
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -32,6 +32,9 @@ class PlayerNative extends PlayerBase {
|
||||
@override
|
||||
String get playerType => 'mpv';
|
||||
|
||||
@override
|
||||
bool get providesNativeStats => Platform.isAndroid;
|
||||
|
||||
/// Node properties are returned as structured maps on macOS/iOS/Linux,
|
||||
/// but as JSON strings on Android/Windows.
|
||||
static final String _nodeFormat = (Platform.isAndroid || Platform.isWindows) ? 'string' : 'node';
|
||||
@@ -294,6 +297,14 @@ class PlayerNative extends PlayerBase {
|
||||
return await invoke<String>('getProperty', {'name': name});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getStats() async {
|
||||
if (disposed || !Platform.isAndroid) return super.getStats();
|
||||
await _ensureInitialized();
|
||||
final result = await invoke<Map>('getStats');
|
||||
return Map<String, dynamic>.from(result ?? const {});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> command(List<String> args) async {
|
||||
if (disposed) return;
|
||||
|
||||
Reference in New Issue
Block a user