mod criterion;
mod ctrm;
mod paramount;
mod pixelogic;
pub mod vocab;
use crate::disc::{DiscTitle, Stream};
use crate::sector::SectorReader;
use crate::udf::UdfFs;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct StreamLabel {
pub stream_number: u16,
pub stream_type: StreamLabelType,
pub language: String,
pub name: String,
pub purpose: LabelPurpose,
pub qualifier: LabelQualifier,
pub codec_hint: String,
pub variant: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StreamLabelType {
Audio,
Subtitle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelPurpose {
Normal,
Commentary,
Descriptive,
Score,
Ime,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LabelQualifier {
None,
Sdh,
DescriptiveService,
Forced,
}
type DetectFn = fn(&UdfFs) -> bool;
type ParseFn = fn(&mut dyn SectorReader, &UdfFs) -> Option<Vec<StreamLabel>>;
const PARSERS: &[(&str, DetectFn, ParseFn)] = &[
("paramount", paramount::detect, paramount::parse),
("criterion", criterion::detect, criterion::parse),
("pixelogic", pixelogic::detect, pixelogic::parse),
("ctrm", ctrm::detect, ctrm::parse),
];
pub fn apply(reader: &mut dyn SectorReader, udf: &UdfFs, titles: &mut [DiscTitle]) {
let labels = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extract(reader, udf)))
.unwrap_or_default();
if labels.is_empty() {
return;
}
for title in titles.iter_mut() {
let mut audio_idx: u16 = 0;
let mut sub_idx: u16 = 0;
for stream in &mut title.streams {
match stream {
Stream::Audio(a) => {
audio_idx += 1;
if let Some(label) = labels.iter().find(|l| {
l.stream_type == StreamLabelType::Audio && l.stream_number == audio_idx
}) {
a.purpose = label.purpose;
let mut parts = Vec::new();
if !label.variant.is_empty() {
parts.push(format!("({})", label.variant));
}
if !label.codec_hint.is_empty() {
parts.push(label.codec_hint.clone());
}
if !parts.is_empty() {
a.label = parts.join(" ");
} else if !label.name.is_empty() && label.purpose == LabelPurpose::Normal {
a.label = label.name.clone();
}
}
}
Stream::Subtitle(s) => {
sub_idx += 1;
if let Some(label) = labels.iter().find(|l| {
l.stream_type == StreamLabelType::Subtitle && l.stream_number == sub_idx
}) {
s.qualifier = label.qualifier;
if label.qualifier == LabelQualifier::Forced {
s.forced = true;
}
}
}
_ => {}
}
}
}
}
pub fn fill_defaults(titles: &mut [crate::disc::DiscTitle]) {
use crate::disc::Stream;
for title in titles.iter_mut() {
for stream in &mut title.streams {
match stream {
Stream::Audio(a) if a.label.is_empty() => {
a.label = generate_audio_label(&a.codec, &a.channels, a.secondary);
}
Stream::Video(v) if v.label.is_empty() => {
v.label =
generate_video_label(&v.codec, v.resolution.pixels(), &v.hdr, v.secondary);
}
Stream::Subtitle(s) if s.forced => {
}
_ => {}
}
}
}
}
fn generate_video_label(
codec: &crate::disc::Codec,
pixels: (u32, u32),
hdr: &crate::disc::HdrFormat,
secondary: bool,
) -> String {
use crate::disc::HdrFormat;
if secondary {
return match hdr {
HdrFormat::DolbyVision => "Dolby Vision EL".to_string(),
_ => String::new(),
};
}
let mut parts = Vec::new();
parts.push(codec.name().to_string());
let (w, h) = pixels;
let res = if w >= 7680 {
"8K"
} else if w >= 3840 {
"4K"
} else if w >= 1920 {
"1080p"
} else if w >= 1280 {
"720p"
} else if h >= 576 {
"576p"
} else if h >= 480 {
"480p"
} else {
""
};
if !res.is_empty() {
parts.push(res.into());
}
match hdr {
HdrFormat::Sdr => {}
_ => parts.push(hdr.name().to_string()),
}
parts.join(" ")
}
fn generate_audio_label(
codec: &crate::disc::Codec,
channels: &crate::disc::AudioChannels,
_secondary: bool,
) -> String {
use crate::disc::{AudioChannels, Codec};
let codec_name = match codec {
Codec::TrueHd => "Dolby TrueHD",
Codec::Ac3 => "Dolby Digital",
Codec::Ac3Plus => "Dolby Digital Plus",
Codec::DtsHdMa => "DTS-HD Master Audio",
Codec::DtsHdHr => "DTS-HD High Resolution",
Codec::Dts => "DTS",
Codec::Lpcm => "LPCM",
Codec::Aac => "AAC",
Codec::Mp2 => "MPEG Audio",
Codec::Mp3 => "MP3",
Codec::Flac => "FLAC",
Codec::Opus => "Opus",
_ => return String::new(),
};
let channel_str = match channels {
AudioChannels::Mono => "1.0",
AudioChannels::Stereo => "2.0",
AudioChannels::Stereo21 => "2.1",
AudioChannels::Quad => "4.0",
AudioChannels::Surround50 => "5.0",
AudioChannels::Surround51 => "5.1",
AudioChannels::Surround61 => "6.1",
AudioChannels::Surround71 => "7.1",
AudioChannels::Unknown => "",
};
if channel_str.is_empty() {
codec_name.to_string()
} else {
format!("{} {}", codec_name, channel_str)
}
}
fn extract(reader: &mut dyn SectorReader, udf: &UdfFs) -> Vec<StreamLabel> {
for (_name, detect, parse) in PARSERS {
if detect(udf) {
if let Some(labels) = parse(reader, udf) {
return labels;
}
}
}
Vec::new()
}
pub(crate) fn jar_file_exists(udf: &UdfFs, filename: &str) -> bool {
find_jar_file(udf, filename).is_some()
}
pub(crate) fn find_jar_file(udf: &UdfFs, filename: &str) -> Option<String> {
let jar_dir = udf.find_dir("/BDMV/JAR")?;
for entry in &jar_dir.entries {
if entry.is_dir {
let path = format!("/BDMV/JAR/{}/{}", entry.name, filename);
for child in &entry.entries {
if !child.is_dir && child.name.eq_ignore_ascii_case(filename) {
return Some(path);
}
}
}
}
None
}
pub(crate) fn read_jar_file(
reader: &mut dyn SectorReader,
udf: &UdfFs,
filename: &str,
) -> Option<Vec<u8>> {
let path = find_jar_file(udf, filename)?;
udf.read_file(reader, &path).ok().filter(|d| !d.is_empty())
}