Files
plezy/lib/utils/codec_utils.dart
T
edde746 3f55cc461e fix(player): show audio codec and bitrate in the performance overlay on ExoPlayer
The Android performance overlay showed only sample rate and channels for
EAC3, FLAC, DTS, TrueHD, and Opus tracks. The overlay reads the codec
from media3's Format.codecs, an RFC 6381 string only MP4/HLS provide
(hence AAC working), and the bitrate from Format.bitrate, which Matroska
carries only when the muxer wrote BPS statistics tags.

Fall back to the already-transmitted sample MIME type for the codec name
(audio and video), and measure the audio bitrate in the FFmpeg demuxer
from packet sizes over their pts span — the same source mpv uses for its
audio-bitrate property — when the container declares none.

close #2063
2026-08-23 03:44:45 +02:00

171 lines
5.3 KiB
Dart

import '../i18n/strings.g.dart';
/// Utility class for codec-related operations.
///
/// Provides centralized codec name mappings, file extension lookups,
/// and display name formatting.
class CodecUtils {
CodecUtils._();
static String getSubtitleExtension(String? codec) {
if (codec == null) return 'srt';
switch (codec.toLowerCase()) {
case 'subrip':
case 'srt':
return 'srt';
case 'ass':
case 'ssa':
return 'ass';
case 'webvtt':
case 'vtt':
return 'vtt';
case 'mov_text':
return 'srt';
case 'pgs':
case 'pgssub':
case 'hdmv_pgs_subtitle':
return 'sup';
case 'dvd_subtitle':
case 'dvdsub':
case 'vobsub':
case 'dvb_sub':
case 'dvb_subtitle':
return 'sub';
default:
return 'srt';
}
}
static bool isTextSubtitleCodec(String? codec) {
if (codec == null) return false;
return switch (codec.toLowerCase()) {
'srt' || 'subrip' || 'ass' || 'ssa' || 'webvtt' || 'vtt' || 'mov_text' => true,
_ => false,
};
}
/// Image-based (bitmap) subtitle codecs. Plex burns these into the video
/// when the selected output transport cannot carry a bitmap subtitle
/// rendition.
static bool isImageSubtitleCodec(String? codec) {
if (codec == null) return false;
return switch (codec.toLowerCase()) {
'pgs' ||
'pgssub' ||
'hdmv_pgs_subtitle' ||
'dvd_subtitle' ||
'dvdsub' ||
'vobsub' ||
'dvb_sub' ||
// Jellyfin's own spelling, which is what the transcode profile asks it to burn.
'dvbsub' ||
'dvb_subtitle' => true,
_ => false,
};
}
/// Subtitle codecs Plex can deliver in a transcode. Text codecs can become
/// segmented HLS WebVTT; image codecs can be burned into the video.
static bool isTranscodableSubtitleCodec(String? codec) {
return isTextSubtitleCodec(codec) || isImageSubtitleCodec(codec);
}
/// Formats a subtitle codec name to a user-friendly display format.
///
/// Converts internal codec names like 'SUBRIP' to friendly names like 'SRT'.
static String formatSubtitleCodec(String codec) {
final upper = codec.toUpperCase();
return switch (upper) {
'SUBRIP' => 'SRT',
'DVD_SUBTITLE' => 'DVD',
'WEBVTT' => 'VTT',
'HDMV_PGS_SUBTITLE' => 'PGS',
'MOV_TEXT' => 'MOV',
_ => upper,
};
}
/// Formats a video codec name to a user-friendly display format.
///
/// Converts internal codec names like 'hevc' to friendly names like 'HEVC'.
static String formatVideoCodec(String codec) {
final lower = codec.toLowerCase();
return switch (lower) {
'h264' || 'avc1' || 'avc' => 'H.264',
'hevc' || 'h265' || 'hev1' => 'HEVC',
'av1' => 'AV1',
'vp8' => 'VP8',
'vp9' => 'VP9',
'mpeg2video' || 'mpeg2' => 'MPEG-2',
'mpeg4' => 'MPEG-4',
'vc1' => 'VC-1',
_ => codec.toUpperCase(),
};
}
/// Formats an audio channel count as a friendly layout name (2 → 'Stereo',
/// 6 → '5.1'). Returns null when [channels] is null or not positive.
static String? formatAudioChannels(int? channels) {
if (channels == null || channels <= 0) return null;
return switch (channels) {
1 => t.fileInfo.channelsMono,
2 => t.videoSettings.audioOutputStereo,
3 => '3.0',
4 => '4.0',
5 => '4.1',
6 => '5.1',
7 => '6.1',
8 => '7.1',
_ => '${channels}ch',
};
}
/// Formats an audio codec name to a user-friendly display format.
///
/// Accepts ffmpeg-style names as reported by mpv and the media
/// servers ('aac', 'eac3'), RFC 6381 codec IDs as reported by
/// ExoPlayer's `Format.codecs` ('mp4a.40.2', 'ec-3', 'dtsc'), and
/// `audio/...` MIME types as reported by ExoPlayer's
/// `Format.sampleMimeType` ('audio/eac3', 'audio/vnd.dts').
static String formatAudioCodec(String codec) {
final lower = codec.toLowerCase();
if (lower.startsWith('audio/')) {
return switch (lower.substring('audio/'.length)) {
'mp4a-latm' => 'AAC',
'mpeg' || 'mpeg-l2' => 'MP3',
'true-hd' => 'TrueHD',
'vnd.dts' => 'DTS',
'vnd.dts.hd' || 'vnd.dts.hd;profile=lbr' => 'DTS-HD',
'vnd.dts.uhd;audio=p2' => 'DTS:X',
'ac3' => 'AC3',
'eac3' || 'eac3-joc' => 'E-AC3',
'ac4' => 'AC4',
'raw' || 'wav' => 'PCM',
'alac' => 'ALAC',
final rest => formatAudioCodec(rest),
};
}
// 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' || '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',
'vorbis' => 'Vorbis',
'pcm_s16le' || 'pcm_s24le' || 'pcm' => 'PCM',
_ => codec.toUpperCase(),
};
}
}