fix: performance overlay for exoplayer

This commit is contained in:
edde746
2026-01-23 19:11:46 +01:00
parent 943f7dab08
commit 3a2f2c9c69
8 changed files with 352 additions and 129 deletions
@@ -228,24 +228,26 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
surfaceContainer!!.addView(surfaceView)
// Create SubtitleView - will be added to surfaceContainer above video
// With OVERLAY_CANVAS mode, libass-android adds AssSubtitleView as a child
// which renders ASS subtitles with full styling
// Create SubtitleView - added to surfaceContainer above video
// With OVERLAY_OPEN_GL mode, libass-android adds AssSubtitleTextureView as a child
// which renders ASS subtitles with full styling using GPU texture composition
subtitleView = SubtitleView(activity).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
Log.d(TAG, "SubtitleView created")
// Add SubtitleView to surfaceContainer (above video SurfaceView)
// Flutter renders on top of entire surfaceContainer, keeping subtitles below UI
surfaceContainer!!.addView(subtitleView)
Log.d(TAG, "SubtitleView created and added to surfaceContainer")
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
contentView.addView(surfaceContainer, 0)
// Find FlutterView and configure z-order
// Video SurfaceView is at the bottom (setZOrderOnTop=false, setZOrderMediaOverlay=false)
// Flutter SurfaceView uses setZOrderMediaOverlay to render above video
// SubtitleView will be added to surfaceContainer so Flutter stays on top
// Flutter SurfaceView uses setZOrderMediaOverlay to render above video and subtitles
for (i in 0 until contentView.childCount) {
val child = contentView.getChildAt(i)
if (child is ViewGroup && child.javaClass.name.contains("FlutterView")) {
@@ -265,22 +267,6 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
}
}
// Add SubtitleView just below FlutterView so it stays above video but below Flutter UI
var flutterViewIndex: Int? = null
for (i in 0 until contentView.childCount) {
val child = contentView.getChildAt(i)
if (child is ViewGroup && child.javaClass.name.contains("FlutterView")) {
flutterViewIndex = i
break
}
}
if (flutterViewIndex != null) {
contentView.addView(subtitleView, flutterViewIndex)
Log.d(TAG, "SubtitleView added below FlutterView at index $flutterViewIndex")
} else {
contentView.addView(subtitleView)
Log.d(TAG, "SubtitleView added to contentView (FlutterView not found)")
}
ensureFlutterOverlayOnTop()
overlayLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
ensureFlutterOverlayOnTop()
@@ -317,15 +303,16 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
val dataSourceFactory = DefaultDataSource.Factory(activity)
val extractorsFactory = DefaultExtractorsFactory()
// Use buildWithAssSupport with OVERLAY_CANVAS mode for proper libass rendering
// This renders ASS subtitles with full styling via AssSubtitleView
// Use buildWithAssSupport with OVERLAY_OPEN_GL mode for proper libass rendering
// OVERLAY_OPEN_GL uses TextureView which follows normal View hierarchy z-ordering,
// preventing hardware overlay promotion issues on devices like Nvidia Shield
Log.d(TAG, "SubtitleView childCount before buildWithAssSupport: ${subtitleView?.childCount}")
exoPlayer = ExoPlayer.Builder(activity)
.setTrackSelector(trackSelector!!)
.setAudioAttributes(audioAttributes, false) // We handle audio focus manually
.buildWithAssSupport(
context = activity,
renderType = AssRenderType.OVERLAY_CANVAS, // Use OVERLAY mode for libass styling
renderType = AssRenderType.OVERLAY_OPEN_GL, // Use OVERLAY_OPEN_GL to fix z-ordering on Nvidia Shield
subtitleView = subtitleView,
dataSourceFactory = dataSourceFactory,
extractorsFactory = extractorsFactory,
@@ -1049,6 +1036,75 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
}
}
// Stats
fun getStats(): Map<String, Any?> {
val player = exoPlayer ?: return emptyMap()
val videoFormat = player.videoFormat
val audioFormat = player.audioFormat
// Get decoder info from the format's codecs field and check if hardware accelerated
val videoDecoderInfo = getVideoDecoderInfo(videoFormat)
return mapOf(
// Video metrics
"videoCodec" to videoFormat?.codecs,
"videoMimeType" to videoFormat?.sampleMimeType,
"videoWidth" to videoFormat?.width,
"videoHeight" to videoFormat?.height,
"videoFps" to videoFormat?.frameRate,
"videoBitrate" to videoFormat?.bitrate,
"videoDecoderName" to videoDecoderInfo,
"videoDroppedFrames" to player.videoDecoderCounters?.droppedBufferCount,
"videoRenderedFrames" to player.videoDecoderCounters?.renderedOutputBufferCount,
// Color info
"colorSpace" to videoFormat?.colorInfo?.colorSpace,
"colorRange" to videoFormat?.colorInfo?.colorRange,
"colorTransfer" to videoFormat?.colorInfo?.colorTransfer,
"hdrStaticInfo" to (videoFormat?.colorInfo?.hdrStaticInfo != null),
// Audio metrics
"audioCodec" to audioFormat?.codecs,
"audioMimeType" to audioFormat?.sampleMimeType,
"audioSampleRate" to audioFormat?.sampleRate,
"audioChannels" to audioFormat?.channelCount,
"audioBitrate" to audioFormat?.bitrate,
// Buffer metrics
"bufferedPositionMs" to player.bufferedPosition,
"currentPositionMs" to player.currentPosition,
"totalBufferedDurationMs" to player.totalBufferedDuration,
// Playback state
"playbackSpeed" to player.playbackParameters.speed,
"isPlaying" to player.isPlaying,
"playbackState" to player.playbackState,
)
}
private fun getVideoDecoderInfo(videoFormat: androidx.media3.common.Format?): String? {
if (videoFormat == null) return null
val mimeType = videoFormat.sampleMimeType ?: return null
// Check available decoders for this mime type
try {
val codecList = android.media.MediaCodecList(android.media.MediaCodecList.ALL_CODECS)
for (info in codecList.codecInfos) {
if (info.isEncoder) continue
for (type in info.supportedTypes) {
if (type.equals(mimeType, ignoreCase = true)) {
// Return the first hardware decoder found, or software if none
val name = info.name
if (!name.startsWith("OMX.google.") && !name.contains(".sw.")) {
return name // Hardware decoder
}
}
}
}
// Fallback - assume software if no HW decoder found
return "Software"
} catch (e: Exception) {
return null
}
}
// Cleanup
fun dispose() {
@@ -115,6 +115,8 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
if (usingMpvFallback) mpvCore?.isInitialized ?: false
else playerCore?.isInitialized ?: false
)
"getStats" -> handleGetStats(result)
"getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
else -> result.notImplemented()
}
}
@@ -406,6 +408,19 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
result.success(null)
}
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")
} else {
val coreStats = playerCore?.getStats() ?: emptyMap()
coreStats + mapOf("playerType" to "exoplayer")
}
result.success(stats)
} ?: result.success(mapOf("playerType" to "unknown"))
}
// ExoPlayerDelegate
override fun onPropertyChange(name: String, value: Any?) {
+3
View File
@@ -48,6 +48,9 @@ abstract class Player {
/// rendering is initialized. Returns null if not ready.
int? get textureId;
/// The type of player backend being used (e.g., 'mpv', 'exoplayer').
String get playerType;
// ============================================
// Playback Control
// ============================================
+26
View File
@@ -28,6 +28,9 @@ class PlayerAndroid implements Player {
@override
int? get textureId => null; // Uses SurfaceView, not Flutter texture
@override
String get playerType => 'exoplayer';
// Stream controllers
final _playingController = StreamController<bool>.broadcast();
final _completedController = StreamController<bool>.broadcast();
@@ -495,6 +498,29 @@ class PlayerAndroid implements Player {
}
}
/// Get all playback stats from ExoPlayer.
/// Returns a map with video/audio codec info, buffer state, and performance metrics.
Future<Map<String, dynamic>> getStats() async {
_checkDisposed();
try {
final result = await _methodChannel.invokeMethod<Map>('getStats');
return Map<String, dynamic>.from(result ?? {});
} catch (e) {
return {};
}
}
/// Get the current player type ('exoplayer' or 'mpv' if fallback is active).
Future<String> getPlayerType() async {
_checkDisposed();
try {
final result = await _methodChannel.invokeMethod<String>('getPlayerType');
return result ?? 'unknown';
} catch (e) {
return 'unknown';
}
}
@override
Future<void> command(List<String> args) async {
_checkDisposed();
+3
View File
@@ -30,6 +30,9 @@ class PlayerNative implements Player {
@override
int? get textureId => null; // Uses direct Metal layer, not Flutter texture
@override
String get playerType => 'mpv';
// Stream controllers
final _playingController = StreamController<bool>.broadcast();
final _completedController = StreamController<bool>.broadcast();
@@ -45,6 +45,8 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
@override
Widget build(BuildContext context) {
final isMpv = _stats.playerType == 'mpv';
return Container(
constraints: const BoxConstraints(maxWidth: 380),
padding: const EdgeInsets.all(12),
@@ -66,21 +68,24 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
_buildSection(Symbols.videocam_rounded, 'Video', [
_metric('Codec', _stats.videoCodec ?? 'N/A'),
_metric('Resolution', _stats.resolution),
_metric('FPS', _stats.videoFpsFormatted),
_metric('Bitrate', _stats.videoBitrateFormatted),
if (_stats.hasValidVideoFps) _metric('FPS', _stats.videoFpsFormatted),
if (_stats.hasValidVideoBitrate) _metric('Bitrate', _stats.videoBitrateFormatted),
_metric('Decoder', _stats.hwdecFormatted),
if (_stats.aspectName != null && _stats.aspectName!.isNotEmpty) _metric('Aspect', _stats.aspectName!),
if (_stats.rotate != null && _stats.rotate != 0) _metric('Rotation', _stats.rotateFormatted),
]),
const SizedBox(height: 12),
_buildSection(Symbols.palette_rounded, 'Color', [
_metric('Pixel Fmt', _stats.pixelformat ?? 'N/A'),
if (_stats.hwPixelformat != null && _stats.hwPixelformat != _stats.pixelformat)
_metric('HW Fmt', _stats.hwPixelformat!),
_metric('Matrix', _stats.colormatrix ?? 'N/A'),
_metric('Primaries', _stats.primaries ?? 'N/A'),
_metric('Transfer', _stats.gamma ?? 'N/A'),
]),
// Color section - MPV only (ExoPlayer doesn't provide this info)
if (isMpv) ...[
const SizedBox(height: 12),
_buildSection(Symbols.palette_rounded, 'Color', [
_metric('Pixel Fmt', _stats.pixelformat ?? 'N/A'),
if (_stats.hwPixelformat != null && _stats.hwPixelformat != _stats.pixelformat)
_metric('HW Fmt', _stats.hwPixelformat!),
_metric('Matrix', _stats.colormatrix ?? 'N/A'),
_metric('Primaries', _stats.primaries ?? 'N/A'),
_metric('Transfer', _stats.gamma ?? 'N/A'),
]),
],
if (_stats.hasHdrMetadata) ...[
const SizedBox(height: 12),
_buildSection(Symbols.hdr_on_rounded, 'HDR', [
@@ -101,26 +106,27 @@ class _PlayerPerformanceOverlayState extends State<PlayerPerformanceOverlay> {
mainAxisSize: MainAxisSize.min,
children: [
_buildSection(Symbols.volume_up_rounded, 'Audio', [
_metric('Codec', _stats.audioCodec ?? 'N/A'),
if (_stats.audioCodec != null) _metric('Codec', _stats.audioCodec!),
_metric('Sample Rate', _stats.sampleRateFormatted),
_metric('Channels', _stats.audioChannels ?? 'N/A'),
_metric('Bitrate', _stats.audioBitrateFormatted),
if (_stats.hasValidAudioBitrate) _metric('Bitrate', _stats.audioBitrateFormatted),
]),
const SizedBox(height: 12),
_buildSection(Symbols.speed_rounded, 'Performance', [
_metric('Render FPS', _stats.actualFpsFormatted),
_metric('Display FPS', _stats.displayFpsFormatted),
_metric('A/V Sync', _stats.avsyncFormatted),
if (isMpv) _metric('Render FPS', _stats.actualFpsFormatted),
if (isMpv) _metric('Display FPS', _stats.displayFpsFormatted),
if (isMpv) _metric('A/V Sync', _stats.avsyncFormatted),
_metric('Dropped', _stats.droppedFramesFormatted),
]),
const SizedBox(height: 12),
_buildSection(Symbols.memory_rounded, 'Buffer', [
_metric('Duration', _stats.cacheDurationFormatted),
_metric('Cache Used', _stats.cacheUsedFormatted),
_metric('Speed', _stats.cacheSpeedFormatted),
if (isMpv) _metric('Cache Used', _stats.cacheUsedFormatted),
if (isMpv) _metric('Speed', _stats.cacheSpeedFormatted),
]),
const SizedBox(height: 12),
_buildSection(Symbols.apps_rounded, 'App', [
_metric('Player', _stats.playerTypeFormatted),
_metric('Memory', _stats.appMemoryFormatted),
_metric('UI FPS', _stats.uiFpsFormatted),
]),
@@ -1,8 +1,11 @@
/// Data model for video player performance statistics.
///
/// Contains metrics queried from libmpv including video/audio codec info,
/// playback performance, and buffer state.
/// Contains metrics queried from the video player (MPV or ExoPlayer)
/// including video/audio codec info, playback performance, and buffer state.
class PerformanceStats {
// Player info
final String playerType; // 'mpv' or 'exoplayer'
// Video metrics
final String? videoCodec;
final int? videoWidth;
@@ -12,6 +15,7 @@ class PerformanceStats {
final int? videoBitrate;
final String? aspectName;
final int? rotate;
final String? videoDecoderName;
// Color/Format metrics
final String? pixelformat;
@@ -49,6 +53,7 @@ class PerformanceStats {
final double? uiFps;
const PerformanceStats({
this.playerType = 'unknown',
this.videoCodec,
this.videoWidth,
this.videoHeight,
@@ -57,6 +62,7 @@ class PerformanceStats {
this.videoBitrate,
this.aspectName,
this.rotate,
this.videoDecoderName,
this.pixelformat,
this.hwPixelformat,
this.colormatrix,
@@ -84,7 +90,8 @@ class PerformanceStats {
/// Creates an empty stats object (used as initial state).
const PerformanceStats.empty()
: videoCodec = null,
: playerType = 'unknown',
videoCodec = null,
videoWidth = null,
videoHeight = null,
videoFps = null,
@@ -92,6 +99,7 @@ class PerformanceStats {
videoBitrate = null,
aspectName = null,
rotate = null,
videoDecoderName = null,
pixelformat = null,
hwPixelformat = null,
colormatrix = null,
@@ -196,6 +204,23 @@ class PerformanceStats {
/// Format hardware decoding mode.
String get hwdecFormatted {
// For ExoPlayer, use the decoder name
if (videoDecoderName != null && videoDecoderName!.isNotEmpty) {
// Check if it's a hardware decoder (contains OMX, c2, or MediaCodec patterns)
final decoder = videoDecoderName!;
if (decoder.contains('c2.') || decoder.contains('OMX.') || decoder.contains('.hw.')) {
// Extract a cleaner name
if (decoder.contains('c2.android.')) return 'Android HW';
if (decoder.contains('c2.nvidia')) return 'NVIDIA HW';
if (decoder.contains('c2.qti') || decoder.contains('c2.qcom')) return 'Qualcomm HW';
if (decoder.contains('c2.mtk') || decoder.contains('c2.mediatek')) return 'MediaTek HW';
if (decoder.contains('c2.exynos') || decoder.contains('c2.samsung')) return 'Exynos HW';
if (decoder.contains('OMX.google')) return 'Software';
return 'Hardware';
}
return 'Software';
}
// For MPV, use hwdec-current property
if (hwdecCurrent == null || hwdecCurrent!.isEmpty || hwdecCurrent == 'no') {
return 'Software';
}
@@ -249,4 +274,28 @@ class PerformanceStats {
bool get hasHdrMetadata {
return maxLuma != null || maxCll != null;
}
/// Check if video FPS is valid (not null, not negative, not zero).
bool get hasValidVideoFps {
return videoFps != null && videoFps! > 0;
}
/// Check if video bitrate is valid (not null, not negative, not zero).
bool get hasValidVideoBitrate {
return videoBitrate != null && videoBitrate! > 0;
}
/// Check if audio bitrate is valid (not null, not negative, not zero).
bool get hasValidAudioBitrate {
return audioBitrate != null && audioBitrate! > 0;
}
/// Format player type for display.
String get playerTypeFormatted {
return switch (playerType.toLowerCase()) {
'mpv' => 'MPV',
'exoplayer' => 'ExoPlayer',
_ => playerType,
};
}
}
@@ -4,10 +4,13 @@ import 'dart:io' show ProcessInfo;
import 'package:flutter/scheduler.dart';
import '../../../../mpv/mpv.dart';
import '../../../../mpv/player/player_android.dart';
import '../../../../utils/app_logger.dart';
import 'performance_stats.dart';
/// Service that polls MPV 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.
///
/// Usage:
/// ```dart
@@ -30,6 +33,9 @@ class PerformanceStatsService {
DateTime _lastFpsUpdate = DateTime.now();
double? _currentUiFps;
/// Whether we're using ExoPlayer (Android) or MPV
bool get _isExoPlayer => player is PlayerAndroid;
PerformanceStatsService(this.player);
/// Stream of performance stats updates.
@@ -70,96 +76,155 @@ class PerformanceStatsService {
_pollingTimer = null;
}
/// Fetch all performance stats from MPV.
/// Fetch all performance stats from the player.
Future<void> _fetchStats() async {
try {
// Fetch all properties in parallel for efficiency
final results = await Future.wait([
player.getProperty('video-codec'), // 0
player.getProperty('video-params/w'), // 1
player.getProperty('video-params/h'), // 2
player.getProperty('container-fps'), // 3
player.getProperty('estimated-vf-fps'), // 4
player.getProperty('video-bitrate'), // 5
player.getProperty('hwdec-current'), // 6
player.getProperty('audio-codec-name'), // 7
player.getProperty('audio-params/samplerate'), // 8
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
]);
// Get app memory usage
int? appMemory;
try {
appMemory = ProcessInfo.currentRss;
} catch (_) {
// ProcessInfo not available on all platforms
if (_isExoPlayer) {
await _fetchExoPlayerStats();
} else {
await _fetchMpvStats();
}
final stats = PerformanceStats(
videoCodec: _formatCodecName(results[0]),
videoWidth: _parseInt(results[1]),
videoHeight: _parseInt(results[2]),
videoFps: _parseDouble(results[3]),
actualFps: _parseDouble(results[4]),
videoBitrate: _parseInt(results[5]),
hwdecCurrent: results[6],
audioCodec: _formatCodecName(results[7]),
audioSamplerate: _parseInt(results[8]),
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]),
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);
_statsController.add(stats);
} catch (e) {
appLogger.w('Failed to fetch performance stats', error: e);
}
}
/// Fetch stats from ExoPlayer via native method channel.
Future<void> _fetchExoPlayerStats() async {
final exoPlayer = player as PlayerAndroid;
final statsMap = await exoPlayer.getStats();
// Get app memory usage
int? appMemory;
try {
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);
}
/// Format channel count to string (e.g., "2" -> "Stereo", "6" -> "5.1")
String? _formatChannels(int? channels) {
if (channels == null) return null;
return switch (channels) {
1 => 'Mono',
2 => 'Stereo',
6 => '5.1',
8 => '7.1',
_ => '$channels ch',
};
}
/// Fetch stats from MPV via property queries.
Future<void> _fetchMpvStats() async {
// Fetch all properties in parallel for efficiency
final results = await Future.wait([
player.getProperty('video-codec'), // 0
player.getProperty('video-params/w'), // 1
player.getProperty('video-params/h'), // 2
player.getProperty('container-fps'), // 3
player.getProperty('estimated-vf-fps'), // 4
player.getProperty('video-bitrate'), // 5
player.getProperty('hwdec-current'), // 6
player.getProperty('audio-codec-name'), // 7
player.getProperty('audio-params/samplerate'), // 8
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
]);
// Get app memory usage
int? appMemory;
try {
appMemory = ProcessInfo.currentRss;
} catch (_) {
// ProcessInfo not available on all platforms
}
final stats = PerformanceStats(
playerType: 'mpv',
videoCodec: _formatCodecName(results[0]),
videoWidth: _parseInt(results[1]),
videoHeight: _parseInt(results[2]),
videoFps: _parseDouble(results[3]),
actualFps: _parseDouble(results[4]),
videoBitrate: _parseInt(results[5]),
hwdecCurrent: results[6],
audioCodec: _formatCodecName(results[7]),
audioSamplerate: _parseInt(results[8]),
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]),
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);
_statsController.add(stats);
}
/// Parse a string to int, returning null if parsing fails.
int? _parseInt(String? value) {
if (value == null || value.isEmpty) return null;