fix: suppress repeated android log spam from MPV surface and stats polling

- Deduplicate android-surface-size updates with lastSurfaceSize cache
- Short-circuit ensureFlutterOverlayOnTop after initial application
- Remove deprecated cache-used property query
- Skip unavailable MPV properties on Android (display-fps, HDR metadata)
- Gate video-dependent stats queries behind hasVideo check
This commit is contained in:
edde746
2026-02-28 15:00:29 +01:00
parent 15e1f0b9e2
commit 9f81671603
3 changed files with 112 additions and 75 deletions
@@ -478,7 +478,9 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun getMpvStats(): Map<String, Any?> {
val mpv = mpvCore ?: return mapOf("playerType" to "mpv")
return mapOf(
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"),
@@ -495,27 +497,32 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"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"),
"cache-speed" to mpv.getProperty("cache-speed"),
"display-fps" to mpv.getProperty("display-fps"),
"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"),
// Color/Format properties
"video-params/pixelformat" to mpv.getProperty("video-params/pixelformat"),
"video-params/hw-pixelformat" to mpv.getProperty("video-params/hw-pixelformat"),
"video-params/colormatrix" to mpv.getProperty("video-params/colormatrix"),
"video-params/primaries" to mpv.getProperty("video-params/primaries"),
"video-params/gamma" to mpv.getProperty("video-params/gamma"),
// HDR metadata
"video-params/max-luma" to mpv.getProperty("video-params/max-luma"),
"video-params/min-luma" to mpv.getProperty("video-params/min-luma"),
"video-params/max-cll" to mpv.getProperty("video-params/max-cll"),
"video-params/max-fall" to mpv.getProperty("video-params/max-fall"),
// Other
"video-params/aspect-name" to mpv.getProperty("video-params/aspect-name"),
"video-params/rotate" to mpv.getProperty("video-params/rotate")
)
// 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
}
// PiP Mode handling
@@ -605,7 +612,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
fallbackInProgress = false
// Configure basic MPV properties for Plex playback
mpvCore?.setProperty("hwdec", "mediacodec,mediacodec-copy")
mpvCore?.setProperty("hwdec", "auto")
mpvCore?.setProperty("vo", "gpu")
mpvCore?.setProperty("ao", "audiotrack")
@@ -49,10 +49,10 @@ class MpvPlayerCore(private val activity: Activity) :
private var surfaceContainer: android.widget.FrameLayout? = null
private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? =
null
private var voInUse: String = "gpu"
@Volatile private var nativeReady: Boolean = false
@Volatile private var disposing: Boolean = false
private var pendingSurface: Surface? = null
private var lastSurfaceSize: String? = null
var delegate: MpvPlayerDelegate? = null
var isInitialized: Boolean = false
private set
@@ -125,7 +125,10 @@ class MpvPlayerCore(private val activity: Activity) :
}
}
private var flutterOverlayApplied = false
private fun ensureFlutterOverlayOnTop() {
if (flutterOverlayApplied) return
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
contentView.post {
if (!isInitialized) return@post
@@ -153,6 +156,11 @@ class MpvPlayerCore(private val activity: Activity) :
}
flutterContainer?.let { container ->
// Skip if Flutter container is already the topmost child
if (contentView.getChildAt(contentView.childCount - 1) == container) {
flutterOverlayApplied = true
return@post
}
contentView.bringChildToFront(container)
for (j in 0 until container.childCount) {
val flutterChild = container.getChildAt(j)
@@ -167,6 +175,7 @@ class MpvPlayerCore(private val activity: Activity) :
break
}
}
flutterOverlayApplied = true
}
}
}
@@ -244,13 +253,7 @@ class MpvPlayerCore(private val activity: Activity) :
ensureFlutterOverlayOnTop()
// Re-apply surface size on layout change (orientation transitions)
val sv = surfaceView
if (sv != null && nativeReady && !disposing && sv.width > 0 && sv.height > 0) {
try {
MPVLib.setPropertyString("android-surface-size", "${sv.width}x${sv.height}")
} catch (e: Exception) {
Log.w(TAG, "Failed to update surface size on layout change", e)
}
}
if (sv != null) applySurfaceSize(sv.width, sv.height)
}
contentView.viewTreeObserver.addOnGlobalLayoutListener(overlayLayoutListener)
@@ -406,17 +409,13 @@ class MpvPlayerCore(private val activity: Activity) :
attachSurfaceInternal(surface)
// Reassert overlay order whenever the surface is recreated
flutterOverlayApplied = false
ensureFlutterOverlayOnTop()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d(TAG, "Surface changed: ${width}x${height}")
if (!nativeReady || disposing) return
try {
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
} catch (e: Exception) {
Log.w(TAG, "Failed to apply surface size to MPV", e)
}
applySurfaceSize(width, height)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
@@ -432,13 +431,26 @@ class MpvPlayerCore(private val activity: Activity) :
MPVLib.attachSurface(surface)
MPVLib.setOptionString("force-window", "yes")
// Restore video output after surface is available
MPVLib.setPropertyString("vo", voInUse)
MPVLib.setPropertyString("vo", "gpu")
} catch (e: Exception) {
Log.w(TAG, "Failed to attach MPV surface", e)
}
}
private fun applySurfaceSize(width: Int, height: Int) {
if (!nativeReady || disposing || width <= 0 || height <= 0) return
val size = "${width}x${height}"
if (size == lastSurfaceSize) return
lastSurfaceSize = size
try {
MPVLib.setPropertyString("android-surface-size", size)
} catch (e: Exception) {
Log.w(TAG, "Failed to apply surface size to MPV", e)
}
}
private fun detachSurfaceInternal() {
lastSurfaceSize = null
if (!nativeReady) return
// Disable video output before detaching (like mpv-android)
try {
@@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:io' show ProcessInfo;
import 'dart:io' show Platform, ProcessInfo;
import 'package:flutter/scheduler.dart';
@@ -214,7 +214,7 @@ class PerformanceStatsService {
/// Fetch stats from MPV via property queries.
Future<void> _fetchMpvStats() async {
// Fetch all properties in parallel for efficiency
// Fetch core properties in parallel
final results = await Future.wait([
player.getProperty('video-codec'), // 0
player.getProperty('video-params/w'), // 1
@@ -228,28 +228,49 @@ class PerformanceStatsService {
player.getProperty('audio-params/hr-channels'), // 9
player.getProperty('audio-bitrate'), // 10
player.getProperty('total-avsync-change'), // 11
player.getProperty('cache-used'), // 12
player.getProperty('cache-speed'), // 13
player.getProperty('display-fps'), // 14
player.getProperty('frame-drop-count'), // 15
player.getProperty('decoder-frame-drop-count'), // 16
player.getProperty('demuxer-cache-duration'), // 17
// Color/Format properties
player.getProperty('video-params/pixelformat'), // 18
player.getProperty('video-params/hw-pixelformat'), // 19
player.getProperty('video-params/colormatrix'), // 20
player.getProperty('video-params/primaries'), // 21
player.getProperty('video-params/gamma'), // 22
// HDR metadata
player.getProperty('video-params/max-luma'), // 23
player.getProperty('video-params/min-luma'), // 24
player.getProperty('video-params/max-cll'), // 25
player.getProperty('video-params/max-fall'), // 26
// Other
player.getProperty('video-params/aspect-name'), // 27
player.getProperty('video-params/rotate'), // 28
player.getProperty('cache-speed'), // 12
player.getProperty('frame-drop-count'), // 13
player.getProperty('decoder-frame-drop-count'), // 14
player.getProperty('demuxer-cache-duration'), // 15
]);
final hasVideo = results[1] != null;
// Only query video-dependent properties when a video track is active.
// On Android, skip properties that are typically unavailable (display-fps,
// hw-pixelformat, HDR metadata) — MPV's native layer logs errors for these
// that we cannot suppress.
List<String?>? videoResults;
if (hasVideo) {
final isAndroid = Platform.isAndroid;
videoResults = await Future.wait([
isAndroid
? Future.value(null)
: player.getProperty('display-fps'), // 0
player.getProperty('video-params/pixelformat'), // 1
isAndroid
? Future.value(null)
: player.getProperty('video-params/hw-pixelformat'), // 2
player.getProperty('video-params/colormatrix'), // 3
player.getProperty('video-params/primaries'), // 4
player.getProperty('video-params/gamma'), // 5
isAndroid
? Future.value(null)
: player.getProperty('video-params/max-luma'), // 6
isAndroid
? Future.value(null)
: player.getProperty('video-params/min-luma'), // 7
isAndroid
? Future.value(null)
: player.getProperty('video-params/max-cll'), // 8
isAndroid
? Future.value(null)
: player.getProperty('video-params/max-fall'), // 9
player.getProperty('video-params/aspect-name'), // 10
player.getProperty('video-params/rotate'), // 11
]);
}
// Get app memory usage
int? appMemory;
try {
@@ -272,26 +293,23 @@ class PerformanceStatsService {
audioChannels: results[9],
audioBitrate: _parseInt(results[10]),
avsyncChange: _parseDouble(results[11]),
cacheUsed: _parseInt(results[12]),
cacheSpeed: _parseDouble(results[13]),
displayFps: _parseDouble(results[14]),
frameDropCount: _parseInt(results[15]),
decoderFrameDropCount: _parseInt(results[16]),
cacheDuration: _parseDouble(results[17]),
// Color/Format properties
pixelformat: results[18],
hwPixelformat: results[19],
colormatrix: results[20],
primaries: results[21],
gamma: results[22],
// HDR metadata
maxLuma: _parseDouble(results[23]),
minLuma: _parseDouble(results[24]),
maxCll: _parseDouble(results[25]),
maxFall: _parseDouble(results[26]),
// Other
aspectName: results[27],
rotate: _parseInt(results[28]),
cacheSpeed: _parseDouble(results[12]),
frameDropCount: _parseInt(results[13]),
decoderFrameDropCount: _parseInt(results[14]),
cacheDuration: _parseDouble(results[15]),
// Video-dependent properties
displayFps: _parseDouble(videoResults?[0]),
pixelformat: videoResults?[1],
hwPixelformat: videoResults?[2],
colormatrix: videoResults?[3],
primaries: videoResults?[4],
gamma: videoResults?[5],
maxLuma: _parseDouble(videoResults?[6]),
minLuma: _parseDouble(videoResults?[7]),
maxCll: _parseDouble(videoResults?[8]),
maxFall: _parseDouble(videoResults?[9]),
aspectName: videoResults?[10],
rotate: _parseInt(videoResults?[11]),
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);