use super::atom::Mp4Atom;
use oximedia_core::{OxiError, OxiResult};
#[derive(Clone, Debug)]
pub struct BoxHeader {
pub size: u64,
pub box_type: BoxType,
pub header_size: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BoxType(pub [u8; 4]);
impl BoxType {
#[must_use]
pub const fn new(bytes: [u8; 4]) -> Self {
Self(bytes)
}
#[must_use]
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
let bytes = s.as_bytes();
Self([
bytes.first().copied().unwrap_or(0),
bytes.get(1).copied().unwrap_or(0),
bytes.get(2).copied().unwrap_or(0),
bytes.get(3).copied().unwrap_or(0),
])
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap_or("????")
}
#[must_use]
pub const fn as_u32(&self) -> u32 {
u32::from_be_bytes(self.0)
}
}
impl std::fmt::Display for BoxType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl BoxType {
pub const FTYP: Self = Self::new(*b"ftyp");
pub const MOOV: Self = Self::new(*b"moov");
pub const MVHD: Self = Self::new(*b"mvhd");
pub const TRAK: Self = Self::new(*b"trak");
pub const TKHD: Self = Self::new(*b"tkhd");
pub const MDIA: Self = Self::new(*b"mdia");
pub const MDHD: Self = Self::new(*b"mdhd");
pub const HDLR: Self = Self::new(*b"hdlr");
pub const MINF: Self = Self::new(*b"minf");
pub const STBL: Self = Self::new(*b"stbl");
pub const STSD: Self = Self::new(*b"stsd");
pub const STTS: Self = Self::new(*b"stts");
pub const STSC: Self = Self::new(*b"stsc");
pub const STSZ: Self = Self::new(*b"stsz");
pub const STCO: Self = Self::new(*b"stco");
pub const CO64: Self = Self::new(*b"co64");
pub const STSS: Self = Self::new(*b"stss");
pub const CTTS: Self = Self::new(*b"ctts");
pub const MDAT: Self = Self::new(*b"mdat");
pub const FREE: Self = Self::new(*b"free");
pub const SKIP: Self = Self::new(*b"skip");
pub const UDTA: Self = Self::new(*b"udta");
pub const META: Self = Self::new(*b"meta");
pub const EDTS: Self = Self::new(*b"edts");
pub const ELST: Self = Self::new(*b"elst");
}
impl BoxHeader {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < 8 {
return Err(OxiError::UnexpectedEof);
}
let size32 = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let mut box_type = [0u8; 4];
box_type.copy_from_slice(&data[4..8]);
let (size, header_size) = if size32 == 1 {
if data.len() < 16 {
return Err(OxiError::UnexpectedEof);
}
let size64 = u64::from_be_bytes([
data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
]);
(size64, 16)
} else if size32 == 0 {
(0, 8)
} else {
(u64::from(size32), 8)
};
Ok(Self {
size,
box_type: BoxType(box_type),
header_size,
})
}
#[must_use]
pub const fn content_size(&self) -> u64 {
if self.size == 0 {
0 } else {
self.size - self.header_size as u64
}
}
}
#[derive(Clone, Debug)]
pub struct FtypBox {
pub major_brand: BoxType,
pub minor_version: u32,
pub compatible_brands: Vec<BoxType>,
}
impl FtypBox {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < 8 {
return Err(OxiError::Parse {
offset: 0,
message: "ftyp box too short".into(),
});
}
let mut major_brand = [0u8; 4];
major_brand.copy_from_slice(&data[0..4]);
let minor_version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let mut compatible_brands = Vec::new();
let mut offset = 8;
while offset + 4 <= data.len() {
let mut brand = [0u8; 4];
brand.copy_from_slice(&data[offset..offset + 4]);
compatible_brands.push(BoxType(brand));
offset += 4;
}
Ok(Self {
major_brand: BoxType(major_brand),
minor_version,
compatible_brands,
})
}
#[must_use]
pub fn is_mp4(&self) -> bool {
let mp4_brands = [
BoxType::from_str("isom"),
BoxType::from_str("iso2"),
BoxType::from_str("iso3"),
BoxType::from_str("iso4"),
BoxType::from_str("iso5"),
BoxType::from_str("iso6"),
BoxType::from_str("mp41"),
BoxType::from_str("mp42"),
BoxType::from_str("M4V "),
BoxType::from_str("M4A "),
BoxType::from_str("M4P "),
BoxType::from_str("av01"), BoxType::from_str("avis"), ];
mp4_brands.contains(&self.major_brand)
|| self
.compatible_brands
.iter()
.any(|b| mp4_brands.contains(b))
}
}
#[derive(Clone, Debug, Default)]
pub struct MoovBox {
pub mvhd: Option<MvhdBox>,
pub traks: Vec<TrakBox>,
}
#[derive(Clone, Debug)]
pub struct MvhdBox {
pub version: u8,
pub creation_time: u64,
pub modification_time: u64,
pub timescale: u32,
pub duration: u64,
pub rate: f64,
pub volume: f64,
pub next_track_id: u32,
}
impl MvhdBox {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
let mut atom = Mp4Atom::new(data);
let version = atom.read_u8()?;
atom.skip(3)?;
let (creation_time, modification_time, timescale, duration) = if version == 1 {
(
atom.read_u64()?,
atom.read_u64()?,
atom.read_u32()?,
atom.read_u64()?,
)
} else {
(
u64::from(atom.read_u32()?),
u64::from(atom.read_u32()?),
atom.read_u32()?,
u64::from(atom.read_u32()?),
)
};
let rate = atom.read_fixed_16_16()?;
let volume = atom.read_fixed_8_8()?;
atom.skip(2 + 8)?;
atom.skip(36)?;
atom.skip(24)?;
let next_track_id = atom.read_u32()?;
Ok(Self {
version,
creation_time,
modification_time,
timescale,
duration,
rate,
volume,
next_track_id,
})
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn duration_seconds(&self) -> f64 {
if self.timescale == 0 {
0.0
} else {
self.duration as f64 / f64::from(self.timescale)
}
}
}
#[derive(Clone, Debug, Default)]
pub struct TrakBox {
pub tkhd: Option<TkhdBox>,
pub timescale: u32,
pub handler_type: String,
pub codec_tag: u32,
pub width: Option<u32>,
pub height: Option<u32>,
pub sample_rate: Option<u32>,
pub channels: Option<u16>,
pub stts_entries: Vec<SttsEntry>,
pub stsc_entries: Vec<StscEntry>,
pub sample_sizes: Vec<u32>,
pub default_sample_size: u32,
pub chunk_offsets: Vec<u64>,
pub sync_samples: Option<Vec<u32>>,
pub ctts_entries: Vec<CttsEntry>,
pub extradata: Option<Vec<u8>>,
}
#[derive(Clone, Debug)]
pub struct TkhdBox {
pub track_id: u32,
pub duration: u64,
pub width: f64,
pub height: f64,
}
impl TkhdBox {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
let mut atom = Mp4Atom::new(data);
let version = atom.read_u8()?;
atom.skip(3)?;
let (creation_time, modification_time, track_id, duration) = if version == 1 {
let ct = atom.read_u64()?;
let mt = atom.read_u64()?;
let tid = atom.read_u32()?;
atom.skip(4)?; let dur = atom.read_u64()?;
(ct, mt, tid, dur)
} else {
let ct = u64::from(atom.read_u32()?);
let mt = u64::from(atom.read_u32()?);
let tid = atom.read_u32()?;
atom.skip(4)?; let dur = u64::from(atom.read_u32()?);
(ct, mt, tid, dur)
};
let _ = (creation_time, modification_time);
atom.skip(8)?;
atom.skip(4)?;
atom.skip(4)?;
atom.skip(36)?;
let width = atom.read_fixed_16_16()?;
let height = atom.read_fixed_16_16()?;
Ok(Self {
track_id,
duration,
width,
height,
})
}
}
#[derive(Clone, Debug)]
pub struct SttsEntry {
pub sample_count: u32,
pub sample_delta: u32,
}
#[derive(Clone, Debug)]
pub struct StscEntry {
pub first_chunk: u32,
pub samples_per_chunk: u32,
pub sample_description_index: u32,
}
#[derive(Clone, Debug)]
pub struct CttsEntry {
pub sample_count: u32,
pub sample_offset: i32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_box_header_normal() {
let data = [0x00, 0x00, 0x00, 0x14, b'f', b't', b'y', b'p'];
let header = BoxHeader::parse(&data).unwrap();
assert_eq!(header.size, 20);
assert_eq!(header.box_type, BoxType::FTYP);
assert_eq!(header.header_size, 8);
assert_eq!(header.content_size(), 12);
}
#[test]
fn test_box_header_extended() {
let data = [
0x00, 0x00, 0x00, 0x01, b'm', b'd', b'a', b't', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, ];
let header = BoxHeader::parse(&data).unwrap();
assert_eq!(header.size, 256);
assert_eq!(header.box_type, BoxType::MDAT);
assert_eq!(header.header_size, 16);
assert_eq!(header.content_size(), 240);
}
#[test]
fn test_box_header_to_eof() {
let data = [0x00, 0x00, 0x00, 0x00, b'm', b'd', b'a', b't'];
let header = BoxHeader::parse(&data).unwrap();
assert_eq!(header.size, 0);
assert_eq!(header.content_size(), 0);
}
#[test]
fn test_box_type_constants() {
assert_eq!(BoxType::FTYP.as_str(), "ftyp");
assert_eq!(BoxType::MOOV.as_str(), "moov");
assert_eq!(BoxType::MDAT.as_str(), "mdat");
}
#[test]
fn test_box_type_from_str() {
assert_eq!(BoxType::from_str("moov"), BoxType::MOOV);
assert_eq!(BoxType::from_str("ftyp"), BoxType::FTYP);
}
#[test]
fn test_box_type_display() {
assert_eq!(format!("{}", BoxType::FTYP), "ftyp");
}
#[test]
fn test_ftyp_parse() {
let data = [
b'i', b's', b'o', b'm', 0x00, 0x00, 0x00, 0x00, b'm', b'p', b'4', b'1', ];
let ftyp = FtypBox::parse(&data).unwrap();
assert_eq!(ftyp.major_brand.as_str(), "isom");
assert_eq!(ftyp.minor_version, 0);
assert_eq!(ftyp.compatible_brands.len(), 1);
assert_eq!(ftyp.compatible_brands[0].as_str(), "mp41");
assert!(ftyp.is_mp4());
}
#[test]
fn test_ftyp_is_mp4() {
let ftyp = FtypBox {
major_brand: BoxType::from_str("av01"),
minor_version: 0,
compatible_brands: vec![],
};
assert!(ftyp.is_mp4());
}
#[test]
fn test_mvhd_parse_v0() {
#[rustfmt::skip]
let data = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x27, 0x10, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, ];
let mvhd = MvhdBox::parse(&data).unwrap();
assert_eq!(mvhd.version, 0);
assert_eq!(mvhd.timescale, 1000);
assert_eq!(mvhd.duration, 10000);
assert!((mvhd.rate - 1.0).abs() < f64::EPSILON);
assert!((mvhd.volume - 1.0).abs() < f64::EPSILON);
assert_eq!(mvhd.next_track_id, 2);
assert!((mvhd.duration_seconds() - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_tkhd_parse_v0() {
#[rustfmt::skip]
let data = [
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
0x07, 0x80, 0x00, 0x00,
0x04, 0x38, 0x00, 0x00,
];
let tkhd = TkhdBox::parse(&data).unwrap();
assert_eq!(tkhd.track_id, 1);
assert_eq!(tkhd.duration, 10000);
assert!((tkhd.width - 1920.0).abs() < 1.0);
assert!((tkhd.height - 1080.0).abs() < 1.0);
}
#[test]
fn test_box_type_as_u32() {
assert_eq!(BoxType::FTYP.as_u32(), 0x66747970); }
}