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');