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
171 lines
5.3 KiB
Dart
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(),
|
|
};
|
|
}
|
|
}
|