fix: mpv fallback stats
This commit is contained in:
@@ -411,8 +411,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
private fun handleGetStats(result: MethodChannel.Result) {
|
||||
activity?.runOnUiThread {
|
||||
val stats = if (usingMpvFallback) {
|
||||
// For MPV fallback, return empty - stats are fetched via getProperty
|
||||
mapOf("playerType" to "mpv")
|
||||
// For MPV fallback, query MPV properties directly
|
||||
getMpvStats()
|
||||
} else {
|
||||
val coreStats = playerCore?.getStats() ?: emptyMap()
|
||||
coreStats + mapOf("playerType" to "exoplayer")
|
||||
@@ -421,6 +421,54 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||
} ?: 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
|
||||
|
||||
override fun onPropertyChange(name: String, value: Any?) {
|
||||
|
||||
+108
-32
@@ -11,6 +11,7 @@ import 'performance_stats.dart';
|
||||
/// Service that polls player properties and provides performance stats via a stream.
|
||||
///
|
||||
/// Supports both MPV (desktop/iOS) and ExoPlayer (Android) backends.
|
||||
/// Handles runtime backend switching (e.g., ExoPlayer -> MPV fallback on Android).
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
@@ -33,8 +34,10 @@ class PerformanceStatsService {
|
||||
DateTime _lastFpsUpdate = DateTime.now();
|
||||
double? _currentUiFps;
|
||||
|
||||
/// Whether we're using ExoPlayer (Android) or MPV
|
||||
bool get _isExoPlayer => player is PlayerAndroid;
|
||||
// Track runtime player type for logging (can differ from Dart object type after fallback)
|
||||
// Values: 'exoplayer', 'mpv', or 'unknown'
|
||||
String _runtimePlayerType = 'unknown';
|
||||
StreamSubscription<void>? _backendSwitchedSubscription;
|
||||
|
||||
PerformanceStatsService(this.player);
|
||||
|
||||
@@ -44,6 +47,15 @@ class PerformanceStatsService {
|
||||
/// Start polling for stats at regular intervals.
|
||||
void startPolling() {
|
||||
_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
|
||||
_startFpsTracking();
|
||||
// Fetch immediately, then poll
|
||||
@@ -51,6 +63,16 @@ class PerformanceStatsService {
|
||||
_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.
|
||||
void _startFpsTracking() {
|
||||
_frameCount = 0;
|
||||
@@ -79,9 +101,17 @@ class PerformanceStatsService {
|
||||
/// Fetch all performance stats from the player.
|
||||
Future<void> _fetchStats() async {
|
||||
try {
|
||||
if (_isExoPlayer) {
|
||||
await _fetchExoPlayerStats();
|
||||
// Ensure we know the runtime type on first fetch
|
||||
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 {
|
||||
// For non-Android platforms, use MPV property queries
|
||||
await _fetchMpvStats();
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -89,10 +119,12 @@ class PerformanceStatsService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch stats from ExoPlayer via native method channel.
|
||||
Future<void> _fetchExoPlayerStats() async {
|
||||
final exoPlayer = player as PlayerAndroid;
|
||||
final statsMap = await exoPlayer.getStats();
|
||||
/// Fetch stats from Android player (ExoPlayer or MPV fallback).
|
||||
/// The native side returns appropriate stats based on the active backend.
|
||||
Future<void> _fetchAndroidStats() async {
|
||||
final androidPlayer = player as PlayerAndroid;
|
||||
final statsMap = await androidPlayer.getStats();
|
||||
final playerType = statsMap['playerType'] as String? ?? 'unknown';
|
||||
|
||||
// Get app memory usage
|
||||
int? appMemory;
|
||||
@@ -100,30 +132,72 @@ class PerformanceStatsService {
|
||||
appMemory = ProcessInfo.currentRss;
|
||||
} catch (_) {}
|
||||
|
||||
final stats = PerformanceStats(
|
||||
playerType: (statsMap['playerType'] as String?) ?? '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);
|
||||
if (playerType == 'mpv') {
|
||||
// Parse MPV stats format (returned when in fallback mode)
|
||||
final stats = PerformanceStats(
|
||||
playerType: 'mpv',
|
||||
videoCodec: _formatCodecName(statsMap['video-codec'] as String?),
|
||||
videoWidth: _parseInt(statsMap['video-params/w'] as String?),
|
||||
videoHeight: _parseInt(statsMap['video-params/h'] as String?),
|
||||
videoFps: _parseDouble(statsMap['container-fps'] as String?),
|
||||
actualFps: _parseDouble(statsMap['estimated-vf-fps'] as String?),
|
||||
videoBitrate: _parseInt(statsMap['video-bitrate'] as String?),
|
||||
hwdecCurrent: statsMap['hwdec-current'] as String?,
|
||||
audioCodec: _formatCodecName(statsMap['audio-codec-name'] as String?),
|
||||
audioSamplerate: _parseInt(statsMap['audio-params/samplerate'] as String?),
|
||||
audioChannels: statsMap['audio-params/hr-channels'] as String?,
|
||||
audioBitrate: _parseInt(statsMap['audio-bitrate'] as String?),
|
||||
avsyncChange: _parseDouble(statsMap['total-avsync-change'] as String?),
|
||||
cacheUsed: _parseInt(statsMap['cache-used'] as String?),
|
||||
cacheSpeed: _parseDouble(statsMap['cache-speed'] as String?),
|
||||
displayFps: _parseDouble(statsMap['display-fps'] as String?),
|
||||
frameDropCount: _parseInt(statsMap['frame-drop-count'] as String?),
|
||||
decoderFrameDropCount: _parseInt(statsMap['decoder-frame-drop-count'] as String?),
|
||||
cacheDuration: _parseDouble(statsMap['demuxer-cache-duration'] as String?),
|
||||
// Color/Format properties
|
||||
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")
|
||||
@@ -260,6 +334,8 @@ class PerformanceStatsService {
|
||||
|
||||
/// Dispose of the service and release resources.
|
||||
void dispose() {
|
||||
_backendSwitchedSubscription?.cancel();
|
||||
_backendSwitchedSubscription = null;
|
||||
stopPolling();
|
||||
_statsController.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user