fix: performance overlay for exoplayer
This commit is contained in:
@@ -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?) {
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+148
-83
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user