diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index a7daae54..88fc0802 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -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 { + 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?) { diff --git a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart index 0e13c08e..59ffbe92 100644 --- a/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart +++ b/lib/widgets/video_controls/widgets/performance_overlay/performance_stats_service.dart @@ -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? _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 _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 _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 _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 _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(); }