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
153 lines
4.6 KiB
Dart
153 lines
4.6 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 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' || '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(),
|
|
};
|
|
}
|
|
}
|