From ce151a91e8cd88c884fe09c63c2f8a83eac8c26b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:23:08 +0200 Subject: [PATCH] fix(player): show AAC instead of mp4a.40.2 in ExoPlayer track labels ExoPlayer reports RFC 6381 codec IDs (mp4a.40.2, ec-3, dtsc) where mpv reports ffmpeg names, so the audio track picker and performance overlay showed raw identifiers instead of friendly names on the ExoPlayer path. Normalize RFC 6381 audio IDs in CodecUtils.formatAudioCodec, route the performance overlay's audio codec through it, and cover the video IDs (hvc1/hev1, av01, vp09) that fell through the overlay's matcher. close #1899 --- lib/utils/codec_utils.dart | 21 ++++++--- .../performance_stats_service.dart | 43 ++++++++++--------- test/utils/codec_utils_test.dart | 23 ++++++++++ 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/lib/utils/codec_utils.dart b/lib/utils/codec_utils.dart index e686fa57e..8121af33c 100644 --- a/lib/utils/codec_utils.dart +++ b/lib/utils/codec_utils.dart @@ -122,16 +122,25 @@ class CodecUtils { } /// Formats an audio codec name to a user-friendly display format. + /// + /// Accepts both ffmpeg-style names as reported by mpv and the media + /// servers ('aac', 'eac3') and RFC 6381 codec IDs as reported by + /// ExoPlayer's `Format.codecs` ('mp4a.40.2', 'ec-3', 'dtsc'). static String formatAudioCodec(String codec) { final lower = codec.toLowerCase(); + // MP4 object types 0x69/0x6B under the mp4a prefix are MPEG layer + // audio; every other mp4a object type in the wild is an AAC variant. + if (lower == 'mp4a.69' || lower == 'mp4a.6b') return 'MP3'; + if (lower == 'mp4a' || lower.startsWith('mp4a.')) return 'AAC'; + if (lower == 'ac-4' || lower.startsWith('ac-4.')) return 'AC4'; return switch (lower) { 'aac' => 'AAC', - 'ac3' => 'AC3', - 'eac3' || 'ec3' => 'E-AC3', - 'truehd' => 'TrueHD', - 'dts' => 'DTS', - 'dca' => 'DTS', - 'dtshd' || 'dts-hd' => 'DTS-HD', + 'ac3' || 'ac-3' => 'AC3', + 'eac3' || 'ec3' || 'ec-3' => 'E-AC3', + 'truehd' || 'mlpa' => 'TrueHD', + 'dts' || 'dca' || 'dtsc' || 'dtse' => 'DTS', + 'dtshd' || 'dts-hd' || 'dtsh' || 'dtsl' => 'DTS-HD', + 'dtsx' => 'DTS:X', 'flac' => 'FLAC', 'mp3' || 'mp3float' => 'MP3', 'opus' => 'Opus', 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 530c1035f..149cdb0b6 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 @@ -150,14 +150,14 @@ class PerformanceStatsService { // Parse MPV stats format (returned when in fallback mode) final stats = PerformanceStats( playerType: 'mpv', - videoCodec: _formatCodecName(statsMap['video-codec'] as String?), + videoCodec: _formatVideoCodecName(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?), + audioCodec: _formatAudioCodecName(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?), @@ -192,14 +192,14 @@ class PerformanceStatsService { final stats = PerformanceStats( playerType: 'exoplayer', // Video metrics - videoCodec: _formatCodecName(statsMap['videoCodec'] as String?), + videoCodec: _formatVideoCodecName(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?), + audioCodec: _formatAudioCodecName(statsMap['audioCodec'] as String?), audioSamplerate: statsMap['audioSampleRate'] as int?, audioChannels: CodecUtils.formatAudioChannels(statsMap['audioChannels'] as int?), audioBitrate: statsMap['audioBitrate'] as int?, @@ -291,14 +291,14 @@ class PerformanceStatsService { final stats = PerformanceStats( playerType: 'mpv', - videoCodec: _formatCodecName(results.first), + videoCodec: _formatVideoCodecName(results.first), 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]), + audioCodec: _formatAudioCodecName(results[7]), audioSamplerate: _parseInt(results[8]), audioChannels: results[9], audioBitrate: _parseInt(results[10]), @@ -345,27 +345,28 @@ class PerformanceStatsService { return double.tryParse(value); } - /// Format codec name for display (uppercase common codecs). - String? _formatCodecName(String? codec) { + /// Format a video codec name for display. Handles mpv's descriptive + /// strings ('hevc (Main 10)') and ExoPlayer's RFC 6381 codec IDs + /// ('hvc1.2.4.L153.B0', 'av01.0.08M.10'). + String? _formatVideoCodecName(String? codec) { if (codec == null || codec.isEmpty) return null; - // Common codec name mappings final upper = codec.toUpperCase(); - if (upper.contains('HEVC') || upper.contains('H265')) return 'HEVC'; + if (upper.contains('HEVC') || upper.contains('H265') || upper.contains('HVC1') || upper.contains('HEV1')) { + return 'HEVC'; + } if (upper.contains('H264') || upper.contains('AVC')) return 'H.264'; - if (upper.contains('AV1')) return 'AV1'; - if (upper.contains('VP9')) return 'VP9'; - if (upper.contains('AAC')) return 'AAC'; - if (upper.contains('AC3') || upper.contains('AC-3')) return 'AC3'; - if (upper.contains('EAC3') || upper.contains('E-AC-3')) return 'EAC3'; - if (upper.contains('DTS')) return 'DTS'; - if (upper.contains('TRUEHD')) return 'TrueHD'; - if (upper.contains('FLAC')) return 'FLAC'; - if (upper.contains('OPUS')) return 'Opus'; - if (upper.contains('VORBIS')) return 'Vorbis'; - if (upper.contains('MP3')) return 'MP3'; + if (upper.contains('AV1') || upper.contains('AV01')) return 'AV1'; + if (upper.contains('VP9') || upper.contains('VP09')) return 'VP9'; return codec; } + /// Format an audio codec name for display. Both mpv's ffmpeg names and + /// ExoPlayer's RFC 6381 codec IDs go through the shared mapping. + String? _formatAudioCodecName(String? codec) { + if (codec == null || codec.isEmpty) return null; + return CodecUtils.formatAudioCodec(codec); + } + /// Dispose of the service and release resources. void dispose() { if (_disposed) return; diff --git a/test/utils/codec_utils_test.dart b/test/utils/codec_utils_test.dart index a25f6802f..5ba27da69 100644 --- a/test/utils/codec_utils_test.dart +++ b/test/utils/codec_utils_test.dart @@ -162,6 +162,29 @@ void main() { expect(CodecUtils.formatAudioCodec('dts-hd'), 'DTS-HD'); }); + test('RFC 6381 codec IDs from ExoPlayer map to friendly names', () { + // AAC-LC as reported for MP4/HLS direct play (#1899). + expect(CodecUtils.formatAudioCodec('mp4a.40.2'), 'AAC'); + expect(CodecUtils.formatAudioCodec('MP4A.40.2'), 'AAC'); + expect(CodecUtils.formatAudioCodec('mp4a.40.5'), 'AAC'); + expect(CodecUtils.formatAudioCodec('mp4a.40.29'), 'AAC'); + expect(CodecUtils.formatAudioCodec('mp4a'), 'AAC'); + // MPEG layer audio object types under the mp4a prefix. + expect(CodecUtils.formatAudioCodec('mp4a.69'), 'MP3'); + expect(CodecUtils.formatAudioCodec('mp4a.6B'), 'MP3'); + // Dolby and DTS sample entry names. + expect(CodecUtils.formatAudioCodec('ac-3'), 'AC3'); + expect(CodecUtils.formatAudioCodec('ec-3'), 'E-AC3'); + expect(CodecUtils.formatAudioCodec('ac-4'), 'AC4'); + expect(CodecUtils.formatAudioCodec('ac-4.02.01.01'), 'AC4'); + expect(CodecUtils.formatAudioCodec('mlpa'), 'TrueHD'); + expect(CodecUtils.formatAudioCodec('dtsc'), 'DTS'); + expect(CodecUtils.formatAudioCodec('dtse'), 'DTS'); + expect(CodecUtils.formatAudioCodec('dtsh'), 'DTS-HD'); + expect(CodecUtils.formatAudioCodec('dtsl'), 'DTS-HD'); + expect(CodecUtils.formatAudioCodec('dtsx'), 'DTS:X'); + }); + test('mp3 aliases', () { expect(CodecUtils.formatAudioCodec('mp3'), 'MP3'); expect(CodecUtils.formatAudioCodec('mp3float'), 'MP3');