fix: mpv fallback stats

This commit is contained in:
edde746
2026-01-24 16:50:12 +01:00
parent 09f18692e8
commit d31b4ab7a2
2 changed files with 158 additions and 34 deletions
@@ -411,8 +411,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private fun handleGetStats(result: MethodChannel.Result) { private fun handleGetStats(result: MethodChannel.Result) {
activity?.runOnUiThread { activity?.runOnUiThread {
val stats = if (usingMpvFallback) { val stats = if (usingMpvFallback) {
// For MPV fallback, return empty - stats are fetched via getProperty // For MPV fallback, query MPV properties directly
mapOf("playerType" to "mpv") getMpvStats()
} else { } else {
val coreStats = playerCore?.getStats() ?: emptyMap() val coreStats = playerCore?.getStats() ?: emptyMap()
coreStats + mapOf("playerType" to "exoplayer") coreStats + mapOf("playerType" to "exoplayer")
@@ -421,6 +421,54 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
} ?: result.success(mapOf("playerType" to "unknown")) } ?: result.success(mapOf("playerType" to "unknown"))
} }
/**
* Get playback stats from MPV when in fallback mode.
* Queries relevant MPV properties and returns them in a map format
* compatible with the performance overlay.
*/
private fun getMpvStats(): Map<String, Any?> {
val mpv = mpvCore ?: return mapOf("playerType" to "mpv")
return mapOf(
"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"),
"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"),
"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")
)
}
// ExoPlayerDelegate // ExoPlayerDelegate
override fun onPropertyChange(name: String, value: Any?) { override fun onPropertyChange(name: String, value: Any?) {
@@ -11,6 +11,7 @@ import 'performance_stats.dart';
/// Service that polls player properties and provides performance stats via a stream. /// Service that polls player properties and provides performance stats via a stream.
/// ///
/// Supports both MPV (desktop/iOS) and ExoPlayer (Android) backends. /// Supports both MPV (desktop/iOS) and ExoPlayer (Android) backends.
/// Handles runtime backend switching (e.g., ExoPlayer -> MPV fallback on Android).
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -33,8 +34,10 @@ class PerformanceStatsService {
DateTime _lastFpsUpdate = DateTime.now(); DateTime _lastFpsUpdate = DateTime.now();
double? _currentUiFps; double? _currentUiFps;
/// Whether we're using ExoPlayer (Android) or MPV // Track runtime player type for logging (can differ from Dart object type after fallback)
bool get _isExoPlayer => player is PlayerAndroid; // Values: 'exoplayer', 'mpv', or 'unknown'
String _runtimePlayerType = 'unknown';
StreamSubscription<void>? _backendSwitchedSubscription;
PerformanceStatsService(this.player); PerformanceStatsService(this.player);
@@ -44,6 +47,15 @@ class PerformanceStatsService {
/// Start polling for stats at regular intervals. /// Start polling for stats at regular intervals.
void startPolling() { void startPolling() {
_pollingTimer?.cancel(); _pollingTimer?.cancel();
// Listen for backend switches on Android (ExoPlayer -> MPV fallback)
if (player is PlayerAndroid) {
_backendSwitchedSubscription?.cancel();
_backendSwitchedSubscription = player.streams.backendSwitched.listen((_) {
_updateRuntimePlayerType();
});
}
// Start FPS tracking // Start FPS tracking
_startFpsTracking(); _startFpsTracking();
// Fetch immediately, then poll // Fetch immediately, then poll
@@ -51,6 +63,16 @@ class PerformanceStatsService {
_pollingTimer = Timer.periodic(pollInterval, (_) => _fetchStats()); _pollingTimer = Timer.periodic(pollInterval, (_) => _fetchStats());
} }
/// Update the runtime player type by querying the native layer.
Future<void> _updateRuntimePlayerType() async {
if (player is PlayerAndroid) {
_runtimePlayerType = await (player as PlayerAndroid).getPlayerType();
appLogger.d('Performance stats: runtime player type updated to $_runtimePlayerType');
} else {
_runtimePlayerType = 'mpv'; // Non-Android always uses MPV
}
}
/// Start tracking UI frame rate. /// Start tracking UI frame rate.
void _startFpsTracking() { void _startFpsTracking() {
_frameCount = 0; _frameCount = 0;
@@ -79,9 +101,17 @@ class PerformanceStatsService {
/// Fetch all performance stats from the player. /// Fetch all performance stats from the player.
Future<void> _fetchStats() async { Future<void> _fetchStats() async {
try { try {
if (_isExoPlayer) { // Ensure we know the runtime type on first fetch
await _fetchExoPlayerStats(); if (_runtimePlayerType == 'unknown') {
await _updateRuntimePlayerType();
}
if (player is PlayerAndroid) {
// For Android (ExoPlayer or MPV fallback), always use getStats()
// The native side returns appropriate stats based on which backend is active
await _fetchAndroidStats();
} else { } else {
// For non-Android platforms, use MPV property queries
await _fetchMpvStats(); await _fetchMpvStats();
} }
} catch (e) { } catch (e) {
@@ -89,10 +119,12 @@ class PerformanceStatsService {
} }
} }
/// Fetch stats from ExoPlayer via native method channel. /// Fetch stats from Android player (ExoPlayer or MPV fallback).
Future<void> _fetchExoPlayerStats() async { /// The native side returns appropriate stats based on the active backend.
final exoPlayer = player as PlayerAndroid; Future<void> _fetchAndroidStats() async {
final statsMap = await exoPlayer.getStats(); final androidPlayer = player as PlayerAndroid;
final statsMap = await androidPlayer.getStats();
final playerType = statsMap['playerType'] as String? ?? 'unknown';
// Get app memory usage // Get app memory usage
int? appMemory; int? appMemory;
@@ -100,30 +132,72 @@ class PerformanceStatsService {
appMemory = ProcessInfo.currentRss; appMemory = ProcessInfo.currentRss;
} catch (_) {} } catch (_) {}
final stats = PerformanceStats( if (playerType == 'mpv') {
playerType: (statsMap['playerType'] as String?) ?? 'exoplayer', // Parse MPV stats format (returned when in fallback mode)
// Video metrics final stats = PerformanceStats(
videoCodec: _formatCodecName(statsMap['videoCodec'] as String?), playerType: 'mpv',
videoWidth: statsMap['videoWidth'] as int?, videoCodec: _formatCodecName(statsMap['video-codec'] as String?),
videoHeight: statsMap['videoHeight'] as int?, videoWidth: _parseInt(statsMap['video-params/w'] as String?),
videoFps: (statsMap['videoFps'] as num?)?.toDouble(), videoHeight: _parseInt(statsMap['video-params/h'] as String?),
videoBitrate: statsMap['videoBitrate'] as int?, videoFps: _parseDouble(statsMap['container-fps'] as String?),
videoDecoderName: statsMap['videoDecoderName'] as String?, actualFps: _parseDouble(statsMap['estimated-vf-fps'] as String?),
// Audio metrics videoBitrate: _parseInt(statsMap['video-bitrate'] as String?),
audioCodec: _formatCodecName(statsMap['audioCodec'] as String?), hwdecCurrent: statsMap['hwdec-current'] as String?,
audioSamplerate: statsMap['audioSampleRate'] as int?, audioCodec: _formatCodecName(statsMap['audio-codec-name'] as String?),
audioChannels: _formatChannels(statsMap['audioChannels'] as int?), audioSamplerate: _parseInt(statsMap['audio-params/samplerate'] as String?),
audioBitrate: statsMap['audioBitrate'] as int?, audioChannels: statsMap['audio-params/hr-channels'] as String?,
// Performance metrics audioBitrate: _parseInt(statsMap['audio-bitrate'] as String?),
frameDropCount: statsMap['videoDroppedFrames'] as int?, avsyncChange: _parseDouble(statsMap['total-avsync-change'] as String?),
// Buffer metrics - convert ms to seconds for duration cacheUsed: _parseInt(statsMap['cache-used'] as String?),
cacheDuration: ((statsMap['totalBufferedDurationMs'] as int?) ?? 0) / 1000.0, cacheSpeed: _parseDouble(statsMap['cache-speed'] as String?),
// App metrics displayFps: _parseDouble(statsMap['display-fps'] as String?),
appMemoryBytes: appMemory, frameDropCount: _parseInt(statsMap['frame-drop-count'] as String?),
uiFps: _currentUiFps, decoderFrameDropCount: _parseInt(statsMap['decoder-frame-drop-count'] as String?),
); cacheDuration: _parseDouble(statsMap['demuxer-cache-duration'] as String?),
// Color/Format properties
_statsController.add(stats); pixelformat: statsMap['video-params/pixelformat'] as String?,
hwPixelformat: statsMap['video-params/hw-pixelformat'] as String?,
colormatrix: statsMap['video-params/colormatrix'] as String?,
primaries: statsMap['video-params/primaries'] as String?,
gamma: statsMap['video-params/gamma'] as String?,
// HDR metadata
maxLuma: _parseDouble(statsMap['video-params/max-luma'] as String?),
minLuma: _parseDouble(statsMap['video-params/min-luma'] as String?),
maxCll: _parseDouble(statsMap['video-params/max-cll'] as String?),
maxFall: _parseDouble(statsMap['video-params/max-fall'] as String?),
// Other
aspectName: statsMap['video-params/aspect-name'] as String?,
rotate: _parseInt(statsMap['video-params/rotate'] as String?),
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);
_statsController.add(stats);
} else {
// Parse ExoPlayer stats format
final stats = PerformanceStats(
playerType: 'exoplayer',
// Video metrics
videoCodec: _formatCodecName(statsMap['videoCodec'] as String?),
videoWidth: statsMap['videoWidth'] as int?,
videoHeight: statsMap['videoHeight'] as int?,
videoFps: (statsMap['videoFps'] as num?)?.toDouble(),
videoBitrate: statsMap['videoBitrate'] as int?,
videoDecoderName: statsMap['videoDecoderName'] as String?,
// Audio metrics
audioCodec: _formatCodecName(statsMap['audioCodec'] as String?),
audioSamplerate: statsMap['audioSampleRate'] as int?,
audioChannels: _formatChannels(statsMap['audioChannels'] as int?),
audioBitrate: statsMap['audioBitrate'] as int?,
// Performance metrics
frameDropCount: statsMap['videoDroppedFrames'] as int?,
// Buffer metrics - convert ms to seconds for duration
cacheDuration: ((statsMap['totalBufferedDurationMs'] as int?) ?? 0) / 1000.0,
// App metrics
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);
_statsController.add(stats);
}
} }
/// Format channel count to string (e.g., "2" -> "Stereo", "6" -> "5.1") /// Format channel count to string (e.g., "2" -> "Stereo", "6" -> "5.1")
@@ -260,6 +334,8 @@ class PerformanceStatsService {
/// Dispose of the service and release resources. /// Dispose of the service and release resources.
void dispose() { void dispose() {
_backendSwitchedSubscription?.cancel();
_backendSwitchedSubscription = null;
stopPolling(); stopPolling();
_statsController.close(); _statsController.close();
} }