fix: mpv fallback stats
This commit is contained in:
@@ -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?) {
|
||||||
|
|||||||
+108
-32
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user