use std::fmt;
use crate::colour::ContainerColour;
use crate::edit::EditList;
#[cfg(feature = "mkv")]
use crate::mac_language::bibliographic;
use crate::mac_language::macintosh_iso639;
#[cfg(feature = "mp4")]
use crate::mac_language::terminology;
use crate::matroska::MatroskaTrack;
use crate::metadata::MetadataItem;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FourCc(pub [u8; 4]);
impl FourCc {
pub const fn bytes(self) -> [u8; 4] {
self.0
}
}
impl fmt::Display for FourCc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for &byte in &self.0 {
if byte.is_ascii_graphic() || byte == b' ' {
write!(f, "{}", byte as char)?;
} else {
write!(f, "\\x{byte:02x}")?;
}
}
Ok(())
}
}
impl fmt::Debug for FourCc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FourCc({self})")
}
}
impl From<&[u8; 4]> for FourCc {
fn from(bytes: &[u8; 4]) -> Self {
Self(*bytes)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TrackKind {
Video,
Audio,
Subtitle,
Data,
Other,
}
impl TrackKind {
pub fn from_handler(handler: FourCc) -> Self {
match &handler.0 {
b"vide" => Self::Video,
b"soun" => Self::Audio,
b"sbtl" | b"subt" | b"text" | b"clcp" => Self::Subtitle,
b"meta" | b"tmcd" | b"hint" => Self::Data,
_ => Self::Other,
}
}
pub fn from_matroska_type(track_type: u64) -> Self {
match track_type {
1 => Self::Video,
2 => Self::Audio,
17 => Self::Subtitle,
32 | 33 => Self::Data,
_ => Self::Other,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Timebase {
pub num: u32,
pub den: u32,
}
impl Timebase {
pub const fn from_timescale(timescale: u32) -> Self {
Self {
num: 1,
den: timescale,
}
}
pub const fn from_nanoseconds(nanoseconds: u32) -> Self {
Self {
num: nanoseconds,
den: 1_000_000_000,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Language {
Iso639([u8; 3]),
Other(u16),
}
impl Language {
pub fn from_code(code: u16) -> Self {
if code < 0x400 || code == 0x7FFF {
return Self::Other(code);
}
let letters = [
(((code >> 10) & 0x1F) as u8) + 0x60,
(((code >> 5) & 0x1F) as u8) + 0x60,
((code & 0x1F) as u8) + 0x60,
];
if letters.iter().all(u8::is_ascii_lowercase) {
Self::Iso639(letters)
} else {
Self::Other(code)
}
}
pub fn iso639(&self) -> Option<&str> {
match self {
Self::Iso639(letters) => std::str::from_utf8(letters).ok(),
Self::Other(_) => None,
}
}
pub fn iso639_mapped(&self) -> Option<&str> {
match self {
Self::Iso639(_) => self.iso639(),
Self::Other(code) => macintosh_iso639(*code),
}
}
#[cfg(feature = "mkv")]
pub fn matroska_code(&self) -> &str {
bibliographic(self.iso639_mapped().unwrap_or("und"))
}
#[cfg(feature = "mp4")]
pub fn mdhd_form(self) -> Self {
self.iso639()
.map(terminology)
.and_then(|code| <[u8; 3]>::try_from(code.as_bytes()).ok())
.map_or(self, Self::Iso639)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CodecConfig {
Avc(Vec<u8>),
Hevc(Vec<u8>),
Av1(Vec<u8>),
Vp(Vec<u8>),
Esds(Vec<u8>),
Opus(Vec<u8>),
}
impl CodecConfig {
pub fn box_type(&self) -> FourCc {
FourCc(match self {
Self::Avc(_) => *b"avcC",
Self::Hevc(_) => *b"hvcC",
Self::Av1(_) => *b"av1C",
Self::Vp(_) => *b"vpcC",
Self::Esds(_) => *b"esds",
Self::Opus(_) => *b"dOps",
})
}
pub fn bytes(&self) -> &[u8] {
match self {
Self::Avc(bytes)
| Self::Hevc(bytes)
| Self::Av1(bytes)
| Self::Vp(bytes)
| Self::Esds(bytes)
| Self::Opus(bytes) => bytes,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PixelAspect {
pub h_spacing: u32,
pub v_spacing: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct VideoInfo {
pub width: u16,
pub height: u16,
pub pixel_aspect: Option<PixelAspect>,
pub colour: Option<ContainerColour>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct AudioInfo {
pub channels: u32,
pub sample_size: u32,
pub sample_rate: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SampleEntry {
pub fourcc: FourCc,
pub raw: Vec<u8>,
pub data_reference_index: u16,
pub codec_config: Option<CodecConfig>,
pub video: Option<VideoInfo>,
pub audio: Option<AudioInfo>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrackReference {
pub kind: FourCc,
pub track_ids: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Track {
pub id: u32,
pub kind: TrackKind,
pub handler: Option<FourCc>,
pub timebase: Timebase,
pub duration: Option<u64>,
pub language: Language,
pub matrix: [i32; 9],
pub display_width: u32,
pub display_height: u32,
pub sample_entries: Vec<SampleEntry>,
pub edit_list: EditList,
pub references: Vec<TrackReference>,
pub chapter_track: bool,
pub metadata: Vec<MetadataItem>,
pub sample_count: u64,
pub matroska: Option<MatroskaTrack>,
}
impl Track {
pub fn codec_id(&self) -> Option<&str> {
self.matroska.as_ref().map(|mkv| mkv.codec_id.as_str())
}
pub fn sample_entry(&self) -> Option<&SampleEntry> {
self.sample_entries.first()
}
pub fn codec(&self) -> Option<FourCc> {
self.sample_entry().map(|entry| entry.fourcc)
}
pub fn codec_config(&self) -> Option<&CodecConfig> {
self.sample_entry()
.and_then(|entry| entry.codec_config.as_ref())
}
pub fn video(&self) -> Option<&VideoInfo> {
self.sample_entry().and_then(|entry| entry.video.as_ref())
}
pub fn colour(&self) -> Option<ContainerColour> {
match &self.matroska {
Some(mkv) => mkv.video.as_ref().and_then(|video| video.colour),
None => self.video().and_then(|video| video.colour),
}
}
pub fn audio(&self) -> Option<&AudioInfo> {
self.sample_entry().and_then(|entry| entry.audio.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn language_decodes_packed_iso_codes() {
assert_eq!(Language::from_code(0x55C4).iso639(), Some("und"));
assert_eq!(Language::from_code(0x15C7).iso639(), Some("eng"));
}
#[test]
fn language_keeps_macintosh_codes() {
assert_eq!(Language::from_code(0), Language::Other(0));
assert_eq!(Language::from_code(0x7FFF), Language::Other(0x7FFF));
}
#[test]
fn iso639_keeps_its_contract_and_iso639_mapped_adds_the_table() {
let english = Language::from_code(0);
assert_eq!(english.iso639(), None);
assert_eq!(english.iso639_mapped(), Some("eng"));
assert_eq!(Language::from_code(0x15C7).iso639_mapped(), Some("eng"));
assert_eq!(Language::from_code(0x7FFF).iso639_mapped(), None);
assert_eq!(Language::Other(0x0400).iso639_mapped(), None);
}
#[cfg(feature = "mkv")]
#[test]
fn matroska_code_is_bibliographic_with_und_for_no_code() {
assert_eq!(Language::Iso639(*b"fra").matroska_code(), "fre");
assert_eq!(Language::Iso639(*b"fre").matroska_code(), "fre");
assert_eq!(Language::Other(1).matroska_code(), "fre");
assert_eq!(Language::Other(0x7FFF).matroska_code(), "und");
}
#[cfg(feature = "mp4")]
#[test]
fn mdhd_form_is_terminology_and_keeps_other_values() {
assert_eq!(
Language::Iso639(*b"fre").mdhd_form(),
Language::Iso639(*b"fra")
);
assert_eq!(
Language::Iso639(*b"eng").mdhd_form(),
Language::Iso639(*b"eng")
);
assert_eq!(Language::Other(1).mdhd_form(), Language::Other(1));
}
#[test]
fn fourcc_displays_unprintable_bytes_escaped() {
assert_eq!(FourCc(*b"avc1").to_string(), "avc1");
assert_eq!(FourCc([0xA9, b'n', b'a', b'm']).to_string(), "\\xa9nam");
}
}