From 7626f74b99aa50171c0fabfaa55ad82a7e1b1635 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:29:43 -0400 Subject: [PATCH] Media: use craft-media for bitstream, isobmff and matroska (#1034) * Media: use craft-media for bitstream, isobmff and matroska deckcraft-bitstream, deckcraft-isobmff and deckcraft-matroska re-export craft_media::{bitstream, isobmff, matroska} from craft-libs. The packages and their public paths stay, so the codec crates build unchanged. deckcraft-bitstream keeps the decoder memory budget as its own module (budget). deckcraft-media names the new CodecConfig::Apv entry. * Media: pin craft-media to storytold/craft-libs at f97c84d2 (#245) * Media: pin craft-media to storytold/craft-libs at f0ad6c1e (#246) --- Cargo.lock | 156 +-- Cargo.toml | 1 + crates/bitstream/Cargo.toml | 4 +- crates/bitstream/src/lib.rs | 489 +------ crates/isobmff/Cargo.toml | 3 +- crates/isobmff/src/bytes.rs | 361 ------ crates/isobmff/src/codec.rs | 1625 ------------------------ crates/isobmff/src/demux.rs | 1174 ----------------- crates/isobmff/src/error.rs | 38 - crates/isobmff/src/lib.rs | 28 +- crates/isobmff/src/mux.rs | 969 -------------- crates/isobmff/src/source.rs | 123 -- crates/matroska/Cargo.toml | 1 + crates/matroska/src/codec.rs | 225 ---- crates/matroska/src/demux.rs | 1176 ----------------- crates/matroska/src/ebml.rs | 476 ------- crates/matroska/src/error.rs | 53 - crates/matroska/src/ids.rs | 211 --- crates/matroska/src/lib.rs | 40 +- crates/matroska/src/meta.rs | 612 --------- crates/matroska/src/mux.rs | 539 -------- crates/matroska/src/source.rs | 173 --- crates/matroska/src/synthetic_tests.rs | 254 ---- crates/matroska/src/track.rs | 348 ----- crates/media/src/video.rs | 1 + 25 files changed, 35 insertions(+), 9045 deletions(-) delete mode 100644 crates/isobmff/src/bytes.rs delete mode 100644 crates/isobmff/src/codec.rs delete mode 100644 crates/isobmff/src/demux.rs delete mode 100644 crates/isobmff/src/error.rs delete mode 100644 crates/isobmff/src/mux.rs delete mode 100644 crates/isobmff/src/source.rs delete mode 100644 crates/matroska/src/codec.rs delete mode 100644 crates/matroska/src/demux.rs delete mode 100644 crates/matroska/src/ebml.rs delete mode 100644 crates/matroska/src/error.rs delete mode 100644 crates/matroska/src/ids.rs delete mode 100644 crates/matroska/src/meta.rs delete mode 100644 crates/matroska/src/mux.rs delete mode 100644 crates/matroska/src/source.rs delete mode 100644 crates/matroska/src/synthetic_tests.rs delete mode 100644 crates/matroska/src/track.rs diff --git a/Cargo.lock b/Cargo.lock index 9885eb33..38a55619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,30 +444,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec 0.8.0", -] - [[package]] name = "bit-set" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" dependencies = [ - "bit-vec 0.9.1", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bit-vec" version = "0.9.1" @@ -820,6 +805,14 @@ dependencies = [ "windows 0.54.0", ] +[[package]] +name = "craft-media" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4#f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "craft-pdf" version = "0.1.0" @@ -933,7 +926,7 @@ dependencies = [ name = "deckcraft-bitstream" version = "0.10.0" dependencies = [ - "proptest", + "craft-media", ] [[package]] @@ -1032,13 +1025,15 @@ dependencies = [ name = "deckcraft-isobmff" version = "0.10.0" dependencies = [ - "deckcraft-bitstream", - "thiserror 2.0.21", + "craft-media", ] [[package]] name = "deckcraft-matroska" version = "0.10.0" +dependencies = [ + "craft-media", +] [[package]] name = "deckcraft-mcp" @@ -1645,12 +1640,6 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.2.0" @@ -2262,7 +2251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", - "quick-error 2.0.1", + "quick-error", ] [[package]] @@ -2649,7 +2638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" dependencies = [ "arrayvec", - "bit-set 0.10.0", + "bit-set", "bitflags 2.13.2", "cfg-if", "cfg_aliases", @@ -3400,15 +3389,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "presser" version = "0.3.1" @@ -3439,37 +3419,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", - "bitflags 2.13.2", - "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - [[package]] name = "pxfm" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -3506,44 +3461,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core", -] - [[package]] name = "range-alloc" version = "0.1.5" @@ -3658,12 +3575,6 @@ dependencies = [ "bitflags 2.13.2", ] -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -3750,18 +3661,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - [[package]] name = "rustybuzz" version = "0.20.1" @@ -4399,7 +4298,7 @@ dependencies = [ "fax", "flate2", "half", - "quick-error 2.0.1", + "quick-error", "weezl", "zune-jpeg", ] @@ -4546,12 +4445,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicode-bidi" version = "0.3.18" @@ -4707,15 +4600,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -5044,8 +4928,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" dependencies = [ "arrayvec", - "bit-set 0.10.0", - "bit-vec 0.9.1", + "bit-set", + "bit-vec", "bitflags 2.13.2", "bytemuck", "cfg_aliases", @@ -5117,7 +5001,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set 0.10.0", + "bit-set", "bitflags 2.13.2", "block2 0.6.2", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 6f50bcd0..6f0f9e73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ harfrust = "0.13" vello_cpu = "0.2" vello_common = { version = "0.2", default-features = false } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] } +craft-media = { git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4", version = "=0.1.0", default-features = false } craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false, features = ["write"] } i_overlay = "9" quick-xml = "0.41" diff --git a/crates/bitstream/Cargo.toml b/crates/bitstream/Cargo.toml index 8465f1c6..ccdad35b 100644 --- a/crates/bitstream/Cargo.toml +++ b/crates/bitstream/Cargo.toml @@ -9,6 +9,4 @@ rust-version.workspace = true workspace = true [dependencies] - -[dev-dependencies] -proptest = { workspace = true } +craft-media = { workspace = true, features = ["bitstream"] } diff --git a/crates/bitstream/src/lib.rs b/crates/bitstream/src/lib.rs index a4f7d407..dcef8c8a 100644 --- a/crates/bitstream/src/lib.rs +++ b/crates/bitstream/src/lib.rs @@ -1,495 +1,14 @@ -//! Bit-level reading and writing shared by DeckCraft's codec crates (ported from FilmCraft). +//! Bit-level reading and writing for DeckCraft's codec crates: a re-export of +//! [`craft_media::bitstream`] from craft-libs, plus [`budget`]. //! //! - [`BitReader`]: MSB-first reader with Exp-Golomb (`ue(v)`, `se(v)`), as used by H.264/HEVC/AV1 headers. //! - [`BitWriter`]: the matching writer (for encoders and header rewriting). //! - [`unescape_rbsp`] / [`escape_rbsp`]: remove/insert H.264/HEVC emulation-prevention bytes. -//! - [`budget`]: the memory budget video decoders check their picture store against. //! - [`annexb_nals`] / [`length_prefixed_nals`]: split Annex-B byte streams and AVCC/HVCC samples into NAL units. +//! - [`budget`]: the memory budget video decoders check their picture store against. #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))] -use std::fmt; +pub use craft_media::bitstream::*; pub mod budget; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BitError { - /// Tried to read past the end of the buffer. - Eof, - /// An Exp-Golomb code longer than 32 leading zeros. - InvalidExpGolomb, - /// A NAL length prefix width outside 1..=4 bytes. - InvalidLengthSize, -} - -impl fmt::Display for BitError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BitError::Eof => f.write_str("unexpected end of bitstream"), - BitError::InvalidExpGolomb => f.write_str("invalid Exp-Golomb code"), - BitError::InvalidLengthSize => f.write_str("unsupported NAL length prefix size"), - } - } -} -impl std::error::Error for BitError {} - -pub type Result = std::result::Result; - -/// MSB-first bit reader over a byte slice. -#[derive(Clone)] -pub struct BitReader<'a> { - data: &'a [u8], - /// Position in bits. - pos: usize, -} - -impl<'a> BitReader<'a> { - pub fn new(data: &'a [u8]) -> Self { - Self { data, pos: 0 } - } - pub fn data(&self) -> &'a [u8] { - self.data - } - /// Bit position from the start. - pub fn position(&self) -> usize { - self.pos - } - pub fn bits_left(&self) -> usize { - (self.data.len() * 8).saturating_sub(self.pos) - } - pub fn is_byte_aligned(&self) -> bool { - self.pos.is_multiple_of(8) - } - pub fn byte_align(&mut self) { - self.pos = self.pos.div_ceil(8) * 8; - } - /// Byte offset of the current position (rounded down). - pub fn byte_pos(&self) -> usize { - self.pos / 8 - } - pub fn seek_bits(&mut self, pos: usize) { - self.pos = pos; - } - - /// Peek up to 32 bits without consuming (zero-padded past the end). - #[inline] - pub fn peek(&self, n: u32) -> u32 { - debug_assert!(n <= 32); - if n == 0 { - return 0; - } - let byte = self.pos / 8; - let shift = self.pos % 8; - let mut v: u64 = 0; - for i in 0..5 { - v = (v << 8) | *self.data.get(byte + i).unwrap_or(&0) as u64; - } - // v holds 40 bits starting at `byte` - ((v << (24 + shift)) >> (64 - n)) as u32 - } - - #[inline] - pub fn read_bits(&mut self, n: u32) -> Result { - if n as usize > self.bits_left() { - return Err(BitError::Eof); - } - let v = self.peek(n); - self.pos += n as usize; - Ok(v) - } - - pub fn read_bits_u64(&mut self, n: u32) -> Result { - if n <= 32 { - return self.read_bits(n).map(u64::from); - } - let hi = self.read_bits(n - 32)? as u64; - let lo = self.read_bits(32)? as u64; - Ok((hi << 32) | lo) - } - - #[inline] - pub fn read_bit(&mut self) -> Result { - self.read_bits(1).map(|b| b == 1) - } - pub fn read_flag(&mut self) -> Result { - self.read_bit() - } - - pub fn skip(&mut self, n: usize) -> Result<()> { - if n > self.bits_left() { - return Err(BitError::Eof); - } - self.pos += n; - Ok(()) - } - - /// Unsigned Exp-Golomb `ue(v)`. - pub fn read_ue(&mut self) -> Result { - let mut zeros = 0u32; - while !self.read_bit()? { - zeros += 1; - if zeros > 32 { - return Err(BitError::InvalidExpGolomb); - } - } - if zeros == 0 { - return Ok(0); - } - let v = self.read_bits_u64(zeros)?; - let r = (1u64 << zeros) - 1 + v; - u32::try_from(r).map_err(|_| BitError::InvalidExpGolomb) - } - - /// `ue(v) + add` (the `..._minus1` / `..._minus8` fields), failing instead of overflowing on a - /// hostile value near `u32::MAX`. - pub fn read_ue_plus(&mut self, add: u32) -> Result { - self.read_ue()?.checked_add(add).ok_or(BitError::InvalidExpGolomb) - } - - /// Signed Exp-Golomb `se(v)`. Codes whose value does not fit an `i32` (the code for `2^31`) are rejected. - pub fn read_se(&mut self) -> Result { - let k = self.read_ue()? as i64; - let v = if k & 1 == 1 { (k + 1) / 2 } else { -(k / 2) }; - i32::try_from(v).map_err(|_| BitError::InvalidExpGolomb) - } - - /// Truncated Exp-Golomb `te(v)` with range `max`. - pub fn read_te(&mut self, max: u32) -> Result { - if max > 1 { self.read_ue() } else { Ok(!self.read_bit()? as u32) } - } - - /// H.264/HEVC `more_rbsp_data()`: true if there is data before the rbsp trailing bits. - pub fn more_rbsp_data(&self) -> bool { - let total = self.data.len() * 8; - if self.pos >= total { - return false; - } - // find last set bit (the rbsp_stop_one_bit) - let mut last = None; - for (i, &b) in self.data.iter().enumerate().rev() { - if b != 0 { - last = Some(i * 8 + 7 - b.trailing_zeros() as usize); - break; - } - } - match last { - Some(stop) => self.pos < stop, - None => false, - } - } -} - -/// MSB-first bit writer. -#[derive(Default, Clone)] -pub struct BitWriter { - buf: Vec, - acc: u64, - nbits: u32, -} - -impl BitWriter { - pub fn new() -> Self { - Self::default() - } - pub fn write_bits(&mut self, value: u32, n: u32) { - debug_assert!(n <= 32); - if n == 0 { - return; - } - let v = (value as u64) & ((1u64 << n) - 1); - self.acc = (self.acc << n) | v; - self.nbits += n; - while self.nbits >= 8 { - self.nbits -= 8; - self.buf.push((self.acc >> self.nbits) as u8); - } - self.acc &= (1u64 << self.nbits) - 1; - } - pub fn write_bit(&mut self, b: bool) { - self.write_bits(b as u32, 1); - } - pub fn write_ue(&mut self, v: u32) { - self.write_ue_u64(v as u64); - } - fn write_ue_u64(&mut self, v: u64) { - let x = v + 1; - let len = 64 - x.leading_zeros(); - let zeros = len - 1; - for _ in 0..zeros { - self.write_bit(false); - } - if len > 32 { - self.write_bits((x >> 32) as u32, len - 32); - self.write_bits(x as u32, 32); - } else { - self.write_bits(x as u32, len); - } - } - /// Signed Exp-Golomb `se(v)`. `i32::MIN` needs a code above `u32::MAX`, which `read_se` rejects. - pub fn write_se(&mut self, v: i32) { - let v = v as i64; - let k = if v > 0 { (v as u64) * 2 - 1 } else { ((-v) as u64) * 2 }; - self.write_ue_u64(k); - } - pub fn bit_len(&self) -> usize { - self.buf.len() * 8 + self.nbits as usize - } - pub fn is_byte_aligned(&self) -> bool { - self.nbits == 0 - } - /// Pad with zero bits to a byte boundary. - pub fn align_zero(&mut self) { - if self.nbits > 0 { - self.write_bits(0, 8 - self.nbits); - } - } - /// `rbsp_trailing_bits()`: a one bit then zeros to alignment. - pub fn rbsp_trailing(&mut self) { - self.write_bit(true); - self.align_zero(); - } - pub fn write_bytes(&mut self, bytes: &[u8]) { - if self.nbits == 0 { - self.buf.extend_from_slice(bytes); - } else { - for &b in bytes { - self.write_bits(b as u32, 8); - } - } - } - pub fn finish(mut self) -> Vec { - self.align_zero(); - self.buf - } - pub fn bytes(&self) -> &[u8] { - &self.buf - } -} - -/// Remove emulation-prevention bytes (`00 00 03` → `00 00`) from a NAL payload. -pub fn unescape_rbsp(data: &[u8]) -> Vec { - let mut out = Vec::with_capacity(data.len()); - let mut zeros = 0; - for &b in data { - if zeros >= 2 && b == 3 { - zeros = 0; - continue; - } - out.push(b); - zeros = if b == 0 { zeros + 1 } else { 0 }; - } - out -} - -/// Insert emulation-prevention bytes so no `00 00 0x` (x ≤ 3) sequence appears. -pub fn escape_rbsp(data: &[u8]) -> Vec { - let mut out = Vec::with_capacity(data.len() + data.len() / 64); - let mut zeros = 0; - for &b in data { - if zeros >= 2 && b <= 3 { - out.push(3); - zeros = 0; - } - out.push(b); - zeros = if b == 0 { zeros + 1 } else { 0 }; - } - out -} - -/// Split an Annex-B byte stream (start codes `00 00 01` / `00 00 00 01`) into NAL units (escaped payloads). -pub fn annexb_nals(data: &[u8]) -> Vec<&[u8]> { - let mut starts = Vec::new(); - let mut i = 0; - while i + 3 <= data.len() { - if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 { - starts.push(i + 3); - i += 3; - } else { - i += 1; - } - } - let mut out = Vec::with_capacity(starts.len()); - for (k, &s) in starts.iter().enumerate() { - let mut e = if k + 1 < starts.len() { starts[k + 1] - 3 } else { data.len() }; - // trim trailing zero bytes (part of a 4-byte start code or trailing_zero_8bits) - while e > s && data[e - 1] == 0 { - e -= 1; - } - if e > s { - out.push(&data[s..e]); - } - } - out -} - -/// Split a length-prefixed sample (AVCC/HVCC, `length_size` 1..=4 bytes, as the 2-bit `lengthSizeMinusOne` allows) into NAL units. -pub fn length_prefixed_nals(data: &[u8], length_size: usize) -> Result> { - if !(1..=4).contains(&length_size) { - return Err(BitError::InvalidLengthSize); - } - let mut out = Vec::new(); - let mut i = 0; - while i < data.len() { - if length_size > data.len() - i { - return Err(BitError::Eof); - } - let mut len = 0usize; - for k in 0..length_size { - len = (len << 8) | data[i + k] as usize; - } - i += length_size; - if len > data.len() - i { - return Err(BitError::Eof); - } - out.push(&data[i..i + len]); - i += len; - } - Ok(out) -} - -/// CRC-32 (IEEE, reflected) — used by containers (e.g. Matroska CRC-32 elements, PNG). -pub fn crc32(data: &[u8]) -> u32 { - let mut crc = 0xFFFF_FFFFu32; - for &b in data { - crc ^= b as u32; - for _ in 0..8 { - crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 }; - } - } - !crc -} - -#[cfg(test)] -mod tests { - use super::*; - use proptest::prelude::*; - - #[test] - fn read_ue_plus_rejects_overflow() { - let mut w = BitWriter::new(); - w.write_ue(7); - w.write_ue(u32::MAX); - w.rbsp_trailing(); - let b = w.finish(); - let mut r = BitReader::new(&b); - assert_eq!(r.read_ue_plus(1).unwrap(), 8); - assert!(r.read_ue_plus(1).is_err()); - } - - #[test] - fn signed_exp_golomb_extremes() { - let mut w = BitWriter::new(); - for v in [i32::MAX, i32::MIN + 1, i32::MIN] { - w.write_se(v); - } - w.rbsp_trailing(); - let b = w.finish(); - let mut r = BitReader::new(&b); - assert_eq!(r.read_se().unwrap(), i32::MAX); - assert_eq!(r.read_se().unwrap(), i32::MIN + 1); - assert_eq!(r.read_se(), Err(BitError::InvalidExpGolomb)); - // ue code 0xFFFF_FFFF maps to +2^31, which must not wrap to a negative i32. - let mut w = BitWriter::new(); - w.write_ue(u32::MAX); - w.rbsp_trailing(); - let b = w.finish(); - assert_eq!(BitReader::new(&b).read_se(), Err(BitError::InvalidExpGolomb)); - } - - #[test] - fn length_prefixed_rejects_bad_width_and_oversized_length() { - assert_eq!(length_prefixed_nals(&[1, 2, 3, 4, 5, 6], 5), Err(BitError::InvalidLengthSize)); - assert_eq!(length_prefixed_nals(&[1, 2, 3], 0), Err(BitError::InvalidLengthSize)); - assert_eq!(length_prefixed_nals(&[0, 0, 1, 7], 3).unwrap(), vec![&[7u8][..]]); - assert_eq!(length_prefixed_nals(&[0xFF, 0xFF, 0xFF, 0xFF, 1], 4), Err(BitError::Eof)); - assert_eq!(length_prefixed_nals(&[0, 0, 0, 1, 9], 4).unwrap(), vec![&[9u8][..]]); - } - - #[test] - fn reads_bits() { - let d = [0b1010_1100, 0b0101_0011]; - let mut r = BitReader::new(&d); - assert_eq!(r.read_bits(1).unwrap(), 1); - assert_eq!(r.read_bits(3).unwrap(), 0b010); - assert_eq!(r.read_bits(8).unwrap(), 0b1100_0101); - assert_eq!(r.bits_left(), 4); - assert_eq!(r.read_bits(5), Err(BitError::Eof)); - } - - #[test] - fn exp_golomb_known() { - // 1 | 010 | 011 | 00100 | 00101 -> 0,1,2,3,4 - let mut w = BitWriter::new(); - for v in 0..5 { - w.write_ue(v); - } - let b = w.finish(); - let mut r = BitReader::new(&b); - for v in 0..5 { - assert_eq!(r.read_ue().unwrap(), v); - } - let mut w = BitWriter::new(); - for v in [0, 1, -1, 2, -2, 1000, -1000] { - w.write_se(v); - } - let b = w.finish(); - let mut r = BitReader::new(&b); - for v in [0, 1, -1, 2, -2, 1000, -1000] { - assert_eq!(r.read_se().unwrap(), v); - } - } - - #[test] - fn emulation_prevention() { - let raw = [0, 0, 1, 0, 0, 0, 5, 0, 0, 3]; - let esc = escape_rbsp(&raw); - assert_eq!(esc, vec![0, 0, 3, 1, 0, 0, 3, 0, 5, 0, 0, 3, 3]); - assert_eq!(unescape_rbsp(&esc), raw); - } - - #[test] - fn annexb_split() { - let s = [0, 0, 0, 1, 0x67, 1, 2, 0, 0, 1, 0x68, 3, 0, 0, 0, 1, 0x65, 9, 9]; - let n = annexb_nals(&s); - assert_eq!(n, vec![&[0x67, 1, 2][..], &[0x68, 3][..], &[0x65, 9, 9][..]]); - let l = [0, 0, 0, 2, 0x67, 1, 0, 0, 0, 1, 0x68]; - assert_eq!(length_prefixed_nals(&l, 4).unwrap(), vec![&[0x67, 1][..], &[0x68][..]]); - } - - #[test] - fn more_rbsp() { - let d = [0b1100_0000]; - let mut r = BitReader::new(&d); - assert!(r.more_rbsp_data()); - r.read_bit().unwrap(); - assert!(!r.more_rbsp_data()); - } - - #[test] - fn crc() { - assert_eq!(crc32(b"123456789"), 0xCBF4_3926); - } - - proptest! { - #[test] - fn roundtrip(vals in proptest::collection::vec((0u32..u32::MAX, 1u32..=32), 0..200), ues in proptest::collection::vec(0u32..u32::MAX - 1, 0..50)) { - let mut w = BitWriter::new(); - for &(v, n) in &vals { w.write_bits(v, n); } - for &u in &ues { w.write_ue(u); } - let b = w.finish(); - let mut r = BitReader::new(&b); - for &(v, n) in &vals { - let mask = if n == 32 { u32::MAX } else { (1 << n) - 1 }; - prop_assert_eq!(r.read_bits(n).unwrap(), v & mask); - } - for &u in &ues { prop_assert_eq!(r.read_ue().unwrap(), u); } - } - - #[test] - fn escape_roundtrip(data in proptest::collection::vec(0u8..4, 0..300)) { - let e = escape_rbsp(&data); - prop_assert_eq!(unescape_rbsp(&e), data); - for w in e.windows(3) { prop_assert!(!(w[0] == 0 && w[1] == 0 && w[2] < 3)); } - } - } -} diff --git a/crates/isobmff/Cargo.toml b/crates/isobmff/Cargo.toml index 04c339c7..7bc074b2 100644 --- a/crates/isobmff/Cargo.toml +++ b/crates/isobmff/Cargo.toml @@ -10,5 +10,4 @@ rust-version.workspace = true workspace = true [dependencies] -deckcraft-bitstream = { workspace = true } -thiserror = { workspace = true } +craft-media = { workspace = true, features = ["isobmff"] } diff --git a/crates/isobmff/src/bytes.rs b/crates/isobmff/src/bytes.rs deleted file mode 100644 index 97f92690..00000000 --- a/crates/isobmff/src/bytes.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! Low-level helpers: four-character codes, a bounds-checked big-endian cursor, box iteration -//! over in-memory payloads, and a box builder for the muxers. - -use crate::error::{Error, Result}; -use std::fmt; - -/// A four-character code (box type, sample entry format, brand, handler type…). -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] -pub struct FourCc(pub [u8; 4]); - -impl FourCc { - pub const fn new(b: &[u8; 4]) -> Self { - FourCc(*b) - } - pub fn as_u32(self) -> u32 { - u32::from_be_bytes(self.0) - } - /// Lossy string form; `©` (0xA9) is mapped to U+00A9, other non-printables to `?`. - pub fn to_string_lossy(self) -> String { - self.0 - .iter() - .map(|&b| match b { - 0xA9 => '©', - 0x20..=0x7E => b as char, - _ => '?', - }) - .collect() - } -} - -impl fmt::Display for FourCc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.to_string_lossy()) - } -} -impl fmt::Debug for FourCc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "FourCc({:?})", self.to_string_lossy()) - } -} -impl From<&[u8; 4]> for FourCc { - fn from(b: &[u8; 4]) -> Self { - FourCc(*b) - } -} -impl PartialEq<[u8; 4]> for FourCc { - fn eq(&self, other: &[u8; 4]) -> bool { - &self.0 == other - } -} -impl PartialEq<&[u8; 4]> for FourCc { - fn eq(&self, other: &&[u8; 4]) -> bool { - &self.0 == *other - } -} -impl PartialEq<&&[u8; 4]> for FourCc { - fn eq(&self, other: &&&[u8; 4]) -> bool { - &self.0 == **other - } -} - -/// Bounds-checked big-endian cursor over a byte slice. -#[derive(Clone)] -pub(crate) struct Cur<'a> { - pub data: &'a [u8], - pub pos: usize, - what: &'static str, -} - -impl<'a> Cur<'a> { - pub fn new(data: &'a [u8], what: &'static str) -> Self { - Cur { data, pos: 0, what } - } - pub fn remaining(&self) -> usize { - self.data.len().saturating_sub(self.pos) - } - pub fn rest(&self) -> &'a [u8] { - self.data.get(self.pos..).unwrap_or(&[]) - } - pub fn bytes(&mut self, n: usize) -> Result<&'a [u8]> { - let end = self.pos.checked_add(n).ok_or(Error::Truncated(self.what))?; - let s = self.data.get(self.pos..end).ok_or(Error::Truncated(self.what))?; - self.pos = end; - Ok(s) - } - pub fn skip(&mut self, n: usize) -> Result<()> { - self.bytes(n).map(|_| ()) - } - pub fn array(&mut self) -> Result<[u8; N]> { - let s = self.bytes(N)?; - let mut a = [0u8; N]; - a.copy_from_slice(s); - Ok(a) - } - pub fn u8(&mut self) -> Result { - Ok(self.array::<1>()?[0]) - } - pub fn u16(&mut self) -> Result { - Ok(u16::from_be_bytes(self.array()?)) - } - pub fn i16(&mut self) -> Result { - Ok(i16::from_be_bytes(self.array()?)) - } - pub fn u24(&mut self) -> Result { - let a = self.array::<3>()?; - Ok(((a[0] as u32) << 16) | ((a[1] as u32) << 8) | a[2] as u32) - } - pub fn u32(&mut self) -> Result { - Ok(u32::from_be_bytes(self.array()?)) - } - pub fn i32(&mut self) -> Result { - Ok(i32::from_be_bytes(self.array()?)) - } - pub fn u64(&mut self) -> Result { - Ok(u64::from_be_bytes(self.array()?)) - } - pub fn i64(&mut self) -> Result { - Ok(i64::from_be_bytes(self.array()?)) - } - pub fn f64(&mut self) -> Result { - Ok(f64::from_be_bytes(self.array()?)) - } - pub fn fourcc(&mut self) -> Result { - Ok(FourCc(self.array()?)) - } - /// Full-box header: (version, flags). - pub fn full_header(&mut self) -> Result<(u8, u32)> { - let v = self.u32()?; - Ok(((v >> 24) as u8, v & 0x00FF_FFFF)) - } - /// Read a count and verify that `count * entry_size` bytes remain (allocation guard). - pub fn count(&mut self, entry_size: usize) -> Result { - let n = self.u32()? as usize; - self.check_count(n, entry_size)?; - Ok(n) - } - pub fn check_count(&self, n: usize, entry_size: usize) -> Result<()> { - match n.checked_mul(entry_size) { - Some(b) if b <= self.remaining() => Ok(()), - _ => Err(Error::Truncated(self.what)), - } - } -} - -/// Header of one box as found in a byte stream. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct BoxHeader { - pub kind: FourCc, - /// Header length including largesize and uuid extended type. - pub header_len: u64, - /// Total box size including header; `None` means "extends to end of the enclosing container". - pub size: Option, - pub uuid: Option<[u8; 16]>, -} - -/// Parse a box header from `buf` (needs up to 32 bytes; fewer if available). -pub(crate) fn parse_box_header(buf: &[u8]) -> Result { - let mut c = Cur::new(buf, "box header"); - let size32 = c.u32()?; - let kind = c.fourcc()?; - let mut header_len = 8u64; - let size = match size32 { - 0 => None, - 1 => { - header_len += 8; - let s = c.u64()?; - Some(s) - } - s => Some(s as u64), - }; - let uuid = if kind == b"uuid" { - header_len += 16; - Some(c.array::<16>()?) - } else { - None - }; - if let Some(s) = size - && s < header_len - { - return Err(Error::Invalid(format!("box '{kind}' size {s} smaller than its header"))); - } - Ok(BoxHeader { kind, header_len, size, uuid }) -} - -/// One child box inside an in-memory payload. -#[derive(Clone, Copy, Debug)] -pub(crate) struct RawBox<'a> { - pub kind: FourCc, - pub payload: &'a [u8], - #[allow(dead_code)] - pub uuid: Option<[u8; 16]>, -} - -/// Iterator over the child boxes of a payload. Yields an error for a malformed box and then stops. -pub(crate) struct Boxes<'a> { - data: &'a [u8], - pos: usize, - done: bool, -} - -pub(crate) fn boxes(data: &[u8]) -> Boxes<'_> { - Boxes { data, pos: 0, done: false } -} - -impl<'a> Iterator for Boxes<'a> { - type Item = Result>; - fn next(&mut self) -> Option { - if self.done { - return None; - } - let rest = &self.data[self.pos..]; - if rest.is_empty() { - self.done = true; - return None; - } - // QuickTime allows a 32-bit zero terminator at the end of some containers. - if rest.len() < 8 { - self.done = true; - if rest.iter().all(|&b| b == 0) { - return None; - } - return Some(Err(Error::Truncated("box header"))); - } - let h = match parse_box_header(rest) { - Ok(h) => h, - Err(e) => { - self.done = true; - return Some(Err(e)); - } - }; - let size = h.size.unwrap_or(rest.len() as u64); - if size > rest.len() as u64 { - self.done = true; - return Some(Err(Error::Invalid(format!("box '{}' size {} exceeds its container ({} bytes left)", h.kind, size, rest.len())))); - } - let size = size as usize; - let payload = &rest[h.header_len as usize..size]; - self.pos += size; - Some(Ok(RawBox { kind: h.kind, payload, uuid: h.uuid })) - } -} - -/// Find the first child of the given type (malformed trailing children are ignored). -pub(crate) fn find<'a>(data: &'a [u8], kind: &[u8; 4]) -> Option<&'a [u8]> { - boxes(data).map_while(|b| b.ok()).find(|b| b.kind == kind).map(|b| b.payload) -} - -/// Growable buffer for building boxes. -#[derive(Default, Clone, Debug)] -pub(crate) struct BoxBuf { - pub buf: Vec, -} - -impl BoxBuf { - pub fn new() -> Self { - Self::default() - } - /// Begin a box; returns a marker to pass to [`end`](Self::end). - pub fn start(&mut self, kind: &[u8; 4]) -> usize { - let at = self.buf.len(); - self.buf.extend_from_slice(&[0, 0, 0, 0]); - self.buf.extend_from_slice(kind); - at - } - pub fn start_full(&mut self, kind: &[u8; 4], version: u8, flags: u32) -> usize { - let at = self.start(kind); - self.u32(((version as u32) << 24) | (flags & 0xFF_FFFF)); - at - } - pub fn end(&mut self, at: usize) { - let size = (self.buf.len() - at) as u32; - self.buf[at..at + 4].copy_from_slice(&size.to_be_bytes()); - } - /// Append a complete box with the given payload. - pub fn leaf(&mut self, kind: &[u8; 4], payload: &[u8]) { - let at = self.start(kind); - self.bytes(payload); - self.end(at); - } - pub fn u8(&mut self, v: u8) { - self.buf.push(v); - } - pub fn u16(&mut self, v: u16) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn i16(&mut self, v: i16) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn u32(&mut self, v: u32) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn i32(&mut self, v: i32) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn u64(&mut self, v: u64) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn i64(&mut self, v: i64) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn f64(&mut self, v: f64) { - self.buf.extend_from_slice(&v.to_be_bytes()); - } - pub fn bytes(&mut self, b: &[u8]) { - self.buf.extend_from_slice(b); - } - pub fn zeros(&mut self, n: usize) { - self.buf.resize(self.buf.len() + n, 0); - } - pub fn len(&self) -> usize { - self.buf.len() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn header_forms() { - let h = parse_box_header(&[0, 0, 0, 16, b'f', b'r', b'e', b'e']).unwrap(); - assert_eq!((h.kind, h.header_len, h.size), (FourCc(*b"free"), 8, Some(16))); - let h = parse_box_header(&[0, 0, 0, 0, b'm', b'd', b'a', b't']).unwrap(); - assert_eq!(h.size, None); - let mut v = vec![0, 0, 0, 1, b'm', b'd', b'a', b't']; - v.extend_from_slice(&100u64.to_be_bytes()); - let h = parse_box_header(&v).unwrap(); - assert_eq!((h.header_len, h.size), (16, Some(100))); - let mut v = vec![0, 0, 0, 30, b'u', b'u', b'i', b'd']; - v.extend_from_slice(&[7u8; 16]); - let h = parse_box_header(&v).unwrap(); - assert_eq!((h.header_len, h.uuid), (24, Some([7u8; 16]))); - assert!(parse_box_header(&[0, 0, 0, 4, b'f', b'r', b'e', b'e']).is_err()); - assert!(parse_box_header(&[0, 0, 0]).is_err()); - } - - #[test] - fn iterate_children() { - let mut b = BoxBuf::new(); - b.leaf(b"aaaa", &[1, 2]); - let m = b.start(b"bbbb"); - b.leaf(b"cccc", &[]); - b.end(m); - b.u32(0); // QT terminator - let kids: Vec<_> = boxes(&b.buf).collect::>>().unwrap(); - assert_eq!(kids.len(), 2); - assert_eq!(kids[0].payload, &[1, 2]); - assert_eq!(find(&b.buf, b"bbbb").map(|p| p.len()), Some(8)); - // Oversized child → error, no panic. - let bad = [0u8, 0, 0, 99, b'x', b'x', b'x', b'x', 0]; - assert!(boxes(&bad).next().unwrap().is_err()); - } - - #[test] - fn cursor_bounds() { - let mut c = Cur::new(&[0, 0, 0, 5, 1], "t"); - assert!(c.count(1).is_err()); - let mut c = Cur::new(&[0xFF, 0xFF, 0xFF, 0xFF], "t"); - assert!(c.count(usize::MAX).is_err()); - } -} diff --git a/crates/isobmff/src/codec.rs b/crates/isobmff/src/codec.rs deleted file mode 100644 index 71580c60..00000000 --- a/crates/isobmff/src/codec.rs +++ /dev/null @@ -1,1625 +0,0 @@ -//! Sample entries (`stsd` children) and codec configuration records: parsing and serialization. - -use crate::bytes::{BoxBuf, Cur, FourCc, boxes}; -use crate::error::{Error, Result, invalid}; -use deckcraft_bitstream::BitReader; - -/// One sample description (an entry of `stsd`). -#[derive(Clone, Debug, PartialEq)] -pub struct SampleEntry { - /// Sample entry format (`avc1`, `mp4a`, `apch`, …). - pub format: FourCc, - pub data_reference_index: u16, - pub codec: CodecConfig, - /// Present for visual sample entries. - pub video: Option, - /// Present for audio sample entries. - pub audio: Option, - /// `btrt` box. - pub bitrate: Option, -} - -/// Fields common to all visual sample entries plus the optional descriptive boxes. -#[derive(Clone, Debug, PartialEq, Default)] -pub struct VideoParams { - pub width: u16, - pub height: u16, - /// Horizontal/vertical resolution in pixels per inch (16.16 fixed point; 72 dpi = 0x0048_0000). - pub horiz_resolution: u32, - pub vert_resolution: u32, - pub frame_count: u16, - pub compressor_name: String, - pub depth: u16, - /// QuickTime `vendor` / version fields (zero in ISO files). - pub vendor: FourCc, - pub temporal_quality: u32, - pub spatial_quality: u32, - pub color: Option, - /// `pasp`: (hSpacing, vSpacing). - pub pixel_aspect: Option<(u32, u32)>, - pub clean_aperture: Option, - pub field_info: Option, - /// QuickTime `gama` (16.16 fixed point). - pub gamma: Option, - /// `mdcv`: mastering display colour volume (SMPTE ST 2086). - pub mastering_display: Option, - /// `clli`: content light level (MaxCLL, MaxFALL in cd/m²; 0 = unknown). - pub content_light: Option<(u16, u16)>, -} - -/// SMPTE ST 2086 mastering display metadata, in the units of the `mdcv` box and the H.264/HEVC -/// SEI: chromaticities in 0.00002 steps (order G, B, R as the SEI), luminance in 0.0001 cd/m². -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct MasteringDisplay { - pub primaries: [(u16, u16); 3], - pub white_point: (u16, u16), - pub max_luminance: u32, - pub min_luminance: u32, -} - -impl MasteringDisplay { - /// BT.2020 primaries, D65, with the given peak and black in cd/m². - pub fn bt2020(max_nits: f64, min_nits: f64) -> Self { - let q = |v: f64| (v / 0.00002).round() as u16; - MasteringDisplay { - primaries: [(q(0.170), q(0.797)), (q(0.131), q(0.046)), (q(0.708), q(0.292))], - white_point: (q(0.3127), q(0.3290)), - max_luminance: (max_nits * 10_000.0).round() as u32, - min_luminance: (min_nits * 10_000.0).round() as u32, - } - } - pub fn max_nits(&self) -> f64 { - self.max_luminance as f64 / 10_000.0 - } - /// The 24-byte payload shared by `mdcv` and the SEI message. - pub fn to_bytes(&self) -> Vec { - let mut v = Vec::with_capacity(24); - for (x, y) in self.primaries { - v.extend_from_slice(&x.to_be_bytes()); - v.extend_from_slice(&y.to_be_bytes()); - } - v.extend_from_slice(&self.white_point.0.to_be_bytes()); - v.extend_from_slice(&self.white_point.1.to_be_bytes()); - v.extend_from_slice(&self.max_luminance.to_be_bytes()); - v.extend_from_slice(&self.min_luminance.to_be_bytes()); - v - } - pub fn parse(p: &[u8]) -> Option { - if p.len() < 24 { - return None; - } - let u16_at = |i: usize| u16::from_be_bytes([p[i], p[i + 1]]); - let u32_at = |i: usize| u32::from_be_bytes([p[i], p[i + 1], p[i + 2], p[i + 3]]); - Some(MasteringDisplay { - primaries: [(u16_at(0), u16_at(2)), (u16_at(4), u16_at(6)), (u16_at(8), u16_at(10))], - white_point: (u16_at(12), u16_at(14)), - max_luminance: u32_at(16), - min_luminance: u32_at(20), - }) - } -} - -/// `colr` box contents. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ColorInfo { - /// ISO `nclx` (with full-range flag). - Nclx { primaries: u16, transfer: u16, matrix: u16, full_range: bool }, - /// QuickTime `nclc`. - Nclc { primaries: u16, transfer: u16, matrix: u16 }, - /// Embedded ICC profile (`prof` restricted/unrestricted `rICC`). - Icc { kind: FourCc, profile: Vec }, -} - -/// `clap` clean aperture (all rationals num/den; offsets are signed). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct CleanAperture { - pub width: (u32, u32), - pub height: (u32, u32), - pub horiz_offset: (i32, u32), - pub vert_offset: (i32, u32), -} - -/// QuickTime `fiel`: field count (1 progressive, 2 interlaced) and ordering detail -/// (0, 1 = top first, 6 = bottom first, 9 / 14 per the QuickTime spec). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct FieldInfo { - pub fields: u8, - pub detail: u8, -} - -/// `btrt` box. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct BitRate { - pub buffer_size: u32, - pub max_bitrate: u32, - pub avg_bitrate: u32, -} - -/// Fields of audio sample entries (ISO v0 plus QuickTime sound description v1/v2 extensions). -#[derive(Clone, Debug, PartialEq, Default)] -pub struct AudioParams { - /// QuickTime sound description version (0, 1, 2). ISO files use 0. - pub qt_version: u16, - pub channels: u32, - /// Bits per sample from the entry (16 for most compressed formats). - pub sample_size: u16, - pub sample_rate: f64, - pub compression_id: i16, - /// v1 fields. - pub samples_per_packet: u32, - pub bytes_per_packet: u32, - pub bytes_per_frame: u32, - pub bytes_per_sample: u32, - /// v2 fields. - pub const_bits_per_channel: u32, - pub lpcm_flags: u32, - pub const_bytes_per_packet: u32, - pub const_frames_per_packet: u32, - /// `enda` little-endian flag inside a QuickTime `wave` box. - pub little_endian: Option, -} - -/// Codec configuration decoded from the sample entry. -#[derive(Clone, Debug, PartialEq)] -pub enum CodecConfig { - Avc(AvcConfig), - Hevc(HevcConfig), - Av1(Av1Config), - Vp9(VpcConfig), - /// Apple ProRes; fourcc is `apch`/`apcn`/`apcs`/`apco`/`ap4h`/`ap4x`. - ProRes { - fourcc: FourCc, - }, - /// Motion JPEG (`jpeg`, `mjpa`, `mjpb`). - Jpeg { - fourcc: FourCc, - }, - /// Avid DNxHD/DNxHR (`AVdn`, `AVdh`). - Dnx { - fourcc: FourCc, - }, - Aac(AacConfig), - /// MPEG-1/2 audio layer III (esds object type 0x69/0x6B, or QuickTime `.mp3`). - Mp3, - Pcm(PcmConfig), - /// ALAC magic cookie (ALACSpecificConfig, 24 bytes, optionally followed by a channel layout). - Alac { - cookie: Vec, - }, - Opus(OpusConfig), - Flac(FlacConfig), - /// `dac3` payload. - Ac3 { - dac3: Vec, - }, - /// `dec3` payload. - Eac3 { - dec3: Vec, - }, - Timecode(TimecodeConfig), - /// Anything else; `raw` is the full sample entry payload (after the 8-byte box header). - Unknown { - fourcc: FourCc, - raw: Vec, - }, -} - -impl CodecConfig { - /// A short human-readable codec name. - pub fn name(&self) -> &'static str { - match self { - CodecConfig::Avc(_) => "h264", - CodecConfig::Hevc(_) => "hevc", - CodecConfig::Av1(_) => "av1", - CodecConfig::Vp9(_) => "vp9", - CodecConfig::ProRes { .. } => "prores", - CodecConfig::Jpeg { .. } => "mjpeg", - CodecConfig::Dnx { .. } => "dnxhd", - CodecConfig::Aac(_) => "aac", - CodecConfig::Mp3 => "mp3", - CodecConfig::Pcm(_) => "pcm", - CodecConfig::Alac { .. } => "alac", - CodecConfig::Opus(_) => "opus", - CodecConfig::Flac(_) => "flac", - CodecConfig::Ac3 { .. } => "ac3", - CodecConfig::Eac3 { .. } => "eac3", - CodecConfig::Timecode(_) => "timecode", - CodecConfig::Unknown { .. } => "unknown", - } - } -} - -/// `avcC` — AVCDecoderConfigurationRecord (ISO/IEC 14496-15 §5.3.3). -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct AvcConfig { - pub profile: u8, - pub compatibility: u8, - pub level: u8, - /// NAL length field size in bytes (1, 2 or 4). - pub length_size: u8, - pub sps: Vec>, - pub pps: Vec>, - /// Bytes after the PPS list (high-profile extension: chroma format, bit depths, SPS-ext). - pub ext: Vec, -} - -impl AvcConfig { - /// Build from parameter sets (NAL units without start codes); profile/level taken from the first SPS. - pub fn new(sps: Vec>, pps: Vec>, length_size: u8) -> Self { - let (profile, compatibility, level) = sps.first().filter(|s| s.len() >= 4).map(|s| (s[1], s[2], s[3])).unwrap_or((0, 0, 0)); - AvcConfig { profile, compatibility, level, length_size, sps, pps, ext: Vec::new() } - } - - pub fn parse(data: &[u8]) -> Result { - let mut c = Cur::new(data, "avcC"); - let _version = c.u8()?; - let profile = c.u8()?; - let compatibility = c.u8()?; - let level = c.u8()?; - let length_size = (c.u8()? & 3) + 1; - let nsps = (c.u8()? & 0x1F) as usize; - let mut sps = Vec::with_capacity(nsps); - for _ in 0..nsps { - let n = c.u16()? as usize; - sps.push(c.bytes(n)?.to_vec()); - } - let npps = c.u8()? as usize; - let mut pps = Vec::with_capacity(npps); - for _ in 0..npps { - let n = c.u16()? as usize; - pps.push(c.bytes(n)?.to_vec()); - } - Ok(AvcConfig { profile, compatibility, level, length_size, sps, pps, ext: c.rest().to_vec() }) - } - - pub fn to_bytes(&self) -> Vec { - let mut b = BoxBuf::new(); - b.u8(1); - b.u8(self.profile); - b.u8(self.compatibility); - b.u8(self.level); - b.u8(0xFC | (self.length_size.clamp(1, 4) - 1)); - b.u8(0xE0 | (self.sps.len().min(31) as u8)); - for s in self.sps.iter().take(31) { - b.u16(s.len() as u16); - b.bytes(s); - } - b.u8(self.pps.len().min(255) as u8); - for p in self.pps.iter().take(255) { - b.u16(p.len() as u16); - b.bytes(p); - } - b.bytes(&self.ext); - b.buf - } -} - -/// One NAL array of an `hvcC` record. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct HevcNalArray { - pub completeness: bool, - pub nal_type: u8, - pub nalus: Vec>, -} - -/// `hvcC` — HEVCDecoderConfigurationRecord (ISO/IEC 14496-15 §8.3.3). -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct HevcConfig { - pub general_profile_space: u8, - pub general_tier_flag: bool, - pub general_profile_idc: u8, - pub general_profile_compatibility_flags: u32, - /// 48 bits of general constraint indicator flags. - pub general_constraint_indicator_flags: u64, - pub general_level_idc: u8, - pub min_spatial_segmentation_idc: u16, - pub parallelism_type: u8, - pub chroma_format_idc: u8, - pub bit_depth_luma: u8, - pub bit_depth_chroma: u8, - pub avg_frame_rate: u16, - pub constant_frame_rate: u8, - pub num_temporal_layers: u8, - pub temporal_id_nested: bool, - pub length_size: u8, - pub arrays: Vec, -} - -impl HevcConfig { - pub fn parse(data: &[u8]) -> Result { - let mut c = Cur::new(data, "hvcC"); - let _version = c.u8()?; - let b = c.u8()?; - let compat = c.u32()?; - let hi = c.u16()? as u64; - let lo = c.u32()? as u64; - let level = c.u8()?; - let min_seg = c.u16()? & 0x0FFF; - let par = c.u8()? & 3; - let chroma = c.u8()? & 3; - let bdl = (c.u8()? & 7) + 8; - let bdc = (c.u8()? & 7) + 8; - let afr = c.u16()?; - let x = c.u8()?; - let narrays = c.u8()? as usize; - let mut arrays = Vec::new(); - for _ in 0..narrays { - let t = c.u8()?; - let n = c.u16()? as usize; - c.check_count(n, 2)?; - let mut nalus = Vec::with_capacity(n); - for _ in 0..n { - let len = c.u16()? as usize; - nalus.push(c.bytes(len)?.to_vec()); - } - arrays.push(HevcNalArray { completeness: t & 0x80 != 0, nal_type: t & 0x3F, nalus }); - } - Ok(HevcConfig { - general_profile_space: b >> 6, - general_tier_flag: b & 0x20 != 0, - general_profile_idc: b & 0x1F, - general_profile_compatibility_flags: compat, - general_constraint_indicator_flags: (hi << 32) | lo, - general_level_idc: level, - min_spatial_segmentation_idc: min_seg, - parallelism_type: par, - chroma_format_idc: chroma, - bit_depth_luma: bdl, - bit_depth_chroma: bdc, - avg_frame_rate: afr, - constant_frame_rate: x >> 6, - num_temporal_layers: (x >> 3) & 7, - temporal_id_nested: x & 4 != 0, - length_size: (x & 3) + 1, - arrays, - }) - } - - pub fn to_bytes(&self) -> Vec { - let mut b = BoxBuf::new(); - b.u8(1); - b.u8((self.general_profile_space << 6) | ((self.general_tier_flag as u8) << 5) | (self.general_profile_idc & 0x1F)); - b.u32(self.general_profile_compatibility_flags); - b.u16((self.general_constraint_indicator_flags >> 32) as u16); - b.u32(self.general_constraint_indicator_flags as u32); - b.u8(self.general_level_idc); - b.u16(0xF000 | (self.min_spatial_segmentation_idc & 0x0FFF)); - b.u8(0xFC | (self.parallelism_type & 3)); - b.u8(0xFC | (self.chroma_format_idc & 3)); - b.u8(0xF8 | (self.bit_depth_luma.saturating_sub(8) & 7)); - b.u8(0xF8 | (self.bit_depth_chroma.saturating_sub(8) & 7)); - b.u16(self.avg_frame_rate); - b.u8((self.constant_frame_rate << 6) - | ((self.num_temporal_layers & 7) << 3) - | ((self.temporal_id_nested as u8) << 2) - | (self.length_size.clamp(1, 4) - 1)); - b.u8(self.arrays.len() as u8); - for a in &self.arrays { - b.u8(((a.completeness as u8) << 7) | (a.nal_type & 0x3F)); - b.u16(a.nalus.len() as u16); - for n in &a.nalus { - b.u16(n.len() as u16); - b.bytes(n); - } - } - b.buf - } - - fn nalus_of(&self, t: u8) -> Vec<&[u8]> { - self.arrays.iter().filter(|a| a.nal_type == t).flat_map(|a| a.nalus.iter().map(|n| n.as_slice())).collect() - } - pub fn vps(&self) -> Vec<&[u8]> { - self.nalus_of(32) - } - pub fn sps(&self) -> Vec<&[u8]> { - self.nalus_of(33) - } - pub fn pps(&self) -> Vec<&[u8]> { - self.nalus_of(34) - } -} - -/// `av1C` — AV1CodecConfigurationRecord. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct Av1Config { - pub seq_profile: u8, - pub seq_level_idx_0: u8, - pub seq_tier_0: bool, - pub high_bitdepth: bool, - pub twelve_bit: bool, - pub monochrome: bool, - pub chroma_subsampling_x: bool, - pub chroma_subsampling_y: bool, - pub chroma_sample_position: u8, - pub initial_presentation_delay_minus_one: Option, - pub config_obus: Vec, -} - -impl Av1Config { - pub fn parse(data: &[u8]) -> Result { - let mut c = Cur::new(data, "av1C"); - let m = c.u8()?; - if m & 0x80 == 0 { - return Err(Error::Invalid("av1C marker bit not set".into())); - } - let a = c.u8()?; - let b = c.u8()?; - let d = c.u8()?; - Ok(Av1Config { - seq_profile: a >> 5, - seq_level_idx_0: a & 0x1F, - seq_tier_0: b & 0x80 != 0, - high_bitdepth: b & 0x40 != 0, - twelve_bit: b & 0x20 != 0, - monochrome: b & 0x10 != 0, - chroma_subsampling_x: b & 0x08 != 0, - chroma_subsampling_y: b & 0x04 != 0, - chroma_sample_position: b & 3, - initial_presentation_delay_minus_one: if d & 0x10 != 0 { Some(d & 0x0F) } else { None }, - config_obus: c.rest().to_vec(), - }) - } - pub fn to_bytes(&self) -> Vec { - let mut b = BoxBuf::new(); - b.u8(0x81); - b.u8((self.seq_profile << 5) | (self.seq_level_idx_0 & 0x1F)); - b.u8(((self.seq_tier_0 as u8) << 7) - | ((self.high_bitdepth as u8) << 6) - | ((self.twelve_bit as u8) << 5) - | ((self.monochrome as u8) << 4) - | ((self.chroma_subsampling_x as u8) << 3) - | ((self.chroma_subsampling_y as u8) << 2) - | (self.chroma_sample_position & 3)); - b.u8(match self.initial_presentation_delay_minus_one { - Some(d) => 0x10 | (d & 0x0F), - None => 0, - }); - b.bytes(&self.config_obus); - b.buf - } -} - -/// `vpcC` — VP codec configuration (version 1). -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct VpcConfig { - pub profile: u8, - pub level: u8, - pub bit_depth: u8, - pub chroma_subsampling: u8, - pub full_range: bool, - pub colour_primaries: u8, - pub transfer_characteristics: u8, - pub matrix_coefficients: u8, - pub codec_init: Vec, -} - -impl VpcConfig { - pub fn parse(data: &[u8]) -> Result { - let mut c = Cur::new(data, "vpcC"); - let (version, _) = c.full_header()?; - if version == 0 { - // Version 0 draft layout: profile, level, bitDepth(4)|colorSpace(4), chroma(4)|transfer(3)|range(1) - let profile = c.u8()?; - let level = c.u8()?; - let a = c.u8()?; - let b = c.u8()?; - return Ok(VpcConfig { profile, level, bit_depth: a >> 4, chroma_subsampling: b >> 4, full_range: b & 1 != 0, ..Default::default() }); - } - let profile = c.u8()?; - let level = c.u8()?; - let a = c.u8()?; - let colour_primaries = c.u8()?; - let transfer_characteristics = c.u8()?; - let matrix_coefficients = c.u8()?; - let n = c.u16()? as usize; - let codec_init = c.bytes(n)?.to_vec(); - Ok(VpcConfig { - profile, - level, - bit_depth: a >> 4, - chroma_subsampling: (a >> 1) & 7, - full_range: a & 1 != 0, - colour_primaries, - transfer_characteristics, - matrix_coefficients, - codec_init, - }) - } - pub fn to_bytes(&self) -> Vec { - let mut b = BoxBuf::new(); - b.u32(1 << 24); - b.u8(self.profile); - b.u8(self.level); - b.u8((self.bit_depth << 4) | ((self.chroma_subsampling & 7) << 1) | self.full_range as u8); - b.u8(self.colour_primaries); - b.u8(self.transfer_characteristics); - b.u8(self.matrix_coefficients); - b.u16(self.codec_init.len() as u16); - b.bytes(&self.codec_init); - b.buf - } -} - -/// AAC (MPEG-4 audio) configuration from `esds`. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct AacConfig { - /// AudioSpecificConfig bytes (DecoderSpecificInfo). - pub asc: Vec, - /// Audio object type (2 = AAC-LC, 5 = SBR/HE-AAC, 29 = PS …) as signalled first in the ASC. - pub object_type: u8, - /// Sampling rate signalled by the ASC (core rate; 0 if the ASC is missing). - pub sample_rate: u32, - /// Channel configuration from the ASC (0 = defined elsewhere). - pub channel_config: u8, - /// DecoderConfigDescriptor fields. - pub max_bitrate: u32, - pub avg_bitrate: u32, - pub buffer_size: u32, -} - -const AAC_RATES: [u32; 13] = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; - -impl AacConfig { - /// Build from an AudioSpecificConfig, decoding its header fields. - pub fn from_asc(asc: Vec) -> Self { - let (object_type, sample_rate, channel_config) = parse_asc(&asc).unwrap_or((0, 0, 0)); - AacConfig { asc, object_type, sample_rate, channel_config, max_bitrate: 0, avg_bitrate: 0, buffer_size: 0 } - } - /// Channel count implied by the channel configuration (0 if unspecified). - pub fn channels(&self) -> u32 { - match self.channel_config { - 1..=6 => self.channel_config as u32, - 7 => 8, - _ => 0, - } - } -} - -/// Decode (audio object type, sampling frequency, channel configuration) from an AudioSpecificConfig. -pub fn parse_asc(asc: &[u8]) -> Option<(u8, u32, u8)> { - let mut r = BitReader::new(asc); - let aot = |r: &mut BitReader| -> Option { - let t = r.read_bits(5).ok()? as u8; - if t == 31 { Some(32 + r.read_bits(6).ok()? as u8) } else { Some(t) } - }; - let rate = |r: &mut BitReader| -> Option { - let i = r.read_bits(4).ok()? as usize; - if i == 15 { r.read_bits(24).ok() } else { AAC_RATES.get(i).copied() } - }; - let t = aot(&mut r)?; - let sr = rate(&mut r)?; - let ch = r.read_bits(4).ok()? as u8; - Some((t, sr, ch)) -} - -/// Uncompressed PCM description. -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub struct PcmConfig { - pub bits: u16, - pub float: bool, - pub big_endian: bool, - /// Signed integer samples (8-bit `raw ` is unsigned). - pub signed: bool, - pub channels: u32, - pub sample_rate: f64, -} - -impl PcmConfig { - /// Bytes per interleaved audio frame (all channels). - /// `None` when `channels` times the bytes per sample does not fit in `u32`. - pub fn bytes_per_frame(&self) -> Option { - u32::from(self.bits).div_ceil(8).checked_mul(self.channels.max(1)) - } -} - -/// `dOps` — Opus specific box. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct OpusConfig { - pub output_channels: u8, - pub pre_skip: u16, - pub input_sample_rate: u32, - pub output_gain: i16, - pub channel_mapping_family: u8, - /// StreamCount, CoupledCount and ChannelMapping bytes when family != 0. - pub channel_mapping: Vec, -} - -impl OpusConfig { - pub fn parse(data: &[u8]) -> Result { - let mut c = Cur::new(data, "dOps"); - let _version = c.u8()?; - Ok(OpusConfig { - output_channels: c.u8()?, - pre_skip: c.u16()?, - input_sample_rate: c.u32()?, - output_gain: c.i16()?, - channel_mapping_family: c.u8()?, - channel_mapping: c.rest().to_vec(), - }) - } - pub fn to_bytes(&self) -> Vec { - let mut b = BoxBuf::new(); - b.u8(0); - b.u8(self.output_channels); - b.u16(self.pre_skip); - b.u32(self.input_sample_rate); - b.i16(self.output_gain); - b.u8(self.channel_mapping_family); - b.bytes(&self.channel_mapping); - b.buf - } -} - -/// `dfLa` — FLAC metadata blocks, with STREAMINFO decoded. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct FlacConfig { - /// Raw metadata blocks (each with its 4-byte header) as stored in `dfLa`. - pub metadata_blocks: Vec, - pub sample_rate: u32, - pub channels: u8, - pub bits_per_sample: u8, - pub total_samples: u64, -} - -impl FlacConfig { - pub fn parse(data: &[u8]) -> Result { - let mut c = Cur::new(data, "dfLa"); - c.full_header()?; - let blocks = c.rest().to_vec(); - let mut cfg = FlacConfig { metadata_blocks: blocks.clone(), ..Default::default() }; - let mut b = Cur::new(&blocks, "dfLa"); - let hdr = b.u32()?; - if (hdr >> 24) & 0x7F == 0 { - let si = b.bytes(((hdr & 0xFF_FFFF) as usize).min(34))?; - if si.len() >= 18 { - let mut r = BitReader::new(&si[10..18]); - cfg.sample_rate = r.read_bits(20).unwrap_or(0); - cfg.channels = r.read_bits(3).unwrap_or(0) as u8 + 1; - cfg.bits_per_sample = r.read_bits(5).unwrap_or(0) as u8 + 1; - cfg.total_samples = r.read_bits_u64(36).unwrap_or(0); - } - } - Ok(cfg) - } -} - -/// QuickTime timecode sample description (`tmcd`) plus the start frame read from its first sample. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct TimecodeConfig { - /// Bit 0 drop frame, bit 1 24-hour max, bit 2 negative times OK, bit 3 counter. - pub flags: u32, - pub timescale: u32, - pub frame_duration: u32, - /// Nominal frames per second (`numberOfFrames`), e.g. 30 for 29.97. - pub frames_per_second: u8, - /// Frame number of the first timecode sample (filled in by the demuxer). - pub start_frame: Option, - /// Source reel name (`name` box) if present. - pub name: Option, -} - -impl TimecodeConfig { - pub fn drop_frame(&self) -> bool { - self.flags & 1 != 0 - } - /// Format a frame number as `HH:MM:SS:FF` (`;` before the frames when drop-frame). - pub fn format_frame(&self, frame: u32) -> String { - let fps = self.frames_per_second.max(1) as u64; - let mut f = frame as u64; - if self.drop_frame() && fps >= 30 { - let drop = (fps / 30) * 2; // 2 frames per minute at 30, 4 at 60 - let per_min = fps * 60 - drop; - let per_10 = per_min * 10 + drop; - let d = f / per_10; - let m = f % per_10; - f += drop * 9 * d + if m > drop { drop * ((m - drop) / per_min) } else { 0 }; - } - let ff = f % fps; - let s = (f / fps) % 60; - let mi = (f / fps / 60) % 60; - let h = (f / fps / 3600) % 24; - let sep = if self.drop_frame() { ';' } else { ':' }; - format!("{h:02}:{mi:02}:{s:02}{sep}{ff:02}") - } -} - -// --------------------------------------------------------------------------------------------- -// Parsing - -fn is_video_fourcc(f: &[u8; 4]) -> bool { - matches!( - f, - b"avc1" - | b"avc3" - | b"hvc1" - | b"hev1" - | b"av01" - | b"vp09" - | b"vp08" - | b"apch" - | b"apcn" - | b"apcs" - | b"apco" - | b"ap4h" - | b"ap4x" - | b"jpeg" - | b"mjpa" - | b"mjpb" - | b"AVdn" - | b"AVdh" - | b"mp4v" - | b"encv" - | b"raw " - | b"2vuy" - | b"v210" - ) -} - -fn is_audio_fourcc(f: &[u8; 4]) -> bool { - matches!( - f, - b"mp4a" - | b"lpcm" - | b"sowt" - | b"twos" - | b"in24" - | b"in32" - | b"fl32" - | b"fl64" - | b"ipcm" - | b"fpcm" - | b"alac" - | b"Opus" - | b"fLaC" - | b"ac-3" - | b"ec-3" - | b".mp3" - | b"enca" - | b"ulaw" - | b"alaw" - | b"NONE" - ) -} - -/// Parse one `stsd` entry. `handler` is the track handler type; `is_qt` selects QuickTime -/// interpretation of the sound description version field. -pub(crate) fn parse_sample_entry(format: FourCc, payload: &[u8], handler: FourCc, is_qt: bool) -> Result { - let mut c = Cur::new(payload, "sample entry"); - c.skip(6)?; - let data_reference_index = c.u16()?; - let f = &format.0; - let unknown = || CodecConfig::Unknown { fourcc: format, raw: payload.to_vec() }; - if f == b"tmcd" || (handler == b"tmcd" && !is_video_fourcc(f)) { - let mut tc = TimecodeConfig::default(); - c.skip(4)?; - tc.flags = c.u32()?; - tc.timescale = c.u32()?; - tc.frame_duration = c.u32()?; - tc.frames_per_second = c.u8()?; - c.skip(1)?; - for b in boxes(c.rest()).map_while(|b| b.ok()) { - if b.kind == b"name" && b.payload.len() >= 4 { - let n = u16::from_be_bytes([b.payload[0], b.payload[1]]) as usize; - let s = b.payload.get(4..4 + n).unwrap_or(&b.payload[4..]); - tc.name = Some(String::from_utf8_lossy(s).into_owned()); - } - } - return Ok(SampleEntry { format, data_reference_index, codec: CodecConfig::Timecode(tc), video: None, audio: None, bitrate: None }); - } - let video_like = is_video_fourcc(f) || (handler == b"vide" && !is_audio_fourcc(f)); - let audio_like = is_audio_fourcc(f) || (handler == b"soun" && !video_like); - if video_like { - let mut v = VideoParams::default(); - let _version = c.u16()?; - let _revision = c.u16()?; - v.vendor = c.fourcc()?; - v.temporal_quality = c.u32()?; - v.spatial_quality = c.u32()?; - v.width = c.u16()?; - v.height = c.u16()?; - v.horiz_resolution = c.u32()?; - v.vert_resolution = c.u32()?; - c.skip(4)?; - v.frame_count = c.u16()?; - let name = c.bytes(32)?; - let n = (name[0] as usize).min(31); - v.compressor_name = String::from_utf8_lossy(&name[1..1 + n]).into_owned(); - v.depth = c.u16()?; - let _ctab = c.i16()?; - let mut codec = None; - let mut bitrate = None; - for b in boxes(c.rest()).map_while(|b| b.ok()) { - let p = b.payload; - match &b.kind.0 { - b"avcC" if matches!(f, b"avc1" | b"avc3") => codec = Some(CodecConfig::Avc(AvcConfig::parse(p)?)), - b"hvcC" if matches!(f, b"hvc1" | b"hev1") => codec = Some(CodecConfig::Hevc(HevcConfig::parse(p)?)), - b"av1C" if f == b"av01" => codec = Some(CodecConfig::Av1(Av1Config::parse(p)?)), - b"vpcC" if f == b"vp09" => codec = Some(CodecConfig::Vp9(VpcConfig::parse(p)?)), - b"colr" => v.color = parse_colr(p).ok(), - b"mdcv" => v.mastering_display = MasteringDisplay::parse(p), - b"clli" if p.len() >= 4 => v.content_light = Some((u16::from_be_bytes([p[0], p[1]]), u16::from_be_bytes([p[2], p[3]]))), - b"pasp" => { - let mut c = Cur::new(p, "pasp"); - v.pixel_aspect = Some((c.u32()?, c.u32()?)); - } - b"clap" => { - let mut c = Cur::new(p, "clap"); - v.clean_aperture = Some(CleanAperture { - width: (c.u32()?, c.u32()?), - height: (c.u32()?, c.u32()?), - horiz_offset: (c.i32()?, c.u32()?), - vert_offset: (c.i32()?, c.u32()?), - }); - } - b"fiel" => { - let mut c = Cur::new(p, "fiel"); - v.field_info = Some(FieldInfo { fields: c.u8()?, detail: c.u8()? }); - } - b"gama" => v.gamma = Some(Cur::new(p, "gama").u32()?), - b"btrt" => bitrate = Some(parse_btrt(p)?), - _ => {} - } - } - let codec = match codec { - Some(c) => c, - None => match f { - b"apch" | b"apcn" | b"apcs" | b"apco" | b"ap4h" | b"ap4x" => CodecConfig::ProRes { fourcc: format }, - b"jpeg" | b"mjpa" | b"mjpb" => CodecConfig::Jpeg { fourcc: format }, - b"AVdn" | b"AVdh" => CodecConfig::Dnx { fourcc: format }, - _ => unknown(), - }, - }; - return Ok(SampleEntry { format, data_reference_index, codec, video: Some(v), audio: None, bitrate }); - } - if audio_like { - let mut a = AudioParams { qt_version: c.u16()?, ..Default::default() }; - let _revision = c.u16()?; - let _vendor = c.u32()?; - a.channels = c.u16()? as u32; - a.sample_size = c.u16()?; - a.compression_id = c.i16()?; - let _packet_size = c.u16()?; - a.sample_rate = c.u32()? as f64 / 65536.0; - if is_qt && a.qt_version == 1 { - a.samples_per_packet = c.u32()?; - a.bytes_per_packet = c.u32()?; - a.bytes_per_frame = c.u32()?; - a.bytes_per_sample = c.u32()?; - } else if is_qt && a.qt_version == 2 { - let _size_of_struct = c.u32()?; - a.sample_rate = c.f64()?; - a.channels = c.u32()?; - if a.channels > u32::from(u16::MAX) { - return invalid("channel count out of range"); - } - let _always = c.u32()?; - a.const_bits_per_channel = c.u32()?; - a.lpcm_flags = c.u32()?; - a.const_bytes_per_packet = c.u32()?; - a.const_frames_per_packet = c.u32()?; - } - // Flatten QuickTime `wave` children into the list of extension boxes. - let mut ext: Vec<(FourCc, &[u8])> = Vec::new(); - for b in boxes(c.rest()).map_while(|b| b.ok()) { - if b.kind == b"wave" { - for w in boxes(b.payload).map_while(|b| b.ok()) { - ext.push((w.kind, w.payload)); - } - } else { - ext.push((b.kind, b.payload)); - } - } - let get = |k: &[u8; 4]| ext.iter().find(|(kk, _)| kk == k).map(|(_, p)| *p); - if let Some(p) = get(b"enda") { - let v = if p.len() >= 2 { u16::from_be_bytes([p[0], p[1]]) } else { p.first().copied().unwrap_or(0) as u16 }; - a.little_endian = Some(v != 0); - } - let bitrate = get(b"btrt").and_then(|p| parse_btrt(p).ok()); - let codec = match f { - b"mp4a" => match get(b"esds") { - Some(p) => parse_esds(p)?, - None => unknown(), - }, - b".mp3" => CodecConfig::Mp3, - b"alac" => match get(b"alac") { - Some(p) if p.len() >= 4 => CodecConfig::Alac { cookie: p[4..].to_vec() }, - _ => unknown(), - }, - b"Opus" => match get(b"dOps") { - Some(p) => CodecConfig::Opus(OpusConfig::parse(p)?), - None => unknown(), - }, - b"fLaC" => match get(b"dfLa") { - Some(p) => CodecConfig::Flac(FlacConfig::parse(p)?), - None => unknown(), - }, - b"ac-3" => CodecConfig::Ac3 { dac3: get(b"dac3").unwrap_or(&[]).to_vec() }, - b"ec-3" => CodecConfig::Eac3 { dec3: get(b"dec3").unwrap_or(&[]).to_vec() }, - b"lpcm" | b"sowt" | b"twos" | b"in24" | b"in32" | b"fl32" | b"fl64" | b"raw " | b"NONE" => CodecConfig::Pcm(qt_pcm(f, &a)?), - b"ipcm" | b"fpcm" => { - let mut pcm = PcmConfig { - bits: a.sample_size, - float: f == b"fpcm", - big_endian: true, - signed: true, - channels: a.channels, - sample_rate: a.sample_rate, - }; - if let Some(p) = get(b"pcmC") { - let mut c = Cur::new(p, "pcmC"); - c.full_header()?; - let flags = c.u8()?; - pcm.big_endian = flags & 1 == 0; - pcm.bits = c.u8()? as u16; - } - if pcm.bytes_per_frame().is_none() { - return invalid("PCM frame size overflows"); - } - CodecConfig::Pcm(pcm) - } - _ => unknown(), - }; - return Ok(SampleEntry { format, data_reference_index, codec, video: None, audio: Some(a), bitrate }); - } - Ok(SampleEntry { format, data_reference_index, codec: unknown(), video: None, audio: None, bitrate: None }) -} - -fn qt_pcm(f: &[u8; 4], a: &AudioParams) -> Result { - let mut p = PcmConfig { channels: a.channels, sample_rate: a.sample_rate, signed: true, ..Default::default() }; - match f { - b"lpcm" => { - p.bits = u16::try_from(a.const_bits_per_channel).map_err(|_| Error::Invalid("const_bits_per_channel out of range".into()))?; - p.float = a.lpcm_flags & 1 != 0; - p.big_endian = a.lpcm_flags & 2 != 0; - p.signed = a.lpcm_flags & 4 != 0 || p.float; - if p.bits == 0 { - p.bits = a.sample_size; - } - } - b"sowt" => { - p.bits = if a.sample_size == 0 { 16 } else { a.sample_size }; - } - b"twos" => { - p.bits = if a.sample_size == 0 { 16 } else { a.sample_size }; - p.big_endian = true; - } - b"raw " | b"NONE" => { - p.bits = if a.sample_size == 0 { 8 } else { a.sample_size }; - p.signed = p.bits != 8; - p.big_endian = true; - } - b"in24" | b"in32" | b"fl32" | b"fl64" => { - p.bits = match f { - b"in24" => 24, - b"in32" | b"fl32" => 32, - _ => 64, - }; - p.float = f[0] == b'f'; - p.big_endian = !a.little_endian.unwrap_or(false); - } - _ => {} - } - if p.bytes_per_frame().is_none() { - return invalid("PCM frame size overflows"); - } - Ok(p) -} - -fn parse_btrt(p: &[u8]) -> Result { - let mut c = Cur::new(p, "btrt"); - Ok(BitRate { buffer_size: c.u32()?, max_bitrate: c.u32()?, avg_bitrate: c.u32()? }) -} - -fn parse_colr(p: &[u8]) -> Result { - let mut c = Cur::new(p, "colr"); - let kind = c.fourcc()?; - Ok(match &kind.0 { - b"nclx" => { - ColorInfo::Nclx { primaries: c.u16()?, transfer: c.u16()?, matrix: c.u16()?, full_range: c.u8().map(|v| v & 0x80 != 0).unwrap_or(false) } - } - b"nclc" => ColorInfo::Nclc { primaries: c.u16()?, transfer: c.u16()?, matrix: c.u16()? }, - _ => ColorInfo::Icc { kind, profile: c.rest().to_vec() }, - }) -} - -fn descriptor(c: &mut Cur) -> Result<(u8, usize)> { - let tag = c.u8()?; - let mut len = 0usize; - for _ in 0..4 { - let b = c.u8()?; - len = (len << 7) | (b & 0x7F) as usize; - if b & 0x80 == 0 { - break; - } - } - Ok((tag, len)) -} - -fn parse_esds(p: &[u8]) -> Result { - let mut c = Cur::new(p, "esds"); - c.full_header()?; - let (tag, len) = descriptor(&mut c)?; - if tag != 3 { - return Err(Error::Invalid(format!("esds: expected ES_Descriptor, got tag {tag}"))); - } - let es = c.bytes(len.min(c.remaining()))?; - let mut c = Cur::new(es, "ES_Descriptor"); - c.u16()?; - let fl = c.u8()?; - if fl & 0x80 != 0 { - c.u16()?; - } - if fl & 0x40 != 0 { - let n = c.u8()? as usize; - c.skip(n)?; - } - if fl & 0x20 != 0 { - c.u16()?; - } - while c.remaining() > 0 { - let (tag, len) = descriptor(&mut c)?; - let body = c.bytes(len.min(c.remaining()))?; - if tag != 4 { - continue; - } - let mut d = Cur::new(body, "DecoderConfigDescriptor"); - let oti = d.u8()?; - let _stream_type = d.u8()?; - let buffer_size = d.u24()?; - let max_bitrate = d.u32()?; - let avg_bitrate = d.u32()?; - let mut asc = Vec::new(); - while d.remaining() > 0 { - let (t, l) = descriptor(&mut d)?; - let b = d.bytes(l.min(d.remaining()))?; - if t == 5 { - asc = b.to_vec(); - break; - } - } - return Ok(match oti { - 0x69 | 0x6B => CodecConfig::Mp3, - _ => { - let mut a = AacConfig::from_asc(asc); - a.max_bitrate = max_bitrate; - a.avg_bitrate = avg_bitrate; - a.buffer_size = buffer_size; - CodecConfig::Aac(a) - } - }); - } - Err(Error::Invalid("esds without DecoderConfigDescriptor".into())) -} - -// --------------------------------------------------------------------------------------------- -// Serialization (muxer) - -fn put_descriptor(b: &mut BoxBuf, tag: u8, body: &[u8]) { - b.u8(tag); - let n = body.len() as u32; - // Always use the 4-byte length form for simplicity (as many encoders do). - b.u8(0x80 | ((n >> 21) & 0x7F) as u8); - b.u8(0x80 | ((n >> 14) & 0x7F) as u8); - b.u8(0x80 | ((n >> 7) & 0x7F) as u8); - b.u8((n & 0x7F) as u8); - b.bytes(body); -} - -pub(crate) fn esds_payload(oti: u8, asc: &[u8], max_bitrate: u32, avg_bitrate: u32, buffer_size: u32) -> Vec { - let mut dcd = BoxBuf::new(); - dcd.u8(oti); - dcd.u8(0x15); // streamType audio (5) << 2 | upstream 0 | reserved 1 - dcd.u8((buffer_size >> 16) as u8); - dcd.u16(buffer_size as u16); - dcd.u32(max_bitrate); - dcd.u32(avg_bitrate); - if !asc.is_empty() { - put_descriptor(&mut dcd, 5, asc); - } - let mut es = BoxBuf::new(); - es.u16(1); // ES_ID - es.u8(0); - put_descriptor(&mut es, 4, &dcd.buf); - put_descriptor(&mut es, 6, &[2]); // SLConfigDescriptor, predefined = 2 (MP4) - let mut b = BoxBuf::new(); - b.u32(0); - put_descriptor(&mut b, 3, &es.buf); - b.buf -} - -impl SampleEntry { - /// Convenience constructor for a video entry. - pub fn video(format: FourCc, codec: CodecConfig, width: u16, height: u16) -> Self { - let compressor_name = match &codec { - CodecConfig::ProRes { fourcc } => match &fourcc.0 { - b"apch" => "Apple ProRes 422 HQ", - b"apcn" => "Apple ProRes 422", - b"apcs" => "Apple ProRes 422 LT", - b"apco" => "Apple ProRes 422 Proxy", - b"ap4h" => "Apple ProRes 4444", - _ => "Apple ProRes 4444 XQ", - }, - CodecConfig::Jpeg { .. } => "Photo - JPEG", - _ => "", - }; - SampleEntry { - format, - data_reference_index: 1, - codec, - video: Some(VideoParams { - width, - height, - horiz_resolution: 0x0048_0000, - vert_resolution: 0x0048_0000, - frame_count: 1, - compressor_name: compressor_name.into(), - depth: 24, - ..Default::default() - }), - audio: None, - bitrate: None, - } - } - - /// Convenience constructor for an audio entry (ISO v0 layout; the muxer upgrades PCM in MOV to v2). - pub fn audio(format: FourCc, codec: CodecConfig, channels: u32, sample_rate: f64, sample_size: u16) -> Self { - SampleEntry { - format, - data_reference_index: 1, - codec, - video: None, - audio: Some(AudioParams { channels, sample_rate, sample_size, ..Default::default() }), - bitrate: None, - } - } - - /// H.264 entry (`avc1`). - pub fn avc(cfg: AvcConfig, width: u16, height: u16) -> Self { - Self::video(FourCc(*b"avc1"), CodecConfig::Avc(cfg), width, height) - } - /// HEVC entry (`hvc1`, parameter sets in the config only). - pub fn hevc(cfg: HevcConfig, width: u16, height: u16) -> Self { - Self::video(FourCc(*b"hvc1"), CodecConfig::Hevc(cfg), width, height) - } - /// ProRes entry; `fourcc` one of `apch`/`apcn`/`apcs`/`apco`/`ap4h`/`ap4x`. - pub fn prores(fourcc: FourCc, width: u16, height: u16) -> Self { - let mut e = Self::video(fourcc, CodecConfig::ProRes { fourcc }, width, height); - if let Some(v) = e.video.as_mut() - && matches!(&fourcc.0, b"ap4h" | b"ap4x") - { - v.depth = 32; - } - e - } - /// Avid DNxHD / DNxHR entry (`AVdn`, or `AVdh` for DNxHR). - pub fn dnx(fourcc: FourCc, width: u16, height: u16) -> Self { - let mut e = Self::video(fourcc, CodecConfig::Dnx { fourcc }, width, height); - if let Some(v) = e.video.as_mut() { - v.depth = 24; - } - e - } - /// Motion-JPEG entry (`jpeg`). - pub fn jpeg(width: u16, height: u16) -> Self { - let f = FourCc(*b"jpeg"); - Self::video(f, CodecConfig::Jpeg { fourcc: f }, width, height) - } - /// AAC entry (`mp4a` + `esds`) from an AudioSpecificConfig. - pub fn aac(asc: Vec, channels: u32, sample_rate: u32) -> Self { - Self::audio(FourCc(*b"mp4a"), CodecConfig::Aac(AacConfig::from_asc(asc)), channels, sample_rate as f64, 16) - } - /// PCM entry. The muxer picks the on-disk format (`sowt`/`twos`/`lpcm` for MOV, `ipcm`/`fpcm` for MP4). - pub fn pcm(cfg: PcmConfig) -> Self { - Self::audio(FourCc(*b"lpcm"), CodecConfig::Pcm(cfg), cfg.channels, cfg.sample_rate, cfg.bits) - } - - /// Serialize as a complete sample entry box. For PCM the format is chosen from the brand. - pub(crate) fn write(&self, b: &mut BoxBuf, qt: bool) -> Result<()> { - if let CodecConfig::Unknown { fourcc, raw } = &self.codec { - let m = b.start(&fourcc.0); - b.bytes(raw); - b.end(m); - return Ok(()); - } - if let CodecConfig::Timecode(tc) = &self.codec { - let m = b.start(b"tmcd"); - b.zeros(6); - b.u16(self.data_reference_index); - b.u32(0); - b.u32(tc.flags); - b.u32(tc.timescale); - b.u32(tc.frame_duration); - b.u8(tc.frames_per_second); - b.u8(0); - if let Some(name) = &tc.name { - let n = b.start(b"name"); - b.u16(name.len() as u16); - b.u16(0); - b.bytes(name.as_bytes()); - b.end(n); - } - b.end(m); - return Ok(()); - } - if let Some(v) = &self.video { - let m = b.start(&self.format.0); - b.zeros(6); - b.u16(self.data_reference_index); - b.u16(0); - b.u16(0); - b.u32(v.vendor.as_u32()); - b.u32(v.temporal_quality); - b.u32(v.spatial_quality); - b.u16(v.width); - b.u16(v.height); - b.u32(v.horiz_resolution); - b.u32(v.vert_resolution); - b.u32(0); - b.u16(v.frame_count); - let name = v.compressor_name.as_bytes(); - let n = name.len().min(31); - let mut nb = [0u8; 32]; - nb[0] = n as u8; - nb[1..1 + n].copy_from_slice(&name[..n]); - b.bytes(&nb); - b.u16(v.depth); - b.i16(-1); - match &self.codec { - CodecConfig::Avc(c) => b.leaf(b"avcC", &c.to_bytes()), - CodecConfig::Hevc(c) => b.leaf(b"hvcC", &c.to_bytes()), - CodecConfig::Av1(c) => b.leaf(b"av1C", &c.to_bytes()), - CodecConfig::Vp9(c) => b.leaf(b"vpcC", &c.to_bytes()), - _ => {} - } - if let Some(g) = v.gamma { - b.leaf(b"gama", &g.to_be_bytes()); - } - if let Some(fi) = v.field_info { - b.leaf(b"fiel", &[fi.fields, fi.detail]); - } - if let Some(ci) = &v.color { - let m = b.start(b"colr"); - match ci { - ColorInfo::Nclx { primaries, transfer, matrix, full_range } => { - b.bytes(b"nclx"); - b.u16(*primaries); - b.u16(*transfer); - b.u16(*matrix); - b.u8(if *full_range { 0x80 } else { 0 }); - } - ColorInfo::Nclc { primaries, transfer, matrix } => { - b.bytes(b"nclc"); - b.u16(*primaries); - b.u16(*transfer); - b.u16(*matrix); - } - ColorInfo::Icc { kind, profile } => { - b.bytes(&kind.0); - b.bytes(profile); - } - } - b.end(m); - } - if let Some(md) = &v.mastering_display { - b.leaf(b"mdcv", &md.to_bytes()); - } - if let Some((cll, fall)) = v.content_light { - let mut d = cll.to_be_bytes().to_vec(); - d.extend_from_slice(&fall.to_be_bytes()); - b.leaf(b"clli", &d); - } - if let Some((h, vv)) = v.pixel_aspect { - let m = b.start(b"pasp"); - b.u32(h); - b.u32(vv); - b.end(m); - } - if let Some(c) = v.clean_aperture { - let m = b.start(b"clap"); - b.u32(c.width.0); - b.u32(c.width.1); - b.u32(c.height.0); - b.u32(c.height.1); - b.i32(c.horiz_offset.0); - b.u32(c.horiz_offset.1); - b.i32(c.vert_offset.0); - b.u32(c.vert_offset.1); - b.end(m); - } - if let Some(br) = self.bitrate { - write_btrt(b, br); - } - b.end(m); - return Ok(()); - } - let a = self.audio.clone().ok_or_else(|| Error::Mux("sample entry has neither video nor audio params".into()))?; - // Choose the on-disk format and sound description version. - let (format, version, pcm) = match &self.codec { - CodecConfig::Pcm(p) => { - if qt { - if p.bits == 16 && !p.float { - (if p.big_endian { *b"twos" } else { *b"sowt" }, 0u16, Some(*p)) - } else if p.bits == 8 && !p.float { - (*b"raw ", 0, Some(*p)) - } else { - (*b"lpcm", 2, Some(*p)) - } - } else { - (if p.float { *b"fpcm" } else { *b"ipcm" }, 0, Some(*p)) - } - } - _ => (self.format.0, if qt { a.qt_version.min(2) } else { 0 }, None), - }; - let m = b.start(&format); - b.zeros(6); - b.u16(self.data_reference_index); - if version == 2 { - let p = pcm.unwrap_or_default(); - b.u16(2); - b.u16(0); - b.u32(0); - b.u16(3); - b.u16(16); - b.i16(-2); - b.u16(0); - b.u32(65536); - b.u32(72); - b.f64(p.sample_rate); - b.u32(p.channels); - b.u32(0x7F00_0000); - b.u32(p.bits as u32); - let flags = (p.float as u32) | ((p.big_endian as u32) << 1) | (((p.signed && !p.float) as u32) << 2) | 8; - b.u32(flags); - b.u32(p.bytes_per_frame().unwrap_or(0)); - b.u32(1); - } else { - b.u16(version); - b.u16(0); - b.u32(0); - b.u16(a.channels.min(u16::MAX as u32) as u16); - b.u16(match pcm { - Some(p) => p.bits, - None => { - if a.sample_size == 0 { - 16 - } else { - a.sample_size - } - } - }); - b.i16(if version == 1 { a.compression_id } else { 0 }); - b.u16(0); - let sr = if a.sample_rate < 65536.0 { (a.sample_rate * 65536.0).round() as u32 } else { 0 }; - b.u32(sr); - if version == 1 { - b.u32(a.samples_per_packet); - b.u32(a.bytes_per_packet); - b.u32(a.bytes_per_frame); - b.u32(a.bytes_per_sample); - } - } - match &self.codec { - CodecConfig::Aac(c) => { - b.leaf(b"esds", &esds_payload(0x40, &c.asc, c.max_bitrate, c.avg_bitrate, c.buffer_size)); - } - CodecConfig::Mp3 => { - if format == *b"mp4a" { - b.leaf(b"esds", &esds_payload(0x6B, &[], 0, 0, 0)); - } - } - CodecConfig::Alac { cookie } => { - let m = b.start_full(b"alac", 0, 0); - b.bytes(cookie); - b.end(m); - } - CodecConfig::Opus(o) => b.leaf(b"dOps", &o.to_bytes()), - CodecConfig::Flac(fl) => { - let m = b.start_full(b"dfLa", 0, 0); - b.bytes(&fl.metadata_blocks); - b.end(m); - } - CodecConfig::Ac3 { dac3 } => b.leaf(b"dac3", dac3), - CodecConfig::Eac3 { dec3 } => b.leaf(b"dec3", dec3), - CodecConfig::Pcm(p) if !qt => { - let m = b.start_full(b"pcmC", 0, 0); - b.u8(if p.big_endian { 0 } else { 1 }); - b.u8(p.bits as u8); - b.end(m); - } - CodecConfig::Pcm(p) if p.channels == 6 => { - // QuickTime channel layout: kAudioChannelLayoutTag_MPEG_5_1_A (L R C LFE Ls Rs) - let m = b.start_full(b"chan", 0, 0); - b.u32((121 << 16) | 6); - b.u32(0); - b.u32(0); - b.end(m); - } - _ => {} - } - if let Some(br) = self.bitrate { - write_btrt(b, br); - } - b.end(m); - Ok(()) - } -} - -fn write_btrt(b: &mut BoxBuf, br: BitRate) { - let m = b.start(b"btrt"); - b.u32(br.buffer_size); - b.u32(br.max_bitrate); - b.u32(br.avg_bitrate); - b.end(m); -} - -#[cfg(test)] -mod tests { - use super::*; - - fn roundtrip(e: &SampleEntry, qt: bool, handler: &[u8; 4]) -> SampleEntry { - let mut b = BoxBuf::new(); - e.write(&mut b, qt).unwrap(); - let bx = crate::bytes::boxes(&b.buf).next().unwrap().unwrap(); - parse_sample_entry(bx.kind, bx.payload, FourCc(*handler), qt).unwrap() - } - - #[test] - fn avcc_roundtrip() { - let cfg = AvcConfig::new(vec![vec![0x67, 0x64, 0x00, 0x1F, 0xAC]], vec![vec![0x68, 0xEB]], 4); - assert_eq!(cfg.profile, 0x64); - assert_eq!(AvcConfig::parse(&cfg.to_bytes()).unwrap(), cfg); - let mut e = SampleEntry::avc(cfg, 1920, 1080); - e.video.as_mut().unwrap().color = Some(ColorInfo::Nclx { primaries: 1, transfer: 1, matrix: 1, full_range: false }); - e.video.as_mut().unwrap().pixel_aspect = Some((4, 3)); - e.video.as_mut().unwrap().field_info = Some(FieldInfo { fields: 2, detail: 9 }); - e.video.as_mut().unwrap().clean_aperture = - Some(CleanAperture { width: (1920, 1), height: (1080, 1), horiz_offset: (-2, 1), vert_offset: (0, 1) }); - e.video.as_mut().unwrap().mastering_display = Some(MasteringDisplay::bt2020(1000.0, 0.0001)); - e.video.as_mut().unwrap().content_light = Some((1000, 400)); - e.bitrate = Some(BitRate { buffer_size: 1, max_bitrate: 2, avg_bitrate: 3 }); - assert_eq!(roundtrip(&e, false, b"vide"), e); - assert_eq!(MasteringDisplay::bt2020(1000.0, 0.0001).max_nits(), 1000.0); - } - - #[test] - fn hvcc_roundtrip() { - let cfg = HevcConfig { - general_profile_idc: 1, - general_profile_compatibility_flags: 0x6000_0000, - general_constraint_indicator_flags: 0x9000_0000_0000, - general_level_idc: 93, - chroma_format_idc: 1, - bit_depth_luma: 8, - bit_depth_chroma: 8, - length_size: 4, - num_temporal_layers: 1, - temporal_id_nested: true, - arrays: vec![ - HevcNalArray { completeness: true, nal_type: 32, nalus: vec![vec![0x40, 1]] }, - HevcNalArray { completeness: true, nal_type: 33, nalus: vec![vec![0x42, 1, 2]] }, - HevcNalArray { completeness: true, nal_type: 34, nalus: vec![vec![0x44, 1]] }, - ], - ..Default::default() - }; - let back = HevcConfig::parse(&cfg.to_bytes()).unwrap(); - assert_eq!(back, cfg); - assert_eq!(back.sps(), vec![&[0x42u8, 1, 2][..]]); - } - - #[test] - fn av1_vp9_opus_roundtrip() { - let a = Av1Config { seq_profile: 1, seq_level_idx_0: 8, high_bitdepth: true, config_obus: vec![1, 2], ..Default::default() }; - assert_eq!(Av1Config::parse(&a.to_bytes()).unwrap(), a); - let v = VpcConfig { - profile: 2, - level: 31, - bit_depth: 10, - chroma_subsampling: 1, - full_range: true, - colour_primaries: 9, - transfer_characteristics: 16, - matrix_coefficients: 9, - codec_init: vec![], - }; - assert_eq!(VpcConfig::parse(&v.to_bytes()).unwrap(), v); - let o = OpusConfig { - output_channels: 2, - pre_skip: 312, - input_sample_rate: 48000, - output_gain: 0, - channel_mapping_family: 0, - channel_mapping: vec![], - }; - assert_eq!(OpusConfig::parse(&o.to_bytes()).unwrap(), o); - } - - #[test] - fn aac_esds_roundtrip() { - let e = SampleEntry::aac(vec![0x12, 0x10], 2, 44100); - match &e.codec { - CodecConfig::Aac(a) => { - assert_eq!((a.object_type, a.sample_rate, a.channel_config), (2, 44100, 2)); - } - _ => panic!(), - } - assert_eq!(roundtrip(&e, false, b"soun"), e); - assert_eq!(roundtrip(&e, true, b"soun"), e); - } - - fn qt_v2_lpcm(channels: u32, bits: u32) -> Vec { - let mut p = vec![0; 6]; - let u16b = |p: &mut Vec, v: u16| p.extend(v.to_be_bytes()); - let u32b = |p: &mut Vec, v: u32| p.extend(v.to_be_bytes()); - u16b(&mut p, 1); - u16b(&mut p, 2); - u16b(&mut p, 0); - u32b(&mut p, 0); - u16b(&mut p, 2); - u16b(&mut p, 16); - u16b(&mut p, 0); - u16b(&mut p, 0); - u32b(&mut p, 65536); - u32b(&mut p, 72); - p.extend(48000.0f64.to_be_bytes()); - u32b(&mut p, channels); - u32b(&mut p, 0); - u32b(&mut p, bits); - u32b(&mut p, 0); - u32b(&mut p, 0); - u32b(&mut p, 1); - p - } - - #[test] - fn pcm_frame_size_rejects_a_hostile_channel_count() { - assert_eq!(PcmConfig { bits: 16, channels: 2, sample_rate: 48000.0, ..Default::default() }.bytes_per_frame(), Some(4)); - assert!(PcmConfig { bits: 16, channels: u32::MAX, ..Default::default() }.bytes_per_frame().is_none()); - assert!(parse_sample_entry(FourCc(*b"lpcm"), &qt_v2_lpcm(u32::MAX, 16), FourCc(*b"soun"), true).is_err()); - assert!(parse_sample_entry(FourCc(*b"lpcm"), &qt_v2_lpcm(2, u32::MAX), FourCc(*b"soun"), true).is_err()); - let ok = parse_sample_entry(FourCc(*b"lpcm"), &qt_v2_lpcm(2, 16), FourCc(*b"soun"), true).unwrap(); - assert!(matches!(ok.codec, CodecConfig::Pcm(p) if p.bytes_per_frame() == Some(4))); - } - - #[test] - fn pcm_entries() { - for (bits, float, be, qt) in [ - (16, false, false, true), - (16, false, true, true), - (24, false, false, true), - (32, true, false, true), - (24, false, true, false), - (32, true, false, false), - ] { - let p = PcmConfig { bits, float, big_endian: be, signed: true, channels: 2, sample_rate: 48000.0 }; - let e = SampleEntry::pcm(p); - let back = roundtrip(&e, qt, b"soun"); - assert_eq!(back.codec, CodecConfig::Pcm(p), "{bits} {float} {be} {qt}"); - } - } - - #[test] - fn timecode_and_prores() { - let tc = - TimecodeConfig { flags: 1, timescale: 30000, frame_duration: 1001, frames_per_second: 30, start_frame: None, name: Some("reel".into()) }; - let e = SampleEntry { - format: FourCc(*b"tmcd"), - data_reference_index: 1, - codec: CodecConfig::Timecode(tc.clone()), - video: None, - audio: None, - bitrate: None, - }; - assert_eq!(roundtrip(&e, true, b"tmcd"), e); - assert_eq!(tc.format_frame(107892), "01:00:00;00"); - let nd = TimecodeConfig { flags: 0, frames_per_second: 24, ..Default::default() }; - assert_eq!(nd.format_frame(86400), "01:00:00:00"); - let p = SampleEntry::prores(FourCc(*b"apch"), 1920, 1080); - assert_eq!(roundtrip(&p, true, b"vide"), p); - } - - #[test] - fn asc_parse() { - assert_eq!(parse_asc(&[0x11, 0x90]), Some((2, 48000, 2))); - assert_eq!(parse_asc(&[]), None); - } -} diff --git a/crates/isobmff/src/demux.rs b/crates/isobmff/src/demux.rs deleted file mode 100644 index ee8eb473..00000000 --- a/crates/isobmff/src/demux.rs +++ /dev/null @@ -1,1174 +0,0 @@ -//! Demuxer: parses box structure lazily (headers + `moov`/`moof` only) and builds per-track sample tables. - -use crate::bytes::{Cur, FourCc, boxes, find, parse_box_header}; -use crate::codec::{CodecConfig, SampleEntry, parse_sample_entry}; -use crate::error::{Error, Result, invalid}; -use crate::source::ByteSource; -use std::collections::HashMap; - -/// Hard cap on per-track sample count (after PCM chunk merging). -const MAX_SAMPLES: usize = 1 << 26; -/// Hard cap on the size of an in-memory `moov`/`moof` box. -const MAX_HEADER_BOX: u64 = 1 << 30; - -/// One sample (access unit) of a track, in the track's media timescale. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -pub struct Sample { - /// Absolute byte offset in the file. - pub offset: u64, - pub size: u32, - /// Decode timestamp (raw media time, edit lists not applied). - pub dts: i64, - /// Presentation timestamp = dts + composition offset (raw media time). - pub pts: i64, - pub duration: u32, - pub is_sync: bool, - /// 1-based index into [`Track::entries`]. - pub description_index: u32, -} - -/// One edit list entry (`elst`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Edit { - /// Duration of this edit in the movie timescale. - pub segment_duration: u64, - /// Start time within the media (track timescale); `-1` marks an empty edit. - pub media_time: i64, - /// Media rate as 16.16 fixed point (0x0001_0000 = normal speed, 0 = dwell). - pub media_rate: i32, -} - -/// Broad classification of a track by its handler. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TrackKind { - Video, - Audio, - Timecode, - Subtitle, - Other, -} - -/// A sample-to-group mapping (`sbgp`) with the matching descriptions (`sgpd`) kept raw. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct SampleGroup { - pub grouping_type: FourCc, - /// (sample_count, group_description_index) runs. - pub runs: Vec<(u32, u32)>, - /// Raw description entries from `sgpd`. - pub descriptions: Vec>, -} - -/// A demuxed track. -#[derive(Clone, Debug, PartialEq)] -pub struct Track { - pub id: u32, - pub kind: TrackKind, - pub handler: FourCc, - pub handler_name: String, - /// Media timescale (`mdhd`). - pub timescale: u32, - /// Media duration in the media timescale (`mdhd`). - pub duration: u64, - /// Track duration in the movie timescale (`tkhd`). - pub tkhd_duration: u64, - /// ISO 639-2/T language code (e.g. `und`, `eng`). - pub language: String, - /// `tkhd` presentation size (16.16 fixed point converted to integer pixels, rounded down). - pub width: u32, - pub height: u32, - pub enabled: bool, - pub alternate_group: u16, - /// `tkhd` volume (8.8 fixed point). - pub volume: i16, - /// `tkhd` transformation matrix. - pub matrix: [i32; 9], - pub entries: Vec, - pub samples: Vec, - pub edits: Vec, - /// Offset to add to raw pts to get presentation time (track timescale), derived from the - /// leading empty edits and the first non-empty edit's `media_time`. - pub edit_offset: i64, - /// Track references (`tref`): (type, track ids). - pub references: Vec<(FourCc, Vec)>, - /// Raw `sdtp` bytes (one per sample) if present. - pub sdtp: Vec, - pub sample_groups: Vec, - /// True if uncompressed audio samples were merged into one [`Sample`] per chunk. - pub pcm_chunked: bool, - sync_index: Vec, - pts_order: Vec, -} - -impl Track { - /// The first sample entry's codec configuration. - pub fn codec(&self) -> Option<&CodecConfig> { - self.entries.first().map(|e| &e.codec) - } - pub fn video(&self) -> Option<&crate::codec::VideoParams> { - self.entries.first().and_then(|e| e.video.as_ref()) - } - /// Clockwise quarter turns (0–3) the `tkhd` matrix applies for display, or None when it is not - /// a pure 0/90/180/270° rotation (mirrored, scaled or sheared — left to the caller). - /// - /// ISO/IEC 14496-12 §8.3.2: a sample point (p, q) is displayed at p′ = a·p + c·q + x, - /// q′ = b·p + d·q + y for the matrix {a, b, u, c, d, v, x, y, w}, with y pointing down. So - /// {0, 1, −1, 0} sends the top row to the right-hand column (90° clockwise — iPhone portrait - /// video), {−1, 0, 0, −1} is 180° and {0, −1, 1, 0} is 270°. Only the signs matter: a·d − b·c - /// carries the scale, which the presentation size already reflects. - pub fn display_rotation(&self) -> Option { - let [a, b, _, c, d, _, _, _, _] = self.matrix; - match (a.signum(), b.signum(), c.signum(), d.signum()) { - (1, 0, 0, 1) => Some(0), - (0, 1, -1, 0) => Some(1), - (-1, 0, 0, -1) => Some(2), - (0, -1, 1, 0) => Some(3), - _ => None, - } - } - pub fn audio(&self) -> Option<&crate::codec::AudioParams> { - self.entries.first().and_then(|e| e.audio.as_ref()) - } - /// Presentation (edit-adjusted) pts of sample `i`. - pub fn presentation_pts(&self, i: usize) -> Option { - self.samples.get(i).map(|s| s.pts.saturating_add(self.edit_offset)) - } - /// Index of the sample being presented at raw media time `pts` (the last sample, in - /// presentation order, whose pts is ≤ `pts`; the first sample if `pts` precedes all). - pub fn sample_at_pts(&self, pts: i64) -> Option { - if self.pts_order.is_empty() { - return None; - } - let k = self.pts_order.partition_point(|&i| self.samples[i as usize].pts <= pts); - Some(self.pts_order[k.saturating_sub(1)] as usize) - } - /// Like [`sample_at_pts`](Self::sample_at_pts) but for edit-adjusted presentation time. - pub fn sample_at_presentation_time(&self, t: i64) -> Option { - self.sample_at_pts(t.saturating_sub(self.edit_offset)) - } - /// The closest sync sample at or before `index` (decode order); 0 if there is none. - pub fn sync_sample_before(&self, index: usize) -> usize { - let k = self.sync_index.partition_point(|&i| i as usize <= index); - if k == 0 { 0 } else { self.sync_index[k - 1] as usize } - } - /// Indices of all sync samples. - pub fn sync_samples(&self) -> &[u32] { - &self.sync_index - } - - fn finish_indexes(&mut self) { - self.sync_index = self.samples.iter().enumerate().filter(|(_, s)| s.is_sync).map(|(i, _)| i as u32).collect(); - let mut order: Vec = (0..self.samples.len() as u32).collect(); - order.sort_by_key(|&i| (self.samples[i as usize].pts, i)); - self.pts_order = order; - } -} - -/// Text metadata from `udta` (QuickTime `©xxx` atoms and iTunes-style `meta/ilst`, incl. `keys`). -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct Metadata { - pub entries: Vec<(String, String)>, -} - -impl Metadata { - pub fn get(&self, key: &str) -> Option<&str> { - self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str()) - } - /// Title from `©nam` or the `com.apple.quicktime.title` key. - pub fn title(&self) -> Option<&str> { - self.get("©nam").or_else(|| self.get("com.apple.quicktime.title")).or_else(|| self.get("title")) - } -} - -/// A parsed MP4/MOV file (headers and sample tables; media data stays in the source). -#[derive(Clone, Debug, PartialEq)] -pub struct Mp4File { - pub major_brand: FourCc, - pub minor_version: u32, - pub compatible_brands: Vec, - /// QuickTime file (major brand `qt ` or no `ftyp`). - pub is_quicktime: bool, - /// Movie timescale (`mvhd`). - pub timescale: u32, - /// Movie duration in the movie timescale (`mvhd`; for fragmented files, the longest track). - pub duration: u64, - pub tracks: Vec, - pub metadata: Metadata, - /// File uses movie fragments (`mvex` present). - pub fragmented: bool, - /// Byte offset of the `moov` box. - pub moov_offset: u64, - /// Number of `moof` boxes found. - pub fragment_count: usize, -} - -impl Mp4File { - /// Read the bytes of sample `index` of track `track` (index into [`tracks`](Self::tracks)). - pub fn read_sample(&self, src: &S, track: usize, index: usize) -> Result> { - let t = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?; - let s = t.samples.get(index).ok_or(Error::NoSuchSample(index))?; - read_range(src, s.offset, s.size as u64) - } - pub fn sample_at_pts(&self, track: usize, pts: i64) -> Option { - self.tracks.get(track)?.sample_at_pts(pts) - } - pub fn sync_sample_before(&self, track: usize, index: usize) -> Option { - Some(self.tracks.get(track)?.sync_sample_before(index)) - } - /// First track of the given kind. - pub fn track_of_kind(&self, kind: TrackKind) -> Option { - self.tracks.iter().position(|t| t.kind == kind) - } -} - -fn read_range(src: &S, offset: u64, size: u64) -> Result> { - let end = offset.checked_add(size).ok_or(Error::Truncated("sample"))?; - if end > src.len() { - return Err(Error::Truncated("sample data")); - } - let mut v = vec![0u8; size as usize]; - src.read_at(offset, &mut v)?; - Ok(v) -} - -/// Parse an MP4/MOV file. Only box headers, `ftyp`, `moov` and `moof` boxes are read. -pub fn open(src: S) -> Result { - let len = src.len(); - let mut offset = 0u64; - let mut ftyp: Option> = None; - let mut moov: Option<(u64, Vec)> = None; - let mut moofs: Vec<(u64, Vec)> = Vec::new(); - while offset + 8 <= len { - let n = (len - offset).min(32) as usize; - let mut hb = [0u8; 32]; - src.read_at(offset, &mut hb[..n])?; - let h = parse_box_header(&hb[..n])?; - let size = h.size.unwrap_or(len - offset); - let end = offset.saturating_add(size).min(len); - let body_len = end.saturating_sub(offset + h.header_len); - let read_body = |max: u64| -> Result> { - if body_len > max { - return Err(Error::TooLarge("header box")); - } - read_range(&src, offset + h.header_len, body_len) - }; - match &h.kind.0 { - b"ftyp" if ftyp.is_none() => ftyp = Some(read_body(1 << 16)?), - b"moov" if moov.is_none() => moov = Some((offset, read_body(MAX_HEADER_BOX)?)), - b"moof" => moofs.push((offset, read_body(MAX_HEADER_BOX)?)), - _ => {} - } - if h.size.is_none() { - break; - } - offset = offset.saturating_add(size); - } - let (moov_offset, moov) = moov.ok_or(Error::NoMoov)?; - let mut file = Mp4File { - major_brand: FourCc::default(), - minor_version: 0, - compatible_brands: Vec::new(), - is_quicktime: true, - timescale: 0, - duration: 0, - tracks: Vec::new(), - metadata: Metadata::default(), - fragmented: false, - moov_offset, - fragment_count: moofs.len(), - }; - if let Some(f) = &ftyp { - let mut c = Cur::new(f, "ftyp"); - file.major_brand = c.fourcc()?; - file.minor_version = c.u32().unwrap_or(0); - while c.remaining() >= 4 { - file.compatible_brands.push(c.fourcc()?); - } - file.is_quicktime = file.major_brand == b"qt "; - } - let mut trex: HashMap = HashMap::new(); - let mut trak_payloads = Vec::new(); - for b in boxes(&moov) { - let b = b?; - match &b.kind.0 { - b"cmov" => return Err(Error::Unsupported("compressed movie header (cmov)".into())), - b"mvhd" => { - let mut c = Cur::new(b.payload, "mvhd"); - let (v, _) = c.full_header()?; - if v == 1 { - c.skip(16)?; - file.timescale = c.u32()?; - file.duration = c.u64()?; - } else { - c.skip(8)?; - file.timescale = c.u32()?; - file.duration = c.u32()? as u64; - } - } - b"trak" => trak_payloads.push(b.payload), - b"mvex" => { - file.fragmented = true; - for m in boxes(b.payload).map_while(|b| b.ok()) { - if m.kind == b"trex" { - let mut c = Cur::new(m.payload, "trex"); - c.full_header()?; - let id = c.u32()?; - trex.insert(id, Trex { description_index: c.u32()?, duration: c.u32()?, size: c.u32()?, flags: c.u32()? }); - } - } - } - b"udta" => parse_udta(b.payload, &mut file.metadata), - b"meta" => parse_meta(b.payload, &mut file.metadata), - _ => {} - } - } - for p in trak_payloads { - let t = parse_trak(p, &file, len)?; - file.tracks.push(t); - } - // Movie fragments. - let mut next_dts: Vec = file.tracks.iter().map(|t| t.samples.last().map(|s| s.dts + s.duration as i64).unwrap_or(0)).collect(); - for (moof_off, moof) in &moofs { - parse_moof(*moof_off, moof, &mut file.tracks, &trex, &mut next_dts, len)?; - } - for t in &mut file.tracks { - if file.fragmented && !moofs.is_empty() { - let d: u64 = t.samples.iter().map(|s| s.duration as u64).sum(); - t.duration = t.duration.max(d); - if t.timescale > 0 && file.timescale > 0 { - let md = (t.duration as u128 * file.timescale as u128 / t.timescale as u128) as u64; - file.duration = file.duration.max(md); - } - } - t.finish_indexes(); - // Timecode start frame from the first sample. - if let Some(CodecConfig::Timecode(tc)) = t.entries.first_mut().map(|e| &mut e.codec) - && let Some(s) = t.samples.first() - && s.size >= 4 - { - let mut b = [0u8; 4]; - if src.read_at(s.offset, &mut b).is_ok() { - tc.start_frame = Some(u32::from_be_bytes(b)); - } - } - } - Ok(file) -} - -#[derive(Clone, Copy, Debug, Default)] -struct Trex { - description_index: u32, - duration: u32, - size: u32, - flags: u32, -} - -fn parse_language(code: u16) -> String { - if code < 0x400 { - // QuickTime Macintosh language code. - return if code == 0 { "eng".into() } else { "und".into() }; - } - let c = |s: u16| (((code >> s) & 0x1F) as u8 + 0x60) as char; - let s: String = [c(10), c(5), c(0)].into_iter().collect(); - if s.chars().all(|ch| ch.is_ascii_lowercase()) { s } else { "und".into() } -} - -fn rescale(v: i128, from: u32, to: u32) -> i64 { - if from == 0 { - return 0; - } - (v * to as i128 / from as i128).clamp(i64::MIN as i128, i64::MAX as i128) as i64 -} - -fn parse_trak(p: &[u8], file: &Mp4File, file_len: u64) -> Result { - let mut t = Track { - id: 0, - kind: TrackKind::Other, - handler: FourCc::default(), - handler_name: String::new(), - timescale: 0, - duration: 0, - tkhd_duration: 0, - language: "und".into(), - width: 0, - height: 0, - enabled: true, - alternate_group: 0, - volume: 0, - matrix: [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x4000_0000], - entries: Vec::new(), - samples: Vec::new(), - edits: Vec::new(), - edit_offset: 0, - references: Vec::new(), - sdtp: Vec::new(), - sample_groups: Vec::new(), - pcm_chunked: false, - sync_index: Vec::new(), - pts_order: Vec::new(), - }; - let mut mdia = None; - for b in boxes(p) { - let b = b?; - match &b.kind.0 { - b"tkhd" => { - let mut c = Cur::new(b.payload, "tkhd"); - let (v, flags) = c.full_header()?; - t.enabled = flags & 1 != 0; - if v == 1 { - c.skip(16)?; - t.id = c.u32()?; - c.skip(4)?; - t.tkhd_duration = c.u64()?; - } else { - c.skip(8)?; - t.id = c.u32()?; - c.skip(4)?; - t.tkhd_duration = c.u32()? as u64; - } - c.skip(8)?; - let _layer = c.i16()?; - t.alternate_group = c.u16()?; - t.volume = c.i16()?; - c.skip(2)?; - for m in t.matrix.iter_mut() { - *m = c.i32()?; - } - t.width = c.u32()? >> 16; - t.height = c.u32()? >> 16; - } - b"edts" => { - if let Some(e) = find(b.payload, b"elst") { - let mut c = Cur::new(e, "elst"); - let (v, _) = c.full_header()?; - let n = c.count(if v == 1 { 20 } else { 12 })?; - for _ in 0..n { - let (segment_duration, media_time) = if v == 1 { (c.u64()?, c.i64()?) } else { (c.u32()? as u64, c.i32()? as i64) }; - t.edits.push(Edit { segment_duration, media_time, media_rate: c.i32()? }); - } - } - } - b"tref" => { - for r in boxes(b.payload).map_while(|b| b.ok()) { - let ids = r.payload.as_chunks::<4>().0.iter().map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]])).collect(); - t.references.push((r.kind, ids)); - } - } - b"mdia" => mdia = Some(b.payload), - _ => {} - } - } - let mdia = mdia.ok_or_else(|| Error::Invalid("trak without mdia".into()))?; - let mut minf = None; - for b in boxes(mdia) { - let b = b?; - match &b.kind.0 { - b"mdhd" => { - let mut c = Cur::new(b.payload, "mdhd"); - let (v, _) = c.full_header()?; - if v == 1 { - c.skip(16)?; - t.timescale = c.u32()?; - t.duration = c.u64()?; - } else { - c.skip(8)?; - t.timescale = c.u32()?; - t.duration = c.u32()? as u64; - } - t.language = parse_language(c.u16()?); - } - b"hdlr" => { - let mut c = Cur::new(b.payload, "hdlr"); - c.full_header()?; - let _pre = c.u32()?; - t.handler = c.fourcc()?; - c.skip(12)?; - let name = c.rest(); - // ISO: null-terminated UTF-8; QuickTime: Pascal string. - let s = if !name.is_empty() && name[0] as usize == name.len() - 1 && name.len() > 1 { - &name[1..] - } else { - name.split(|&b| b == 0).next().unwrap_or(&[]) - }; - t.handler_name = String::from_utf8_lossy(s).into_owned(); - } - b"minf" => minf = Some(b.payload), - _ => {} - } - } - t.kind = match &t.handler.0 { - b"vide" => TrackKind::Video, - b"soun" => TrackKind::Audio, - b"tmcd" => TrackKind::Timecode, - b"text" | b"sbtl" | b"subt" | b"clcp" => TrackKind::Subtitle, - _ => TrackKind::Other, - }; - // Edit offset in media timescale. - let mut empty = 0i128; - for e in &t.edits { - if e.media_time == -1 { - empty += e.segment_duration as i128; - } else { - t.edit_offset = rescale(empty, file.timescale, t.timescale).saturating_sub(e.media_time); - break; - } - } - if let Some(stbl) = minf.and_then(|m| find(m, b"stbl")) { - parse_stbl(stbl, &mut t, file.is_quicktime, file_len)?; - } - Ok(t) -} - -struct SttsCursor<'a> { - runs: &'a [(u32, u32)], - run: usize, - used: u32, -} - -impl SttsCursor<'_> { - /// Advance by `n` samples, returning the summed duration. - fn take(&mut self, mut n: u64) -> u64 { - let mut total = 0u64; - while n > 0 && self.run < self.runs.len() { - let (count, delta) = self.runs[self.run]; - let avail = (count - self.used) as u64; - let k = avail.min(n); - total = total.saturating_add(k.saturating_mul(delta as u64)); - n -= k; - self.used += k as u32; - if self.used >= count { - self.run += 1; - self.used = 0; - } - } - total - } - fn next_value(&mut self) -> u32 { - while self.run < self.runs.len() && self.runs[self.run].0 == 0 { - self.run += 1; - } - let Some(&(_, d)) = self.runs.get(self.run) else { return 0 }; - self.take(1); - d - } -} - -fn parse_stbl(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64) -> Result<()> { - let mut stts: Vec<(u32, u32)> = Vec::new(); - let mut ctts: Vec<(u32, u32)> = Vec::new(); - let mut stss: Option> = None; - let mut stsc: Vec<(u32, u32, u32)> = Vec::new(); - let mut const_size = 0u32; - let mut sample_count = 0usize; - let mut sizes: Vec = Vec::new(); - let mut chunks: Vec = Vec::new(); - let mut sbgp: Vec<(FourCc, Vec<(u32, u32)>)> = Vec::new(); - let mut sgpd: HashMap>> = HashMap::new(); - for b in boxes(stbl) { - let b = b?; - let mut c = Cur::new(b.payload, "stbl child"); - match &b.kind.0 { - b"stsd" => { - c.full_header()?; - let n = c.count(8)?; - for e in boxes(c.rest()).take(n) { - let e = e?; - t.entries.push(parse_sample_entry(e.kind, e.payload, t.handler, is_qt)?); - } - } - b"stts" => { - c.full_header()?; - let n = c.count(8)?; - stts = (0..n).map(|_| Ok((c.u32()?, c.u32()?))).collect::>()?; - } - b"ctts" => { - c.full_header()?; - let n = c.count(8)?; - ctts = (0..n).map(|_| Ok((c.u32()?, c.u32()?))).collect::>()?; - } - b"stss" => { - c.full_header()?; - let n = c.count(4)?; - let mut v: Vec = (0..n).map(|_| c.u32()).collect::>()?; - v.sort_unstable(); - stss = Some(v); - } - b"stsc" => { - c.full_header()?; - let n = c.count(12)?; - stsc = (0..n).map(|_| Ok((c.u32()?, c.u32()?, c.u32()?))).collect::>()?; - } - b"stsz" => { - c.full_header()?; - const_size = c.u32()?; - let n = c.u32()? as usize; - if const_size == 0 { - c.check_count(n, 4)?; - sizes = (0..n).map(|_| c.u32()).collect::>()?; - sample_count = n; - } else { - // Samples of constant size must fit in the file: clamp absurd counts. - sample_count = (n as u64).min(file_len / const_size as u64) as usize; - } - } - b"stz2" => { - c.full_header()?; - let field = c.u32()? & 0xFF; - let n = c.u32()? as usize; - let bits = match field { - 4 | 8 | 16 => field as usize, - _ => return invalid(format!("stz2 field size {field}")), - }; - if n.checked_mul(bits).is_none_or(|b| b.div_ceil(8) > c.remaining()) { - return Err(Error::Truncated("stz2")); - } - let d = c.rest(); - sizes = (0..n) - .map(|i| match bits { - 4 => ((d[i / 2] >> if i % 2 == 0 { 4 } else { 0 }) & 0xF) as u32, - 8 => d[i] as u32, - _ => u16::from_be_bytes([d[2 * i], d[2 * i + 1]]) as u32, - }) - .collect(); - sample_count = n; - } - b"stco" => { - c.full_header()?; - let n = c.count(4)?; - chunks = (0..n).map(|_| c.u32().map(|v| v as u64)).collect::>()?; - } - b"co64" => { - c.full_header()?; - let n = c.count(8)?; - chunks = (0..n).map(|_| c.u64()).collect::>()?; - } - b"sdtp" => { - c.full_header()?; - t.sdtp = c.rest().to_vec(); - } - b"sbgp" => { - let (v, _) = c.full_header()?; - let gt = c.fourcc()?; - if v == 1 { - c.u32()?; - } - let n = c.count(8)?; - let runs = (0..n).map(|_| Ok((c.u32()?, c.u32()?))).collect::>()?; - sbgp.push((gt, runs)); - } - b"sgpd" => { - let (v, _) = c.full_header()?; - let gt = c.fourcc()?; - let default_len = if v >= 1 { c.u32()? } else { 0 }; - if v >= 2 { - c.u32()?; - } - let n = c.count(0)?; - let mut descs = Vec::new(); - for _ in 0..n { - let l = if v >= 1 && default_len == 0 { c.u32()? } else { default_len }; - if l == 0 { - // Version 0 without known length: keep the remainder as a single entry. - descs.push(c.rest().to_vec()); - break; - } - descs.push(c.bytes(l as usize)?.to_vec()); - } - sgpd.insert(gt, descs); - } - _ => {} - } - } - t.sample_groups = - sbgp.into_iter().map(|(gt, runs)| SampleGroup { grouping_type: gt, runs, descriptions: sgpd.remove(>).unwrap_or_default() }).collect(); - if sample_count == 0 || chunks.is_empty() || stsc.is_empty() { - return Ok(()); - } - if stsc[0].0 == 0 || stsc.windows(2).any(|w| w[1].0 <= w[0].0) { - return invalid("stsc first_chunk values are not increasing from 1"); - } - let pcm = match t.entries.first().map(|e| &e.codec) { - Some(CodecConfig::Pcm(p)) if const_size != 0 => Some(*p), - _ => None, - }; - let mut stts_c = SttsCursor { runs: &stts, run: 0, used: 0 }; - let mut ctts_c = SttsCursor { runs: &ctts, run: 0, used: 0 }; - let mut samples = Vec::new(); - let mut sidx = 0usize; - let mut dts = 0i64; - let mut stsc_i = 0usize; - if let Some(p) = pcm { - t.pcm_chunked = true; - let a = t.audio().cloned().unwrap_or_default(); - let bpf = p.bytes_per_frame().ok_or_else(|| Error::Invalid("PCM frame size overflows".into()))?; - let frame_bytes = |frames: u64| -> Option { - if a.qt_version == 1 && a.bytes_per_frame > 0 && a.samples_per_packet > 0 { - frames.checked_div(u64::from(a.samples_per_packet))?.checked_mul(u64::from(a.bytes_per_frame)) - } else if const_size > 1 { - frames.checked_mul(u64::from(const_size)) - } else { - frames.checked_mul(u64::from(bpf)) - } - }; - if chunks.len() > MAX_SAMPLES { - return Err(Error::TooLarge("chunk count")); - } - samples.reserve(chunks.len()); - for (ci, &off) in chunks.iter().enumerate() { - while stsc_i + 1 < stsc.len() && stsc[stsc_i + 1].0 as usize <= ci + 1 { - stsc_i += 1; - } - let spc = (stsc[stsc_i].1 as usize).min(sample_count - sidx); - if spc == 0 { - continue; - } - let dur = stts_c.take(spc as u64); - let size = frame_bytes(spc as u64).ok_or_else(|| Error::Invalid("PCM sample size overflows".into()))?.min(u64::from(u32::MAX)) as u32; - samples.push(Sample { - offset: off, - size, - dts, - pts: dts, - duration: dur.min(u32::MAX as u64) as u32, - is_sync: true, - description_index: stsc[stsc_i].2, - }); - dts = dts.saturating_add(dur.min(i64::MAX as u64) as i64); - sidx += spc; - if sidx >= sample_count { - break; - } - } - } else { - if sample_count > MAX_SAMPLES { - return Err(Error::TooLarge("sample count")); - } - samples.reserve(sample_count); - let mut stss_i = 0usize; - 'chunks: for (ci, &off) in chunks.iter().enumerate() { - while stsc_i + 1 < stsc.len() && stsc[stsc_i + 1].0 as usize <= ci + 1 { - stsc_i += 1; - } - let (_, spc, sdi) = stsc[stsc_i]; - let mut pos = off; - for _ in 0..spc { - if sidx >= sample_count { - break 'chunks; - } - let size = if const_size != 0 { const_size } else { sizes[sidx] }; - let duration = stts_c.next_value(); - let cts = ctts_c.next_value() as i32 as i64; - let is_sync = match &stss { - None => true, - Some(v) => { - while stss_i < v.len() && (v[stss_i] as usize) < sidx + 1 { - stss_i += 1; - } - stss_i < v.len() && v[stss_i] as usize == sidx + 1 - } - }; - samples.push(Sample { offset: pos, size, dts, pts: dts.saturating_add(cts), duration, is_sync, description_index: sdi }); - pos = pos.saturating_add(size as u64); - dts = dts.saturating_add(duration as i64); - sidx += 1; - } - } - } - t.samples = samples; - Ok(()) -} - -fn parse_moof(moof_off: u64, moof: &[u8], tracks: &mut [Track], trex: &HashMap, next_dts: &mut [i64], file_len: u64) -> Result<()> { - // End of data of the previous traf, for the base-offset default of subsequent trafs. - let mut prev_end: Option = None; - for b in boxes(moof) { - let b = b?; - if b.kind != b"traf" { - continue; - } - let mut tfhd = None; - let mut tfdt = None; - let mut truns = Vec::new(); - for c in boxes(b.payload) { - let c = c?; - match &c.kind.0 { - b"tfhd" => tfhd = Some(c.payload), - b"tfdt" => tfdt = Some(c.payload), - b"trun" => truns.push(c.payload), - _ => {} - } - } - let tfhd = tfhd.ok_or_else(|| Error::Invalid("traf without tfhd".into()))?; - let mut c = Cur::new(tfhd, "tfhd"); - let (_, fl) = c.full_header()?; - let id = c.u32()?; - let Some(ti) = tracks.iter().position(|t| t.id == id) else { continue }; - let d = trex.get(&id).copied().unwrap_or_default(); - let base = if fl & 1 != 0 { - c.u64()? - } else if fl & 0x20000 != 0 { - moof_off - } else { - prev_end.unwrap_or(moof_off) - }; - let sdi = if fl & 2 != 0 { c.u32()? } else { d.description_index }; - let def_dur = if fl & 8 != 0 { c.u32()? } else { d.duration }; - let def_size = if fl & 0x10 != 0 { c.u32()? } else { d.size }; - let def_flags = if fl & 0x20 != 0 { c.u32()? } else { d.flags }; - if let Some(p) = tfdt { - let mut c = Cur::new(p, "tfdt"); - let (v, _) = c.full_header()?; - let t = if v == 1 { c.u64()? } else { c.u32()? as u64 }; - next_dts[ti] = t.min(i64::MAX as u64) as i64; - } - let mut data_pos = base; - for tr in truns { - let mut c = Cur::new(tr, "trun"); - let (v, tf) = c.full_header()?; - let per = [0x100u32, 0x200, 0x400, 0x800].iter().filter(|&&f| tf & f != 0).count() * 4; - let n = c.u32()? as usize; - if per > 0 { - c.check_count(n, per)?; - } else if (n as u64).saturating_mul(def_size.max(1) as u64) > file_len { - return Err(Error::TooLarge("trun sample count")); - } - if tracks[ti].samples.len() + n > MAX_SAMPLES { - return Err(Error::TooLarge("sample count")); - } - if tf & 1 != 0 { - data_pos = base.wrapping_add_signed(c.i32()? as i64); - } - let first_flags = if tf & 4 != 0 { Some(c.u32()?) } else { None }; - let t = &mut tracks[ti]; - t.samples.reserve(n); - for i in 0..n { - let dur = if tf & 0x100 != 0 { c.u32()? } else { def_dur }; - let size = if tf & 0x200 != 0 { c.u32()? } else { def_size }; - let flags = if tf & 0x400 != 0 { c.u32()? } else { def_flags }; - // first_sample_flags overrides the flags of the first sample only. - let flags = if i == 0 { first_flags.unwrap_or(flags) } else { flags }; - let cto = if tf & 0x800 != 0 { - let raw = c.u32()?; - if v == 0 { raw as i64 } else { raw as i32 as i64 } - } else { - 0 - }; - let dts = next_dts[ti]; - t.samples.push(Sample { - offset: data_pos, - size, - dts, - pts: dts.saturating_add(cto), - duration: dur, - is_sync: flags & 0x1_0000 == 0, - description_index: sdi, - }); - data_pos = data_pos.saturating_add(size as u64); - next_dts[ti] = dts.saturating_add(dur as i64); - } - } - prev_end = Some(data_pos); - } - Ok(()) -} - -fn parse_udta(p: &[u8], md: &mut Metadata) { - for b in boxes(p).map_while(|b| b.ok()) { - if b.kind == b"meta" { - parse_meta(b.payload, md); - } else if b.kind.0[0] == 0xA9 && b.payload.len() >= 4 { - let n = u16::from_be_bytes([b.payload[0], b.payload[1]]) as usize; - if let Some(s) = b.payload.get(4..4 + n) { - md.entries.push((b.kind.to_string_lossy(), String::from_utf8_lossy(s).into_owned())); - } - } - } -} - -fn parse_meta(p: &[u8], md: &mut Metadata) { - // QuickTime `meta` is a plain box; ISO `meta` is a full box. - let body = if p.len() >= 8 && &p[4..8] == b"hdlr" { p } else { p.get(4..).unwrap_or(&[]) }; - let mut keys: Vec = Vec::new(); - if let Some(k) = find(body, b"keys") { - let mut c = Cur::new(k, "keys"); - if c.full_header().is_ok() - && let Ok(n) = c.count(8) - { - for _ in 0..n { - let Ok(sz) = c.u32() else { break }; - let Ok(_ns) = c.u32() else { break }; - let Ok(name) = c.bytes((sz as usize).saturating_sub(8)) else { break }; - keys.push(String::from_utf8_lossy(name).into_owned()); - } - } - } - let Some(ilst) = find(body, b"ilst") else { return }; - for item in boxes(ilst).map_while(|b| b.ok()) { - let idx = item.kind.as_u32() as usize; - let key = if !keys.is_empty() && idx >= 1 && idx <= keys.len() { keys[idx - 1].clone() } else { item.kind.to_string_lossy() }; - if let Some(d) = find(item.payload, b"data") - && d.len() >= 8 - { - let ty = u32::from_be_bytes([d[0], d[1], d[2], d[3]]) & 0xFF_FFFF; - if ty == 1 || ty == 0 { - md.entries.push((key, String::from_utf8_lossy(&d[8..]).into_owned())); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn language_codes() { - // "und" = 0x55C4 - assert_eq!(parse_language(0x55C4), "und"); - assert_eq!(parse_language(0), "eng"); - assert_eq!(parse_language(0x15C7), "eng"); - } - - #[test] - fn stts_cursor() { - let runs = [(2, 10), (0, 99), (3, 5)]; - let mut c = SttsCursor { runs: &runs, run: 0, used: 0 }; - assert_eq!(c.next_value(), 10); - assert_eq!(c.take(3), 10 + 5 + 5); - assert_eq!(c.next_value(), 5); - assert_eq!(c.next_value(), 0); - } - - #[test] - fn rejects_no_moov_and_cmov() { - let mut b = crate::bytes::BoxBuf::new(); - b.leaf(b"free", &[0; 4]); - assert!(matches!(open(b.buf.clone()), Err(Error::NoMoov))); - let m = b.start(b"moov"); - b.leaf(b"cmov", &[]); - b.end(m); - assert!(matches!(open(b.buf), Err(Error::Unsupported(_)))); - } - - #[test] - fn metadata_qt_and_ilst() { - let mut b = crate::bytes::BoxBuf::new(); - let m = b.start(&[0xA9, b'n', b'a', b'm']); - b.u16(2); - b.u16(0); - b.bytes(b"hi"); - b.end(m); - let meta = b.start_full(b"meta", 0, 0); - b.leaf(b"hdlr", &[0; 24]); - let il = b.start(b"ilst"); - let it = b.start(&[0xA9, b'A', b'R', b'T']); - let d = b.start(b"data"); - b.u32(1); - b.u32(0); - b.bytes(b"me"); - b.end(d); - b.end(it); - b.end(il); - b.end(meta); - let mut md = Metadata::default(); - parse_udta(&b.buf, &mut md); - assert_eq!(md.title(), Some("hi")); - assert_eq!(md.get("©ART"), Some("me")); - } - - use crate::bytes::BoxBuf; - - fn full(b: &mut BoxBuf, kind: &[u8; 4], v: u8, f: impl FnOnce(&mut BoxBuf)) { - let m = b.start_full(kind, v, 0); - f(b); - b.end(m); - } - - /// Hand-built file: uuid/wide/skip boxes, stz2 sizes, co64, ctts v1 with negative offsets, - /// stss, sdtp, sbgp/sgpd, and an edit list with a leading empty edit. - fn handmade(stsz_override: Option<(u32, u32)>) -> Vec { - let mut b = BoxBuf::new(); - let m = b.start(b"ftyp"); - b.bytes(b"isom"); - b.u32(0); - b.bytes(b"isom"); - b.end(m); - let m = b.start(b"uuid"); - b.bytes(&[0xAB; 16]); - b.bytes(b"payload"); - b.end(m); - b.leaf(b"wide", &[]); - b.leaf(b"skip", &[1, 2, 3]); - // mdat with 64-bit size: 4 samples of 3,1,2,4 bytes. - let data_start = b.len() as u64 + 16; - b.u32(1); - b.bytes(b"mdat"); - b.u64(16 + 10); - b.bytes(&[1, 1, 1, 2, 3, 3, 4, 4, 4, 4]); - let moov = b.start(b"moov"); - full(&mut b, b"mvhd", 0, |b| { - b.u32(0); - b.u32(0); - b.u32(1000); - b.u32(500); - b.zeros(80); - }); - let trak = b.start(b"trak"); - full(&mut b, b"tkhd", 0, |b| { - b.u32(0); - b.u32(0); - b.u32(7); - b.u32(0); - b.u32(500); - b.zeros(52); - b.u32(320 << 16); - b.u32(240 << 16); - }); - let edts = b.start(b"edts"); - full(&mut b, b"elst", 1, |b| { - b.u32(2); - b.u64(100); - b.i64(-1); - b.i32(0x10000); - b.u64(400); - b.i64(20); - b.i32(0x10000); - }); - b.end(edts); - let mdia = b.start(b"mdia"); - full(&mut b, b"mdhd", 0, |b| { - b.u32(0); - b.u32(0); - b.u32(100); - b.u32(40); - b.u16(0x15C7); - b.u16(0); - }); - full(&mut b, b"hdlr", 0, |b| { - b.u32(0); - b.bytes(b"vide"); - b.zeros(12); - b.bytes(b"Video\0"); - }); - let minf = b.start(b"minf"); - let stbl = b.start(b"stbl"); - full(&mut b, b"stsd", 0, |b| { - b.u32(1); - crate::codec::SampleEntry::jpeg(320, 240).write(b, false).unwrap(); - }); - full(&mut b, b"stts", 0, |b| { - b.u32(1); - b.u32(4); - b.u32(10); - }); - full(&mut b, b"ctts", 1, |b| { - b.u32(2); - b.u32(2); - b.i32(-5); - b.u32(2); - b.i32(5); - }); - full(&mut b, b"stss", 0, |b| { - b.u32(2); - b.u32(3); - b.u32(1); - }); - full(&mut b, b"stsc", 0, |b| { - b.u32(2); - b.u32(1); - b.u32(3); - b.u32(1); - b.u32(2); - b.u32(1); - b.u32(1); - }); - match stsz_override { - Some((size, count)) => full(&mut b, b"stsz", 0, |b| { - b.u32(size); - b.u32(count); - }), - None => full(&mut b, b"stz2", 0, |b| { - b.u32(4); - b.u32(4); - b.u8(0x31); - b.u8(0x24); - }), - } - full(&mut b, b"co64", 0, |b| { - b.u32(2); - b.u64(data_start); - b.u64(data_start + 6); - }); - full(&mut b, b"sdtp", 0, |b| b.bytes(&[0x20, 0x10, 0x10, 0x10])); - full(&mut b, b"sbgp", 0, |b| { - b.bytes(b"roll"); - b.u32(1); - b.u32(4); - b.u32(1); - }); - full(&mut b, b"sgpd", 1, |b| { - b.bytes(b"roll"); - b.u32(2); - b.u32(1); - b.i16(-1); - }); - b.end(stbl); - b.end(minf); - b.end(mdia); - b.end(trak); - b.end(moov); - // Trailing box extending to EOF (size 0). - b.u32(0); - b.bytes(b"free"); - b.bytes(&[9; 5]); - b.buf - } - - #[test] - fn handmade_sample_table() { - let d = handmade(None); - let f = open(d.as_slice()).unwrap(); - assert_eq!(f.major_brand, *b"isom"); - let t = &f.tracks[0]; - assert_eq!((t.id, t.kind, t.timescale, t.language.as_str()), (7, TrackKind::Video, 100, "eng")); - assert_eq!((t.width, t.height), (320, 240)); - assert_eq!(t.handler_name, "Video"); - let sizes: Vec = t.samples.iter().map(|s| s.size).collect(); - assert_eq!(sizes, [3, 1, 2, 4]); - let offs: Vec = t.samples.iter().map(|s| s.offset - t.samples[0].offset).collect(); - assert_eq!(offs, [0, 3, 4, 6]); - let pts: Vec = t.samples.iter().map(|s| s.pts).collect(); - assert_eq!(pts, [-5, 5, 25, 35]); - let sync: Vec = t.samples.iter().map(|s| s.is_sync).collect(); - assert_eq!(sync, [true, false, true, false]); - for (i, want) in [vec![1u8, 1, 1], vec![2], vec![3, 3], vec![4; 4]].iter().enumerate() { - assert_eq!(&f.read_sample(d.as_slice(), 0, i).unwrap(), want); - } - // Empty edit of 100 movie ticks = 10 media ticks, then media starts at 20. - assert_eq!(t.edits.len(), 2); - assert_eq!(t.edit_offset, 10 - 20); - assert_eq!(t.presentation_pts(2), Some(15)); - assert_eq!(t.sdtp.len(), 4); - assert_eq!(t.sample_groups[0].grouping_type, *b"roll"); - assert_eq!(t.sample_groups[0].descriptions, vec![vec![0xFF, 0xFF]]); - assert!(f.read_sample(d.as_slice(), 0, 4).is_err()); - assert!(f.read_sample(d.as_slice(), 1, 0).is_err()); - } - - #[test] - fn absurd_counts_are_bounded() { - // Constant-size stsz claiming 4 billion samples is clamped to what fits in the file. - let d = handmade(Some((1, u32::MAX))); - let f = open(d.as_slice()).unwrap(); - assert!(f.tracks[0].samples.len() <= d.len()); - // A table claiming more entries than its box holds fails cleanly. - let mut d = handmade(None); - let p = d.windows(4).position(|w| w == b"stts").unwrap(); - d[p + 8..p + 12].copy_from_slice(&u32::MAX.to_be_bytes()); - assert!(matches!(open(d.as_slice()), Err(Error::Truncated(_)))); - // Box larger than its parent. - let mut d = handmade(None); - let p = d.windows(4).position(|w| w == b"stsc").unwrap(); - d[p - 4..p].copy_from_slice(&0x7FFF_FFFFu32.to_be_bytes()); - assert!(open(d.as_slice()).is_err()); - } -} diff --git a/crates/isobmff/src/error.rs b/crates/isobmff/src/error.rs deleted file mode 100644 index b5dfd752..00000000 --- a/crates/isobmff/src/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::io; - -/// Errors produced by the demuxer and muxer. -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("I/O error: {0}")] - Io(#[from] io::Error), - /// A box or structure ended before all required fields were read. - #[error("truncated data while reading {0}")] - Truncated(&'static str), - /// Structurally invalid data. - #[error("invalid data: {0}")] - Invalid(String), - /// Valid but unsupported feature (e.g. compressed `cmov` movie header). - #[error("unsupported: {0}")] - Unsupported(String), - /// A count or size exceeds sanity limits relative to the file size. - #[error("size limit exceeded: {0}")] - TooLarge(&'static str), - /// No `moov` box was found. - #[error("no moov box found")] - NoMoov, - /// A track index was out of range. - #[error("track index {0} out of range")] - NoSuchTrack(usize), - /// A sample index was out of range. - #[error("sample index {0} out of range")] - NoSuchSample(usize), - /// Muxer usage error (e.g. invalid track config, bad sample). - #[error("muxer: {0}")] - Mux(String), -} - -pub type Result = std::result::Result; - -pub(crate) fn invalid(msg: impl Into) -> Result { - Err(Error::Invalid(msg.into())) -} diff --git a/crates/isobmff/src/lib.rs b/crates/isobmff/src/lib.rs index b34a158b..d51006b9 100644 --- a/crates/isobmff/src/lib.rs +++ b/crates/isobmff/src/lib.rs @@ -1,28 +1,6 @@ -//! Clean-room ISO Base Media File Format (MP4, ISO/IEC 14496-12/14/15) and QuickTime (MOV) support. -//! -//! - **Demux:** [`open`] parses box headers plus `moov` (and `moof` for fragmented files) from a -//! [`ByteSource`] and builds per-track [`Sample`] tables; media data is read on demand with -//! [`Mp4File::read_sample`]. -//! - **Mux:** [`Mp4Writer`] writes progressive MP4/MOV (optionally faststart); -//! [`FragmentedWriter`] writes fragmented MP4. -//! -//! See the crate README for supported boxes and limitations. +//! ISO BMFF (MP4) and QuickTime (MOV) demuxer and muxer: a re-export of [`craft_media::isobmff`] +//! from craft-libs. Supported boxes and limitations are in the craft-media README. #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))] -mod bytes; -mod codec; -mod demux; -mod error; -mod mux; -mod source; - -pub use bytes::FourCc; -pub use codec::{ - AacConfig, AudioParams, Av1Config, AvcConfig, BitRate, CleanAperture, CodecConfig, ColorInfo, FieldInfo, FlacConfig, HevcConfig, HevcNalArray, - MasteringDisplay, OpusConfig, PcmConfig, SampleEntry, TimecodeConfig, VideoParams, VpcConfig, parse_asc, -}; -pub use demux::{Edit, Metadata, Mp4File, Sample, SampleGroup, Track, TrackKind, open}; -pub use error::{Error, Result}; -pub use mux::{Brand, FragmentedWriter, Mp4Writer, TrackConfig, WriteSample, WriterOptions}; -pub use source::ByteSource; +pub use craft_media::isobmff::*; diff --git a/crates/isobmff/src/mux.rs b/crates/isobmff/src/mux.rs deleted file mode 100644 index bf9a4cbf..00000000 --- a/crates/isobmff/src/mux.rs +++ /dev/null @@ -1,969 +0,0 @@ -//! Muxers: progressive MP4/MOV ([`Mp4Writer`], optional faststart) and fragmented MP4 ([`FragmentedWriter`]). - -use crate::bytes::BoxBuf; -use crate::codec::{CodecConfig, SampleEntry, TimecodeConfig}; -use crate::demux::Edit; -use crate::error::{Error, Result}; -use std::io::{Read, Seek, SeekFrom, Write}; - -/// Output flavour: determines brands and some sample-entry encodings. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Brand { - /// ISO MP4: `isom` major brand, compatible `isom iso2 avc1 mp41 mp42`. - Mp4, - /// QuickTime MOV: `qt ` major brand. - Mov, -} - -/// Movie-level writer options. -#[derive(Clone, Debug)] -pub struct WriterOptions { - pub brand: Brand, - /// Movie timescale (`mvhd`); default 1000. - pub movie_timescale: u32, - /// Text metadata written to `moov/udta` (keys like `©nam`, `©ART`, `©cmt`, `©too`). - pub metadata: Vec<(String, String)>, -} - -impl WriterOptions { - pub fn new(brand: Brand) -> Self { - WriterOptions { brand, movie_timescale: 1000, metadata: Vec::new() } - } -} - -/// Per-track configuration. -#[derive(Clone, Debug)] -pub struct TrackConfig { - pub entry: SampleEntry, - /// Media timescale. For PCM it must equal the sample rate (one tick per audio frame). - pub timescale: u32, - /// ISO 639-2/T language (`und` by default). - pub language: String, - /// Explicit edit list (segment durations in the movie timescale). Takes precedence over `media_start`. - pub edits: Vec, - /// Convenience: create one edit starting at this media time (e.g. AAC encoder delay / priming, - /// or the first pts of a B-frame stream) spanning the rest of the media. - pub media_start: Option, - /// `hdlr` name; a sensible default is used when `None`. - pub handler_name: Option, -} - -impl TrackConfig { - pub fn new(entry: SampleEntry, timescale: u32) -> Self { - TrackConfig { entry, timescale, language: "und".into(), edits: Vec::new(), media_start: None, handler_name: None } - } -} - -/// A sample handed to the muxers. -#[derive(Clone, Copy, Debug)] -pub struct WriteSample<'a> { - pub data: &'a [u8], - /// Duration in the media timescale. Ignored for PCM (each frame lasts one tick). - pub duration: u32, - /// pts − dts in the media timescale. - pub composition_offset: i32, - pub is_sync: bool, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum Handler { - Video, - Audio, - Timecode, - Other, -} - -fn handler_of(e: &SampleEntry) -> Handler { - if matches!(e.codec, CodecConfig::Timecode(_)) { - Handler::Timecode - } else if e.video.is_some() { - Handler::Video - } else if e.audio.is_some() { - Handler::Audio - } else { - Handler::Other - } -} - -/// Accumulated sample tables for one track. -#[derive(Clone, Debug, Default)] -struct Tables { - stts: Vec<(u32, u32)>, - ctts: Vec<(u32, i32)>, - sizes: Vec, - /// Constant sample size (PCM) — then `sizes` is unused. - const_size: Option, - sample_count: u64, - /// 1-based sync sample numbers. - sync: Vec, - all_sync: bool, - /// (offset relative to file start before shifting, sample count) - chunks: Vec<(u64, u32)>, - media_duration: u64, - /// End of the last sample on the composition timeline (decode time + composition offset + - /// duration, maximum over samples). - comp_end: u64, -} - -impl Tables { - fn new() -> Self { - Tables { all_sync: true, ..Default::default() } - } - fn push_run(runs: &mut Vec<(u32, T)>, count: u32, v: T) { - if let Some(last) = runs.last_mut() - && last.1 == v - && last.0.checked_add(count).is_some() - { - last.0 += count; - return; - } - runs.push((count, v)); - } -} - -struct WTrack { - cfg: TrackConfig, - id: u32, - handler: Handler, - pcm_frame_bytes: Option, - t: Tables, - /// Track id of a linked timecode track (`tref/tmcd`). - tmcd_ref: Option, - /// For timecode tracks: (video track index, start frame). - tmcd_source: Option<(usize, u32)>, -} - -fn pack_language(lang: &str) -> u16 { - let b = lang.as_bytes(); - if b.len() == 3 && b.iter().all(|c| c.is_ascii_lowercase()) { - (((b[0] - 0x60) as u16) << 10) | (((b[1] - 0x60) as u16) << 5) | ((b[2] - 0x60) as u16) - } else { - 0x55C4 // "und" - } -} - -fn rescale(v: u64, from: u32, to: u32) -> u64 { - if from == 0 { - return 0; - } - (v as u128 * to as u128 / from as u128).min(u64::MAX as u128) as u64 -} - -const MATRIX: [i32; 9] = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x4000_0000]; - -fn write_ftyp(b: &mut BoxBuf, brand: Brand) { - let m = b.start(b"ftyp"); - match brand { - Brand::Mp4 => { - b.bytes(b"isom"); - b.u32(0x200); - for c in [b"isom", b"iso2", b"avc1", b"mp41", b"mp42"] { - b.bytes(c); - } - } - Brand::Mov => { - b.bytes(b"qt "); - b.u32(0x200); - b.bytes(b"qt "); - } - } - b.end(m); -} - -fn movie_duration(tracks: &[WTrack], movie_ts: u32) -> u64 { - tracks.iter().map(|t| presentation_duration(t, movie_ts)).max().unwrap_or(0) -} - -fn effective_edits(t: &WTrack, movie_ts: u32) -> Vec { - if !t.cfg.edits.is_empty() { - return t.cfg.edits.clone(); - } - if let Some(start) = t.cfg.media_start { - // presentation runs from `start` to the end of the last sample in composition order (with - // B-frame reordering that is past the end of the decode timeline) - let rest = t.t.comp_end.max(t.t.media_duration).saturating_sub(start.max(0) as u64); - return vec![Edit { segment_duration: rescale(rest, t.cfg.timescale, movie_ts), media_time: start, media_rate: 0x10000 }]; - } - Vec::new() -} - -fn presentation_duration(t: &WTrack, movie_ts: u32) -> u64 { - let e = effective_edits(t, movie_ts); - if e.is_empty() { rescale(t.t.media_duration, t.cfg.timescale, movie_ts) } else { e.iter().map(|e| e.segment_duration).sum() } -} - -fn write_mvhd(b: &mut BoxBuf, timescale: u32, duration: u64, next_id: u32) { - let v1 = duration > u32::MAX as u64; - let m = b.start_full(b"mvhd", v1 as u8, 0); - if v1 { - b.u64(0); - b.u64(0); - b.u32(timescale); - b.u64(duration); - } else { - b.u32(0); - b.u32(0); - b.u32(timescale); - b.u32(duration as u32); - } - b.u32(0x10000); - b.u16(0x100); - b.zeros(10); - for v in MATRIX { - b.i32(v); - } - b.zeros(24); - b.u32(next_id); - b.end(m); -} - -fn write_trak(b: &mut BoxBuf, t: &WTrack, movie_ts: u32, qt: bool, shift: u64, force_co64: bool) -> Result<()> { - let trak = b.start(b"trak"); - let dur = presentation_duration(t, movie_ts); - let v1 = dur > u32::MAX as u64; - let m = b.start_full(b"tkhd", v1 as u8, 3); - if v1 { - b.u64(0); - b.u64(0); - b.u32(t.id); - b.u32(0); - b.u64(dur); - } else { - b.u32(0); - b.u32(0); - b.u32(t.id); - b.u32(0); - b.u32(dur as u32); - } - b.zeros(8); - b.i16(0); - b.u16(0); - b.i16(if t.handler == Handler::Audio { 0x100 } else { 0 }); - b.u16(0); - for v in MATRIX { - b.i32(v); - } - let (w, h) = match &t.cfg.entry.video { - Some(v) => { - let w = match v.pixel_aspect { - Some((hs, vs)) if hs > 0 && vs > 0 => (v.width as u64 * hs as u64 / vs as u64) as u32, - _ => v.width as u32, - }; - (w, v.height as u32) - } - None => (0, 0), - }; - b.u32(w << 16); - b.u32(h << 16); - b.end(m); - let edits = effective_edits(t, movie_ts); - if !edits.is_empty() { - let e = b.start(b"edts"); - let v1 = edits.iter().any(|e| e.segment_duration > u32::MAX as u64 || e.media_time > i32::MAX as i64 || e.media_time < i32::MIN as i64); - let l = b.start_full(b"elst", v1 as u8, 0); - b.u32(edits.len() as u32); - for ed in &edits { - if v1 { - b.u64(ed.segment_duration); - b.i64(ed.media_time); - } else { - b.u32(ed.segment_duration as u32); - b.i32(ed.media_time as i32); - } - b.i32(ed.media_rate); - } - b.end(l); - b.end(e); - } - if let Some(id) = t.tmcd_ref { - let r = b.start(b"tref"); - let m = b.start(b"tmcd"); - b.u32(id); - b.end(m); - b.end(r); - } - let mdia = b.start(b"mdia"); - let mv1 = t.t.media_duration > u32::MAX as u64; - let m = b.start_full(b"mdhd", mv1 as u8, 0); - if mv1 { - b.u64(0); - b.u64(0); - b.u32(t.cfg.timescale); - b.u64(t.t.media_duration); - } else { - b.u32(0); - b.u32(0); - b.u32(t.cfg.timescale); - b.u32(t.t.media_duration as u32); - } - b.u16(pack_language(&t.cfg.language)); - b.u16(0); - b.end(m); - let (htype, default_name) = match t.handler { - Handler::Video => (b"vide", "VideoHandler"), - Handler::Audio => (b"soun", "SoundHandler"), - Handler::Timecode => (b"tmcd", "TimeCodeHandler"), - Handler::Other => (b"meta", "MetaHandler"), - }; - let name = t.cfg.handler_name.clone().unwrap_or_else(|| default_name.into()); - let m = b.start_full(b"hdlr", 0, 0); - b.bytes(if qt { b"mhlr" } else { &[0, 0, 0, 0] }); - b.bytes(htype); - b.zeros(12); - if qt { - let n = name.len().min(255); - b.u8(n as u8); - b.bytes(&name.as_bytes()[..n]); - } else { - b.bytes(name.as_bytes()); - b.u8(0); - } - b.end(m); - let minf = b.start(b"minf"); - match t.handler { - Handler::Video => { - let m = b.start_full(b"vmhd", 0, 1); - b.zeros(8); - b.end(m); - } - Handler::Audio => { - let m = b.start_full(b"smhd", 0, 0); - b.zeros(4); - b.end(m); - } - Handler::Timecode => { - let g = b.start(b"gmhd"); - let m = b.start_full(b"gmin", 0, 0); - b.u16(0x40); - b.u16(0x8000); - b.u16(0x8000); - b.u16(0x8000); - b.i16(0); - b.u16(0); - b.end(m); - let tm = b.start(b"tmcd"); - let m = b.start_full(b"tcmi", 0, 0); - b.u16(0); - b.u16(0); - b.u16(12); - b.u16(0); - b.bytes(&[0xFF; 6]); - b.zeros(6); - b.u8(0); - b.end(m); - b.end(tm); - b.end(g); - } - Handler::Other => { - let m = b.start_full(b"nmhd", 0, 0); - b.end(m); - } - } - if qt { - // QuickTime data handler reference. - let m = b.start_full(b"hdlr", 0, 0); - b.bytes(b"dhlr"); - b.bytes(b"url "); - b.zeros(12); - b.u8(0); - b.end(m); - } - let d = b.start(b"dinf"); - let r = b.start_full(b"dref", 0, 0); - b.u32(1); - let u = b.start_full(b"url ", 0, 1); - b.end(u); - b.end(r); - b.end(d); - let stbl = b.start(b"stbl"); - let m = b.start_full(b"stsd", 0, 0); - b.u32(1); - t.cfg.entry.write(b, qt)?; - b.end(m); - let tb = &t.t; - let m = b.start_full(b"stts", 0, 0); - b.u32(tb.stts.len() as u32); - for &(c, d) in &tb.stts { - b.u32(c); - b.u32(d); - } - b.end(m); - if tb.ctts.iter().any(|&(_, o)| o != 0) { - let neg = tb.ctts.iter().any(|&(_, o)| o < 0); - let m = b.start_full(b"ctts", neg as u8, 0); - b.u32(tb.ctts.len() as u32); - for &(c, o) in &tb.ctts { - b.u32(c); - b.i32(o); - } - b.end(m); - } - if !tb.all_sync { - let m = b.start_full(b"stss", 0, 0); - b.u32(tb.sync.len() as u32); - for &s in &tb.sync { - b.u32(s); - } - b.end(m); - } - let m = b.start_full(b"stsc", 0, 0); - let mut runs: Vec<(u32, u32)> = Vec::new(); - for (i, &(_, n)) in tb.chunks.iter().enumerate() { - if runs.last().is_none_or(|r| r.1 != n) { - runs.push((i as u32 + 1, n)); - } - } - b.u32(runs.len() as u32); - for (first, n) in runs { - b.u32(first); - b.u32(n); - b.u32(1); - } - b.end(m); - let m = b.start_full(b"stsz", 0, 0); - match tb.const_size { - Some(s) => { - b.u32(s); - b.u32(tb.sample_count.min(u32::MAX as u64) as u32); - } - None => { - let first = tb.sizes.first().copied().unwrap_or(0); - // A uniform size of 0 would mean "table follows" (ISO/IEC 14496-12 §8.7.3), so - // all-empty samples must still be written as a table. - if first != 0 && !tb.sizes.is_empty() && tb.sizes.iter().all(|&s| s == first) { - b.u32(first); - b.u32(tb.sizes.len() as u32); - } else { - b.u32(0); - b.u32(tb.sizes.len() as u32); - for &s in &tb.sizes { - b.u32(s); - } - } - } - } - b.end(m); - let co64 = force_co64 || tb.chunks.iter().any(|&(o, _)| o + shift > u32::MAX as u64); - let m = b.start_full(if co64 { b"co64" } else { b"stco" }, 0, 0); - b.u32(tb.chunks.len() as u32); - for &(o, _) in &tb.chunks { - if co64 { - b.u64(o + shift); - } else { - b.u32((o + shift) as u32); - } - } - b.end(m); - b.end(stbl); - b.end(minf); - b.end(mdia); - b.end(trak); - Ok(()) -} - -fn write_udta(b: &mut BoxBuf, md: &[(String, String)]) { - let items: Vec<([u8; 4], &str)> = md - .iter() - .filter_map(|(k, v)| { - let mut chars = k.chars(); - if chars.next()? != '©' { - return None; - } - let rest: Vec = chars.map(|c| c as u32 as u8).collect(); - (rest.len() == 3).then(|| ([0xA9, rest[0], rest[1], rest[2]], v.as_str())) - }) - .collect(); - if items.is_empty() { - return; - } - let u = b.start(b"udta"); - for (k, v) in items { - let m = b.start(&k); - b.u16(v.len() as u16); - b.u16(0x55C4); - b.bytes(v.as_bytes()); - b.end(m); - } - b.end(u); -} - -fn validate_track(cfg: &TrackConfig) -> Result> { - if cfg.timescale == 0 { - return Err(Error::Mux("timescale must be non-zero".into())); - } - if let CodecConfig::Pcm(p) = &cfg.entry.codec { - if p.bits == 0 || p.channels == 0 { - return Err(Error::Mux("PCM needs bits and channels".into())); - } - if (p.sample_rate - cfg.timescale as f64).abs() > 0.5 { - return Err(Error::Mux("PCM track timescale must equal the sample rate".into())); - } - let Some(n) = p.bytes_per_frame() else { - return Err(Error::Mux("PCM frame size overflows".into())); - }; - return Ok(Some(n)); - } - Ok(None) -} - -/// Progressive MP4/MOV writer: `ftyp`, then one `mdat` streamed to `W`, then `moov` at [`finish`](Self::finish) -/// (or moved in front of `mdat` by [`finish_faststart`](Self::finish_faststart)). -pub struct Mp4Writer { - w: W, - opts: WriterOptions, - tracks: Vec, - /// Offset of the placeholder box (8 bytes) preceding the mdat header. - pad_pos: u64, - mdat_pos: u64, - pos: u64, - last_track: Option, -} - -impl Mp4Writer { - /// Start a file: writes `ftyp`, a placeholder box and the `mdat` header. - pub fn new(mut w: W, opts: WriterOptions) -> Result { - let start = w.stream_position()?; - let mut b = BoxBuf::new(); - write_ftyp(&mut b, opts.brand); - let pad_pos = start + b.len() as u64; - b.leaf(if opts.brand == Brand::Mov { b"wide" } else { b"free" }, &[]); - let mdat_pos = start + b.len() as u64; - b.leaf(b"mdat", &[]); - w.write_all(&b.buf)?; - let pos = start + b.len() as u64; - Ok(Mp4Writer { w, opts, tracks: Vec::new(), pad_pos, mdat_pos, pos, last_track: None }) - } - - /// Add a track; returns its index (track ids are index + 1). - pub fn add_track(&mut self, cfg: TrackConfig) -> Result { - let pcm = validate_track(&cfg)?; - let handler = handler_of(&cfg.entry); - let mut t = Tables::new(); - t.const_size = pcm; - self.tracks.push(WTrack { id: self.tracks.len() as u32 + 1, handler, pcm_frame_bytes: pcm, t, cfg, tmcd_ref: None, tmcd_source: None }); - Ok(self.tracks.len() - 1) - } - - /// Add a QuickTime timecode track referenced by `video_track`. Its single sample (the start - /// frame number) is written at finish, spanning the video track's duration. - pub fn add_timecode_track(&mut self, video_track: usize, tc: TimecodeConfig, start_frame: u32) -> Result { - if video_track >= self.tracks.len() { - return Err(Error::NoSuchTrack(video_track)); - } - if tc.timescale == 0 || tc.frame_duration == 0 || tc.frames_per_second == 0 { - return Err(Error::Mux("timecode needs timescale, frame duration and fps".into())); - } - let entry = SampleEntry { - format: crate::bytes::FourCc(*b"tmcd"), - data_reference_index: 1, - codec: CodecConfig::Timecode(tc.clone()), - video: None, - audio: None, - bitrate: None, - }; - let idx = self.add_track(TrackConfig::new(entry, tc.timescale))?; - self.tracks[idx].tmcd_source = Some((video_track, start_frame)); - let id = self.tracks[idx].id; - self.tracks[video_track].tmcd_ref = Some(id); - Ok(idx) - } - - /// Append one sample to track `track` (decode order). For PCM tracks `data` may hold any whole - /// number of audio frames. - pub fn write_sample(&mut self, track: usize, s: WriteSample) -> Result<()> { - let tr = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?; - if tr.tmcd_source.is_some() { - return Err(Error::Mux("timecode track samples are written automatically".into())); - } - let (count, dur) = match tr.pcm_frame_bytes { - Some(fb) => { - if !s.data.len().is_multiple_of(fb as usize) { - return Err(Error::Mux("PCM data is not a whole number of frames".into())); - } - ((s.data.len() / fb as usize) as u32, 1u32) - } - None => { - if s.data.len() > u32::MAX as usize { - return Err(Error::Mux("sample larger than 4 GiB".into())); - } - (1, s.duration) - } - }; - if count == 0 { - return Ok(()); - } - self.w.write_all(s.data)?; - let offset = self.pos; - self.pos += s.data.len() as u64; - let contiguous = self.last_track == Some(track); - self.last_track = Some(track); - let tr = &mut self.tracks[track]; - let t = &mut tr.t; - Tables::push_run(&mut t.stts, count, dur); - Tables::push_run(&mut t.ctts, count, if tr.pcm_frame_bytes.is_some() { 0 } else { s.composition_offset }); - if tr.pcm_frame_bytes.is_none() { - t.sizes.push(s.data.len() as u32); - if s.is_sync { - t.sync.push(t.sample_count as u32 + 1); - } else { - t.all_sync = false; - } - } - t.sample_count += count as u64; - let ct = if tr.pcm_frame_bytes.is_some() { 0 } else { s.composition_offset as i64 }; - t.comp_end = t.comp_end.max((t.media_duration as i64 + ct + count as i64 * dur as i64).max(0) as u64); - t.media_duration += count as u64 * dur as u64; - match t.chunks.last_mut() { - Some(c) if contiguous => c.1 += count, - _ => t.chunks.push((offset, count)), - } - Ok(()) - } - - fn finish_mdat(&mut self) -> Result<()> { - // Timecode samples. - for i in 0..self.tracks.len() { - let Some((vi, start)) = self.tracks[i].tmcd_source else { continue }; - let v = &self.tracks[vi]; - let vd = v.t.media_duration; - let vts = v.cfg.timescale; - let tts = self.tracks[i].cfg.timescale; - let tc_dur = match &self.tracks[i].cfg.entry.codec { - CodecConfig::Timecode(tc) => tc.frame_duration, - _ => 1, - }; - let d = rescale(vd, vts, tts).max(tc_dur as u64).min(u32::MAX as u64) as u32; - self.w.write_all(&start.to_be_bytes())?; - let off = self.pos; - self.pos += 4; - self.last_track = None; - let t = &mut self.tracks[i].t; - t.stts.push((1, d)); - t.sizes.push(4); - t.sync.push(1); - t.sample_count = 1; - t.media_duration = d as u64; - t.chunks.push((off, 1)); - } - let size = self.pos - self.mdat_pos; - if size <= u32::MAX as u64 { - self.w.seek(SeekFrom::Start(self.mdat_pos))?; - self.w.write_all(&(size as u32).to_be_bytes())?; - } else { - // Merge the placeholder into a 16-byte large-size mdat header. - let mut h = [0u8; 16]; - h[..4].copy_from_slice(&1u32.to_be_bytes()); - h[4..8].copy_from_slice(b"mdat"); - h[8..].copy_from_slice(&(self.pos - self.pad_pos).to_be_bytes()); - self.w.seek(SeekFrom::Start(self.pad_pos))?; - self.w.write_all(&h)?; - } - self.w.seek(SeekFrom::Start(self.pos))?; - Ok(()) - } - - fn build_moov(&self, shift: u64, force_co64: bool) -> Result> { - let qt = self.opts.brand == Brand::Mov; - let ts = self.opts.movie_timescale.max(1); - let mut b = BoxBuf::new(); - let m = b.start(b"moov"); - write_mvhd(&mut b, ts, movie_duration(&self.tracks, ts), self.tracks.len() as u32 + 1); - for t in &self.tracks { - write_trak(&mut b, t, ts, qt, shift, force_co64)?; - } - write_udta(&mut b, &self.opts.metadata); - b.end(m); - Ok(b.buf) - } - - /// Finish the file with `moov` after `mdat`. Returns the underlying writer. - pub fn finish(mut self) -> Result { - self.finish_mdat()?; - let moov = self.build_moov(0, false)?; - self.w.write_all(&moov)?; - self.w.flush()?; - Ok(self.w) - } -} - -impl Mp4Writer { - /// Finish with `moov` in front of `mdat` ("fast start"): media data is shifted forward in place. - pub fn finish_faststart(mut self) -> Result { - self.finish_mdat()?; - let mut moov = self.build_moov(0, false)?; - let mut force = false; - loop { - let shift = moov.len() as u64; - let next = self.build_moov(shift, force)?; - if next.len() == moov.len() { - moov = next; - break; - } - // Offsets crossed the 32-bit boundary: switch to co64 (size only grows, so this converges). - force = true; - moov = next; - } - let shift = moov.len() as u64; - let start = self.pad_pos; - let end = self.pos; - let mut buf = vec![0u8; 1 << 20]; - let mut cur = end; - while cur > start { - let n = (cur - start).min(buf.len() as u64) as usize; - cur -= n as u64; - self.w.seek(SeekFrom::Start(cur))?; - self.w.read_exact(&mut buf[..n])?; - self.w.seek(SeekFrom::Start(cur + shift))?; - self.w.write_all(&buf[..n])?; - } - self.w.seek(SeekFrom::Start(start))?; - self.w.write_all(&moov)?; - self.w.seek(SeekFrom::Start(end + shift))?; - self.w.flush()?; - Ok(self.w) - } -} - -/// Buffered fragment sample: (data, duration, composition offset, sync). -type PendingSample = (Vec, u32, i32, bool); - -/// Fragmented MP4 writer: an init segment (`ftyp` + `moov` with `mvex`) followed by -/// `moof`+`mdat` fragments. Samples are buffered until [`flush_fragment`](Self::flush_fragment). -pub struct FragmentedWriter { - w: W, - opts: WriterOptions, - tracks: Vec, - pending: Vec>, - next_dts: Vec, - seq: u32, - init_written: bool, -} - -impl FragmentedWriter { - pub fn new(w: W, opts: WriterOptions) -> Self { - FragmentedWriter { w, opts, tracks: Vec::new(), pending: Vec::new(), next_dts: Vec::new(), seq: 0, init_written: false } - } - - /// Add a track (before the first fragment). PCM is not supported in fragments. - pub fn add_track(&mut self, cfg: TrackConfig) -> Result { - if self.init_written { - return Err(Error::Mux("tracks must be added before the first fragment".into())); - } - if validate_track(&cfg)?.is_some() { - return Err(Error::Mux("PCM is not supported by the fragmented writer".into())); - } - let handler = handler_of(&cfg.entry); - self.tracks.push(WTrack { - id: self.tracks.len() as u32 + 1, - handler, - pcm_frame_bytes: None, - t: Tables::new(), - cfg, - tmcd_ref: None, - tmcd_source: None, - }); - self.pending.push(Vec::new()); - self.next_dts.push(0); - Ok(self.tracks.len() - 1) - } - - fn write_init(&mut self) -> Result<()> { - let qt = self.opts.brand == Brand::Mov; - let ts = self.opts.movie_timescale.max(1); - let mut b = BoxBuf::new(); - write_ftyp(&mut b, self.opts.brand); - let m = b.start(b"moov"); - write_mvhd(&mut b, ts, 0, self.tracks.len() as u32 + 1); - for t in &self.tracks { - write_trak(&mut b, t, ts, qt, 0, false)?; - } - let mv = b.start(b"mvex"); - for t in &self.tracks { - let x = b.start_full(b"trex", 0, 0); - b.u32(t.id); - b.u32(1); - b.u32(0); - b.u32(0); - b.u32(0); - b.end(x); - } - b.end(mv); - write_udta(&mut b, &self.opts.metadata); - b.end(m); - self.w.write_all(&b.buf)?; - self.init_written = true; - Ok(()) - } - - /// Buffer a sample for the next fragment. - pub fn write_sample(&mut self, track: usize, s: WriteSample) -> Result<()> { - let p = self.pending.get_mut(track).ok_or(Error::NoSuchTrack(track))?; - if s.data.len() > u32::MAX as usize { - return Err(Error::Mux("sample larger than 4 GiB".into())); - } - p.push((s.data.to_vec(), s.duration, s.composition_offset, s.is_sync)); - Ok(()) - } - - fn build_moof(&self, data_offsets: &[i32]) -> Vec { - let mut b = BoxBuf::new(); - let m = b.start(b"moof"); - let h = b.start_full(b"mfhd", 0, 0); - b.u32(self.seq); - b.end(h); - for (i, t) in self.tracks.iter().enumerate() { - let p = &self.pending[i]; - if p.is_empty() { - continue; - } - let tf = b.start(b"traf"); - let h = b.start_full(b"tfhd", 0, 0x20000); - b.u32(t.id); - b.end(h); - let h = b.start_full(b"tfdt", 1, 0); - b.u64(self.next_dts[i]); - b.end(h); - let neg = p.iter().any(|s| s.2 < 0); - let h = b.start_full(b"trun", neg as u8, 0x1 | 0x100 | 0x200 | 0x400 | 0x800); - b.u32(p.len() as u32); - b.i32(data_offsets[i]); - for (d, dur, cto, sync) in p { - b.u32(*dur); - b.u32(d.len() as u32); - b.u32(if *sync { 0x0200_0000 } else { 0x0101_0000 }); - b.i32(*cto); - } - b.end(h); - b.end(tf); - } - b.end(m); - b.buf - } - - /// Write all buffered samples as one `moof` + `mdat` fragment (no-op if nothing is buffered). - pub fn flush_fragment(&mut self) -> Result<()> { - if !self.init_written { - self.write_init()?; - } - if self.pending.iter().all(|p| p.is_empty()) { - return Ok(()); - } - self.seq += 1; - let probe = self.build_moof(&vec![0; self.tracks.len()]); - let data_len: u64 = self.pending.iter().flatten().map(|s| s.0.len() as u64).sum(); - let large = data_len + 8 > u32::MAX as u64; - let hdr = if large { 16 } else { 8 }; - let mut offs = Vec::with_capacity(self.tracks.len()); - let mut acc = probe.len() as u64 + hdr; - for p in &self.pending { - offs.push(i32::try_from(acc).map_err(|_| Error::Mux("fragment too large".into()))?); - acc += p.iter().map(|s| s.0.len() as u64).sum::(); - } - let moof = self.build_moof(&offs); - self.w.write_all(&moof)?; - if large { - self.w.write_all(&1u32.to_be_bytes())?; - self.w.write_all(b"mdat")?; - self.w.write_all(&(data_len + 16).to_be_bytes())?; - } else { - self.w.write_all(&((data_len + 8) as u32).to_be_bytes())?; - self.w.write_all(b"mdat")?; - } - for (i, p) in self.pending.iter_mut().enumerate() { - for (d, dur, _, _) in p.drain(..) { - self.w.write_all(&d)?; - self.next_dts[i] += dur as u64; - } - } - Ok(()) - } - - /// Flush remaining samples and return the writer. - pub fn finish(mut self) -> Result { - self.flush_fragment()?; - self.w.flush()?; - Ok(self.w) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::codec::{AvcConfig, PcmConfig}; - use std::io::Cursor; - - fn avc_entry() -> SampleEntry { - SampleEntry::avc(AvcConfig::new(vec![vec![0x67, 0x42, 0, 0x1E]], vec![vec![0x68, 0xCE]], 4), 64, 48) - } - - #[test] - fn language_pack() { - assert_eq!(pack_language("und"), 0x55C4); - assert_eq!(pack_language("eng"), 0x15C7); - assert_eq!(pack_language("EN"), 0x55C4); - } - - #[test] - fn simple_roundtrip_and_faststart() { - for fast in [false, true] { - let mut w = Mp4Writer::new(Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).unwrap(); - let v = w.add_track(TrackConfig::new(avc_entry(), 12800)).unwrap(); - let pcm = PcmConfig { bits: 16, float: false, big_endian: false, signed: true, channels: 2, sample_rate: 48000.0 }; - let a = w.add_track(TrackConfig::new(SampleEntry::pcm(pcm), 48000)).unwrap(); - for i in 0..10u8 { - let data = vec![i; 100 + i as usize]; - w.write_sample( - v, - WriteSample { data: &data, duration: 512, composition_offset: if i % 2 == 0 { 1024 } else { 0 }, is_sync: i % 5 == 0 }, - ) - .unwrap(); - w.write_sample(a, WriteSample { data: &[i; 400], duration: 0, composition_offset: 0, is_sync: true }).unwrap(); - } - let out = if fast { w.finish_faststart() } else { w.finish() }.unwrap().into_inner(); - let f = crate::open(out.as_slice()).unwrap(); - assert_eq!(f.tracks.len(), 2); - let t = &f.tracks[0]; - assert_eq!(t.samples.len(), 10); - for (i, s) in t.samples.iter().enumerate() { - assert_eq!(s.size as usize, 100 + i); - assert_eq!(s.dts, 512 * i as i64); - assert_eq!(s.is_sync, i % 5 == 0); - assert_eq!(f.read_sample(out.as_slice(), 0, i).unwrap(), vec![i as u8; 100 + i]); - } - let at = &f.tracks[1]; - assert!(at.pcm_chunked); - assert_eq!(at.samples.len(), 10); - assert_eq!(at.samples[3].size, 400); - assert_eq!(at.samples[3].duration, 100); - assert_eq!(f.read_sample(out.as_slice(), 1, 3).unwrap(), vec![3u8; 400]); - if fast { - assert!(f.moov_offset < t.samples[0].offset); - } - } - } - - #[test] - fn fragmented_roundtrip() { - let mut w = FragmentedWriter::new(Vec::new(), WriterOptions::new(Brand::Mp4)); - let v = w.add_track(TrackConfig::new(avc_entry(), 1000)).unwrap(); - for i in 0..6u8 { - w.write_sample(v, WriteSample { data: &[i; 10], duration: 40, composition_offset: -(i as i32 % 2), is_sync: i % 3 == 0 }).unwrap(); - if i % 3 == 2 { - w.flush_fragment().unwrap(); - } - } - let out = w.finish().unwrap(); - let f = crate::open(out.as_slice()).unwrap(); - assert!(f.fragmented); - assert_eq!(f.fragment_count, 2); - let t = &f.tracks[0]; - assert_eq!(t.samples.len(), 6); - for (i, s) in t.samples.iter().enumerate() { - assert_eq!(s.dts, 40 * i as i64); - assert_eq!(s.pts, s.dts - (i as i64 % 2)); - assert_eq!(s.is_sync, i % 3 == 0); - assert_eq!(f.read_sample(out.as_slice(), 0, i).unwrap(), vec![i as u8; 10]); - } - assert_eq!(t.sync_sample_before(5), 3); - } -} diff --git a/crates/isobmff/src/source.rs b/crates/isobmff/src/source.rs deleted file mode 100644 index 0dad82b2..00000000 --- a/crates/isobmff/src/source.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Random-access byte sources for the demuxer. - -use std::io; -use std::sync::Arc; - -/// Random-access, read-only byte source (file, in-memory buffer, web Blob…). -/// -/// Implementations must be able to serve reads at arbitrary offsets through `&self`. -pub trait ByteSource { - /// Total length in bytes. - fn len(&self) -> u64; - /// Fill `buf` entirely with bytes starting at `offset`; fail with `UnexpectedEof` if past the end. - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>; - /// True if the source holds no bytes. - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -fn eof() -> io::Error { - io::Error::new(io::ErrorKind::UnexpectedEof, "read past end of byte source") -} - -impl ByteSource for [u8] { - fn len(&self) -> u64 { - <[u8]>::len(self) as u64 - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - let start = usize::try_from(offset).map_err(|_| eof())?; - let end = start.checked_add(buf.len()).ok_or_else(eof)?; - let src = self.get(start..end).ok_or_else(eof)?; - buf.copy_from_slice(src); - Ok(()) - } -} - -impl ByteSource for Vec { - fn len(&self) -> u64 { - self.as_slice().len() as u64 - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - self.as_slice().read_at(offset, buf) - } -} - -impl ByteSource for &T { - fn len(&self) -> u64 { - (**self).len() - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - (**self).read_at(offset, buf) - } -} - -impl ByteSource for Arc { - fn len(&self) -> u64 { - (**self).len() - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - (**self).read_at(offset, buf) - } -} - -impl ByteSource for Box { - fn len(&self) -> u64 { - (**self).len() - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - (**self).read_at(offset, buf) - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl ByteSource for std::fs::File { - fn len(&self) -> u64 { - self.metadata().map(|m| m.len()).unwrap_or(0) - } - #[cfg(unix)] - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - std::os::unix::fs::FileExt::read_exact_at(self, buf, offset) - } - #[cfg(windows)] - fn read_at(&self, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> { - use std::os::windows::fs::FileExt; - while !buf.is_empty() { - let n = self.seek_read(buf, offset)?; - if n == 0 { - return Err(eof()); - } - offset += n as u64; - buf = &mut buf[n..]; - } - Ok(()) - } - #[cfg(not(any(unix, windows)))] - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - use std::io::{Read, Seek, SeekFrom}; - let mut f = self; - f.seek(SeekFrom::Start(offset))?; - f.read_exact(buf) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn slice_reads() { - let v = vec![1u8, 2, 3, 4]; - let mut b = [0u8; 2]; - v.read_at(1, &mut b).unwrap(); - assert_eq!(b, [2, 3]); - assert!(v.read_at(3, &mut b).is_err()); - assert!(v.read_at(u64::MAX, &mut b).is_err()); - let a: Arc<[u8]> = Arc::from(v.clone()); - a.read_at(2, &mut b).unwrap(); - assert_eq!(b, [3, 4]); - assert_eq!(ByteSource::len(&a), 4); - let s: &[u8] = &v; - assert_eq!(ByteSource::len(&s), 4); - } -} diff --git a/crates/matroska/Cargo.toml b/crates/matroska/Cargo.toml index 48d2c236..cc6d7116 100644 --- a/crates/matroska/Cargo.toml +++ b/crates/matroska/Cargo.toml @@ -10,3 +10,4 @@ rust-version.workspace = true workspace = true [dependencies] +craft-media = { workspace = true, features = ["matroska"] } diff --git a/crates/matroska/src/codec.rs b/crates/matroska/src/codec.rs deleted file mode 100644 index e44f47ea..00000000 --- a/crates/matroska/src/codec.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Matroska CodecID → codec mapping (Matroska codec specifications, RFC 9559 §5.1.4.1.21 and the -//! codec mappings registry). - -/// Codec of a track, derived from `CodecID` and `CodecPrivate`. -/// -/// Configuration records are passed through as the raw bytes found in `CodecPrivate`, which for -/// the ISO codecs are exactly the ISOBMFF configuration boxes' payloads (avcC, hvcC, av1C, ASC…). -#[derive(Clone, Debug, PartialEq)] -pub enum Codec { - /// `V_MPEG4/ISO/AVC`; payload is an `AVCDecoderConfigurationRecord` (avcC). - Avc { avcc: Vec }, - /// `V_MPEGH/ISO/HEVC`; payload is an `HEVCDecoderConfigurationRecord` (hvcC). - Hevc { hvcc: Vec }, - /// `V_VP8`. - Vp8, - /// `V_VP9`; `CodecPrivate` (optional) holds VP9 codec feature metadata. - Vp9 { private: Vec }, - /// `V_AV1`; payload is an `AV1CodecConfigurationRecord` (av1C). - Av1 { av1c: Vec }, - /// `V_PRORES`; `fourcc` from `CodecPrivate` (e.g. `apcn`), if present. - ProRes { fourcc: Option<[u8; 4]> }, - /// `V_MJPEG`, or `V_MS/VFW/FOURCC` with an `MJPG` compression FourCC. - Mjpeg, - /// `V_MS/VFW/FOURCC` with any other FourCC; `bitmap_info_header` is the whole `CodecPrivate`. - VfwFourcc { fourcc: [u8; 4], bitmap_info_header: Vec }, - /// `A_AAC` (and the legacy `A_AAC/MPEG{2,4}/` IDs); `asc` is the - /// `AudioSpecificConfig` (synthesised for legacy IDs without `CodecPrivate`). - Aac { asc: Vec }, - /// `A_OPUS`; `head` is the `OpusHead` identification header. - Opus { head: Vec }, - /// `A_VORBIS`; the three Vorbis headers (identification, comment, setup), split from the - /// Xiph-laced `CodecPrivate`. - Vorbis { headers: Vec> }, - /// `A_FLAC`; `private` is `fLaC` + metadata blocks (STREAMINFO first). - Flac { private: Vec }, - /// `A_PCM/INT/LIT`, `A_PCM/INT/BIG`, `A_PCM/FLOAT/IEEE`. Bit depth comes from the Audio element. - Pcm { float: bool, big_endian: bool, bits: u16 }, - /// `A_AC3`. - Ac3, - /// `A_EAC3`. - Eac3, - /// `A_MPEG/L3`. - Mp3, - /// `A_MPEG/L2`. - Mp2, - /// `S_TEXT/UTF8` (SubRip-style plain text). - SubRip, - /// `S_TEXT/WEBVTT`, or WebM's `D_WEBVTT/{SUBTITLES,CAPTIONS,DESCRIPTIONS,METADATA}`. - WebVtt, - /// `S_TEXT/ASS` / `S_TEXT/SSA`; `header` is the script header from `CodecPrivate`. - Ass { header: Vec, ssa: bool }, - /// Any other CodecID (kept verbatim along with `Track::codec_private`). - Other(String), -} - -impl Codec { - /// Short codec name, matching FFmpeg's `codec_name` where one exists (useful for logs/UI). - pub fn name(&self) -> String { - match self { - Codec::Avc { .. } => "h264".into(), - Codec::Hevc { .. } => "hevc".into(), - Codec::Vp8 => "vp8".into(), - Codec::Vp9 { .. } => "vp9".into(), - Codec::Av1 { .. } => "av1".into(), - Codec::ProRes { .. } => "prores".into(), - Codec::Mjpeg => "mjpeg".into(), - Codec::VfwFourcc { fourcc, .. } => String::from_utf8_lossy(fourcc).trim().to_ascii_lowercase(), - Codec::Aac { .. } => "aac".into(), - Codec::Opus { .. } => "opus".into(), - Codec::Vorbis { .. } => "vorbis".into(), - Codec::Flac { .. } => "flac".into(), - Codec::Pcm { float, big_endian, bits } => { - let k = if *float { - 'f' - } else if *bits == 8 { - 'u' - } else { - 's' - }; - if *bits == 8 { format!("pcm_{k}8") } else { format!("pcm_{k}{bits}{}", if *big_endian { "be" } else { "le" }) } - } - Codec::Ac3 => "ac3".into(), - Codec::Eac3 => "eac3".into(), - Codec::Mp3 => "mp3".into(), - Codec::Mp2 => "mp2".into(), - Codec::SubRip => "subrip".into(), - Codec::WebVtt => "webvtt".into(), - Codec::Ass { ssa, .. } => if *ssa { "ssa" } else { "ass" }.into(), - Codec::Other(id) => id.clone(), - } - } -} - -const AAC_RATES: [u32; 13] = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; - -/// Build a 2-byte (or 5-byte with SBR extension) AudioSpecificConfig for legacy `A_AAC/...` IDs. -fn synth_asc(object_type: u8, rate: f64, out_rate: Option, channels: u64, sbr: bool) -> Vec { - let rate_index = |r: f64| AAC_RATES.iter().position(|&x| x as f64 == r).unwrap_or(4) as u8; - let ri = rate_index(rate); - let ch = channels.min(7) as u8; - let mut v = vec![(object_type << 3) | (ri >> 1), ((ri & 1) << 7) | (ch << 3)]; - if sbr { - // backward-compatible explicit signalling: sync 0x2b7, SBR object type 5, flag, ext rate index - let ext = rate_index(out_rate.unwrap_or(rate * 2.0)); - v.push(0x56); - v.push(0xE5); - v.push(0x80 | (ext << 3)); - } - v -} - -/// Split a Xiph-laced `CodecPrivate` (Vorbis/Theora headers). -fn split_xiph(p: &[u8]) -> Vec> { - let Some(&n) = p.first() else { return Vec::new() }; - let mut pos = 1; - let mut sizes = Vec::new(); - for _ in 0..n { - let mut s = 0usize; - loop { - let Some(&b) = p.get(pos) else { return Vec::new() }; - pos += 1; - s += b as usize; - if b != 255 { - break; - } - } - sizes.push(s); - } - let mut out = Vec::new(); - for s in sizes { - let Some(h) = p.get(pos..pos + s) else { return Vec::new() }; - out.push(h.to_vec()); - pos += s; - } - out.push(p[pos.min(p.len())..].to_vec()); - out -} - -/// Map a CodecID + CodecPrivate (+ audio params for legacy AAC / PCM) to a [`Codec`]. -pub(crate) fn map_codec(id: &str, private: &[u8], audio: Option<&crate::AudioInfo>) -> Codec { - let p = private.to_vec(); - match id { - "V_MPEG4/ISO/AVC" => Codec::Avc { avcc: p }, - "V_MPEGH/ISO/HEVC" => Codec::Hevc { hvcc: p }, - "V_VP8" => Codec::Vp8, - "V_VP9" => Codec::Vp9 { private: p }, - "V_AV1" => Codec::Av1 { av1c: p }, - "V_PRORES" => Codec::ProRes { fourcc: private.get(..4).and_then(|f| f.try_into().ok()) }, - "V_MJPEG" => Codec::Mjpeg, - "V_MS/VFW/FOURCC" => { - let fourcc: [u8; 4] = private.get(16..20).and_then(|f| f.try_into().ok()).unwrap_or(*b" "); - match &fourcc { - b"MJPG" | b"mjpg" | b"AVRn" | b"AVDJ" => Codec::Mjpeg, - b"H264" | b"h264" | b"avc1" | b"X264" | b"x264" => Codec::Avc { avcc: Vec::new() }, - b"apch" | b"apcn" | b"apcs" | b"apco" | b"ap4h" | b"ap4x" => Codec::ProRes { fourcc: Some(fourcc) }, - _ => Codec::VfwFourcc { fourcc, bitmap_info_header: p }, - } - } - "A_AAC" => Codec::Aac { asc: p }, - "A_OPUS" => Codec::Opus { head: p }, - "A_VORBIS" => Codec::Vorbis { headers: split_xiph(private) }, - "A_FLAC" => Codec::Flac { private: p }, - "A_PCM/INT/LIT" | "A_PCM/INT/BIG" | "A_PCM/FLOAT/IEEE" => { - let bits = audio.and_then(|a| a.bit_depth).unwrap_or(if id == "A_PCM/FLOAT/IEEE" { 32 } else { 16 }) as u16; - Codec::Pcm { float: id == "A_PCM/FLOAT/IEEE", big_endian: id == "A_PCM/INT/BIG", bits } - } - "A_AC3" | "A_AC3/BSID9" | "A_AC3/BSID10" => Codec::Ac3, - "A_EAC3" => Codec::Eac3, - "A_MPEG/L3" => Codec::Mp3, - "A_MPEG/L2" => Codec::Mp2, - "S_TEXT/UTF8" => Codec::SubRip, - "S_TEXT/WEBVTT" | "D_WEBVTT/SUBTITLES" | "D_WEBVTT/CAPTIONS" | "D_WEBVTT/DESCRIPTIONS" | "D_WEBVTT/METADATA" => Codec::WebVtt, - "S_TEXT/ASS" | "S_ASS" => Codec::Ass { header: p, ssa: false }, - "S_TEXT/SSA" | "S_SSA" => Codec::Ass { header: p, ssa: true }, - _ if id.starts_with("A_AAC/") => { - if !private.is_empty() { - return Codec::Aac { asc: p }; - } - let (rate, out_rate, ch) = audio.map_or((48000.0, None, 2), |a| (a.sampling_frequency, a.output_sampling_frequency, a.channels)); - let object_type = if id.ends_with("/MAIN") { - 1 - } else if id.ends_with("/SSR") { - 3 - } else if id.ends_with("/LTP") { - 4 - } else { - 2 - }; - let sbr = id.ends_with("/SBR"); - Codec::Aac { asc: synth_asc(object_type, rate, out_rate, ch, sbr) } - } - _ => Codec::Other(id.to_string()), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mapping() { - assert_eq!(map_codec("V_MPEG4/ISO/AVC", &[1, 2], None), Codec::Avc { avcc: vec![1, 2] }); - assert_eq!(map_codec("V_PRORES", b"apcn", None), Codec::ProRes { fourcc: Some(*b"apcn") }); - let mut bih = vec![0u8; 40]; - bih[16..20].copy_from_slice(b"MJPG"); - assert_eq!(map_codec("V_MS/VFW/FOURCC", &bih, None), Codec::Mjpeg); - bih[16..20].copy_from_slice(b"FFV1"); - assert!(matches!(map_codec("V_MS/VFW/FOURCC", &bih, None), Codec::VfwFourcc { fourcc, .. } if &fourcc == b"FFV1")); - let a = crate::AudioInfo { sampling_frequency: 44100.0, channels: 2, bit_depth: Some(24), output_sampling_frequency: None }; - assert_eq!(map_codec("A_PCM/INT/LIT", &[], Some(&a)).name(), "pcm_s24le"); - assert_eq!(map_codec("A_PCM/FLOAT/IEEE", &[], Some(&a)), Codec::Pcm { float: true, big_endian: false, bits: 24 }); - // legacy AAC LC 44.1k stereo → 0x12 0x10 - assert_eq!(map_codec("A_AAC/MPEG4/LC", &[], Some(&a)), Codec::Aac { asc: vec![0x12, 0x10] }); - assert_eq!(map_codec("S_TEXT/UTF8", &[], None).name(), "subrip"); - assert_eq!(map_codec("X_WHATEVER", &[], None), Codec::Other("X_WHATEVER".into())); - } - - #[test] - fn xiph_split() { - let mut p = vec![2u8, 3, 2]; - p.extend_from_slice(&[1, 1, 1, 2, 2, 3, 3, 3, 3]); - let h = split_xiph(&p); - assert_eq!(h, vec![vec![1, 1, 1], vec![2, 2], vec![3, 3, 3, 3]]); - } -} diff --git a/crates/matroska/src/demux.rs b/crates/matroska/src/demux.rs deleted file mode 100644 index 66dfe177..00000000 --- a/crates/matroska/src/demux.rs +++ /dev/null @@ -1,1176 +0,0 @@ -//! Matroska / WebM demuxer: EBML header, Segment walk, cluster scanning, packet reading, seeking. - -use std::collections::{HashSet, VecDeque}; - -use crate::ebml::{self, Children, Lacing, Size, parse_lacing, read_id, read_size}; -use crate::error::{Error, Result}; -use crate::ids::*; -use crate::meta::*; -use crate::source::{ByteSource, ReadSeekSource}; -use crate::track::{Sample, Track, TrackKind, gcd}; - -const WIN: usize = 64 * 1024; -/// Upper bound for level-1 metadata elements read into memory (Info, Tracks, Cues, Tags…). -const MAX_META: u64 = 256 << 20; - -/// Options for [`open_with`] / [`Demuxer::with_options`]. -#[derive(Clone, Debug)] -pub struct OpenOptions { - /// Scan every cluster at open to build per-track sample tables and keyframe indices - /// (reads only element and block headers, never frame payloads). Default `true`. - /// - /// With `false`, open stops at the first Cluster and uses the SeekHead to find Cues, Tags, - /// Chapters… placed after the clusters; [`Demuxer::seek`] then uses Cues (or builds the index - /// on first use if there are none). - pub index: bool, - /// Verify CRC-32 elements of level-1 elements (and clusters while indexing). Mismatches are - /// recorded in [`MkvFile::crc_errors`]; parsing continues. Default `false`. - pub verify_crc: bool, -} - -impl Default for OpenOptions { - fn default() -> Self { - Self { index: true, verify_crc: false } - } -} - -/// A parsed Matroska / WebM file (first Segment). -#[derive(Clone, Debug)] -pub struct MkvFile { - /// EBML `DocType`: `matroska` or `webm`. - pub doc_type: String, - pub doc_type_version: u64, - pub doc_type_read_version: u64, - /// Absolute offset of the Segment's data (base for SeekHead and Cues positions). - pub segment_data_start: u64, - /// Absolute end of the Segment (file end for unknown-size / live segments). - pub segment_end: u64, - /// Segment size field was "unknown" (live / streamed output). - pub live: bool, - pub info: SegmentInfo, - pub tracks: Vec, - pub seek_head: Vec, - /// Cue points, sorted by time. - pub cues: Vec, - /// Clusters in file order (complete only when [`MkvFile::indexed`]). - pub clusters: Vec, - pub chapters: Vec, - pub tags: Vec, - pub attachments: Vec, - /// Absolute offset of the first Cluster. - pub first_cluster: Option, - /// Sample tables have been built by scanning all clusters. - pub indexed: bool, - /// Number of CRC-32 elements verified (with `verify_crc`). - pub crc_checked: u32, - /// Absolute offsets of elements whose CRC-32 did not match. - pub crc_errors: Vec, - /// Non-fatal problems met while parsing (damage, resyncs, truncation). - pub warnings: Vec, -} - -/// One demuxed frame. -#[derive(Clone, Debug, PartialEq)] -pub struct Packet { - /// Index into [`MkvFile::tracks`]. - pub track: usize, - /// Matroska `TrackNumber`. - pub track_number: u64, - /// Presentation timestamp in timestamp ticks (the track timebase, [`Track::timebase`]). - pub pts: i64, - /// Presentation timestamp in nanoseconds. - pub pts_ns: i64, - /// Duration in ticks (0 if unknown). - pub duration: u64, - /// Duration in nanoseconds (0 if unknown). - pub duration_ns: u64, - pub keyframe: bool, - pub discardable: bool, - pub invisible: bool, - /// `DiscardPadding` (ns) from the enclosing BlockGroup. - pub discard_padding_ns: Option, - /// Absolute offset of the stored frame data. - pub offset: u64, - /// Index of the frame within a laced block (0 when not laced). Laced frames after the first - /// only have exact timestamps when the block or track carries a duration; otherwise they - /// repeat the block timestamp and `duration` is 0. - pub lace: u16, - /// Frame bytes with header stripping undone. Frames of tracks with other content encodings - /// (zlib, encryption) are returned as stored; see [`Track::frames_readable`]. - pub data: Vec, -} - -/// Result of [`Demuxer::seek`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SeekPoint { - pub track: usize, - /// Keyframe timestamp in ticks and ns. - pub pts: i64, - pub pts_ns: i64, - /// Sample index of the keyframe when the file is indexed. - pub sample: Option, - pub cluster_offset: u64, - pub block_offset: u64, -} - -/// A keyframe index entry ([`Demuxer::keyframe_index`]). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Keyframe { - pub sample: usize, - pub pts: i64, - pub pts_ns: i64, - pub block_offset: u64, -} - -// --------------------------------------------------------------------------------------------- -// I/O with a small read window -// --------------------------------------------------------------------------------------------- - -#[derive(Default, Clone, Debug)] -struct Cache { - buf: Vec, - start: u64, -} - -struct Io<'a, S: ByteSource + ?Sized> { - src: &'a S, - len: u64, - cache: &'a mut Cache, -} - -#[derive(Clone, Copy, Debug)] -struct Hdr { - id: u32, - pos: u64, - size: Size, - hlen: usize, -} - -impl Hdr { - fn data_start(&self) -> u64 { - self.pos + self.hlen as u64 - } - /// End offset, clamped to `limit`; unknown sizes run to `limit`. - fn end(&self, limit: u64) -> u64 { - match self.size { - Size::Known(n) => self.data_start().saturating_add(n).min(limit), - Size::Unknown => limit, - } - } - fn overruns(&self, limit: u64) -> bool { - matches!(self.size, Size::Known(n) if self.data_start().saturating_add(n) > limit) - } -} - -impl<'a, S: ByteSource + ?Sized> Io<'a, S> { - fn new(src: &'a S, cache: &'a mut Cache) -> Self { - let len = src.len(); - Self { src, len, cache } - } - - /// Up to `n` (≤ WIN) bytes at `pos` (fewer at EOF). - fn peek(&mut self, pos: u64, n: usize) -> Result<&[u8]> { - let n = n.min(WIN); - if pos >= self.len { - return Ok(&[]); - } - let want_end = (pos + n as u64).min(self.len); - let c = &*self.cache; - if !(pos >= c.start && want_end <= c.start + c.buf.len() as u64) { - let fill = ((self.len - pos) as usize).min(WIN); - let mut buf = std::mem::take(&mut self.cache.buf); - buf.resize(fill, 0); - self.src.read_at(pos, &mut buf)?; - self.cache.buf = buf; - self.cache.start = pos; - } - let o = (pos - self.cache.start) as usize; - Ok(&self.cache.buf[o..o + (want_end - pos) as usize]) - } - - /// Exactly `n` bytes at `pos`. - fn read_vec(&mut self, pos: u64, n: u64) -> Result> { - if pos.checked_add(n).is_none_or(|e| e > self.len) { - return Err(Error::Io(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "read past end of file"))); - } - if n as usize <= WIN / 2 { - return Ok(self.peek(pos, n as usize)?.to_vec()); - } - let mut v = vec![0u8; n as usize]; - self.src.read_at(pos, &mut v)?; - Ok(v) - } - - /// Element header at `pos` (`None` if undecodable or past `limit`). - fn header(&mut self, pos: u64, limit: u64) -> Result> { - if pos >= limit { - return Ok(None); - } - let b = self.peek(pos, 12)?; - let Some((id, il)) = read_id(b) else { return Ok(None) }; - let Some((size, sl)) = read_size(&b[il..]) else { return Ok(None) }; - let hlen = il + sl; - if pos + hlen as u64 > limit { - return Ok(None); - } - Ok(Some(Hdr { id, pos, size, hlen })) - } - - /// Scan forward from `from` for a plausible level-1 element (Cluster preferred), up to `limit`. - fn resync(&mut self, from: u64, limit: u64) -> Result> { - let mut pos = from; - while pos + 4 <= limit { - let chunk = self.peek(pos, WIN)?.to_vec(); - if chunk.len() < 4 { - return Ok(None); - } - for i in 0..chunk.len() - 3 { - if chunk[i] & 0xF0 != 0x10 { - continue; - } - let id = u32::from_be_bytes([chunk[i], chunk[i + 1], chunk[i + 2], chunk[i + 3]]); - if !is_top_level(id) || id == EBML || id == SEGMENT { - continue; - } - let p = pos + i as u64; - if self.plausible(p, id, limit)? { - return Ok(Some(p)); - } - } - pos += (chunk.len() - 3) as u64; - } - Ok(None) - } - - fn plausible(&mut self, p: u64, id: u32, limit: u64) -> Result { - let Some(h) = self.header(p, limit)? else { return Ok(false) }; - if h.id != id || h.overruns(limit) { - return Ok(false); - } - if h.size == Size::Unknown && id != CLUSTER { - return Ok(false); - } - // first child must decode and fit; clusters must start with Timestamp (after CRC-32/Void) - let mut cp = h.data_start(); - let end = h.end(limit); - for _ in 0..3 { - let Some(c) = self.header(cp, end)? else { return Ok(false) }; - if c.overruns(end) { - return Ok(false); - } - if id != CLUSTER { - return Ok(true); - } - match c.id { - TIMESTAMP => return Ok(true), - CRC32 | VOID => cp = c.end(end), - _ => return Ok(false), - } - } - Ok(false) - } - - /// End of an unknown-size master element: the first child that is a top-level ID or undecodable. - fn unknown_end(&mut self, data_start: u64, limit: u64) -> Result { - let mut pos = data_start; - while pos < limit { - let Some(h) = self.header(pos, limit)? else { return Ok(pos) }; - if is_top_level(h.id) || h.size == Size::Unknown { - return Ok(pos); - } - pos = h.end(limit); - } - Ok(limit) - } -} - -// --------------------------------------------------------------------------------------------- -// Blocks -// --------------------------------------------------------------------------------------------- - -#[derive(Clone, Debug)] -struct BlockRef { - track: usize, - block_offset: u64, - /// Timestamp in ticks (cluster + relative). - ts: i64, - /// Absolute (offset, size) of each frame. - frames: Vec<(u64, u32)>, - block_duration: Option, - keyframe: bool, - discardable: bool, - invisible: bool, - discard_padding: Option, -} - -#[derive(Clone, Copy, Debug)] -struct FrameRef { - track: usize, - offset: u64, - size: u32, - pts: i64, - pts_ns: i64, - duration: u64, - duration_ns: u64, - keyframe: bool, - discardable: bool, - invisible: bool, - discard_padding: Option, - block_offset: u64, - lace: u16, -} - -fn track_lookup(tracks: &[Track]) -> impl Fn(u64) -> Option + '_ { - move |n| tracks.iter().position(|t| t.number == n) -} - -/// Parse the Block/SimpleBlock header at `data_start` (`len` bytes). -/// Returns (track index, relative ts, flags, frames) or `None` for unknown tracks / damage. -fn parse_block( - io: &mut Io<'_, S>, - data_start: u64, - len: u64, - lookup: &dyn Fn(u64) -> Option, -) -> Result)>> { - let head = io.peek(data_start, 16.min(len as usize))?; - let Some((tn, tl, _)) = ebml::read_vint(head) else { return Ok(None) }; - if head.len() < tl + 3 { - return Ok(None); - } - let rel = i16::from_be_bytes([head[tl], head[tl + 1]]); - let flags = head[tl + 2]; - let Some(track) = lookup(tn) else { return Ok(None) }; - let hdr = (tl + 3) as u64; - let lacing = Lacing::from_flags(flags); - let frames = if lacing == Lacing::None { - vec![(data_start + hdr, (len - hdr) as u32)] - } else { - let body = io.read_vec(data_start + hdr, len - hdr)?; - match parse_lacing(lacing, &body) { - Ok(f) => f.into_iter().map(|(o, l)| (data_start + hdr + o as u64, l as u32)).collect(), - Err(_) => return Ok(None), - } - }; - Ok(Some((track, rel, flags, frames))) -} - -/// Cluster timestamps are unsigned and must fit in `i64` before a signed block offset is added. -fn cluster_timestamp(raw: u64) -> Result { - i64::try_from(raw).map_err(|_| Error::Invalid("cluster timestamp out of range".into())) -} - -fn block_timestamp(cluster_ts: i64, rel: i16) -> Result { - cluster_ts.checked_add(i64::from(rel)).ok_or_else(|| Error::Invalid("block timestamp overflow".into())) -} - -/// Parse one SimpleBlock or BlockGroup element. -fn parse_block_elem( - io: &mut Io<'_, S>, - h: &Hdr, - end: u64, - cluster_ts: i64, - lookup: &dyn Fn(u64) -> Option, -) -> Result> { - let ds = h.data_start(); - match h.id { - SIMPLE_BLOCK => { - let Some((track, rel, flags, frames)) = parse_block(io, ds, end - ds, lookup)? else { return Ok(None) }; - Ok(Some(BlockRef { - track, - block_offset: h.pos, - ts: block_timestamp(cluster_ts, rel)?, - frames, - block_duration: None, - keyframe: flags & 0x80 != 0, - discardable: flags & 0x01 != 0, - invisible: flags & 0x08 != 0, - discard_padding: None, - })) - } - BLOCK_GROUP => { - let mut pos = ds; - let mut block = None; - let (mut dur, mut has_ref, mut pad) = (None, false, None); - while pos < end { - let Some(c) = io.header(pos, end)? else { break }; - let ce = c.end(end); - match c.id { - BLOCK => block = parse_block(io, c.data_start(), ce - c.data_start(), lookup)?, - BLOCK_DURATION => dur = Some(ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?)), - REFERENCE_BLOCK => has_ref = true, - DISCARD_PADDING => pad = Some(ebml::int(io.peek(c.data_start(), (ce - c.data_start()) as usize)?)), - _ => {} - } - pos = ce; - } - let Some((track, rel, flags, frames)) = block else { return Ok(None) }; - Ok(Some(BlockRef { - track, - block_offset: h.pos, - ts: block_timestamp(cluster_ts, rel)?, - frames, - block_duration: dur, - keyframe: !has_ref, - discardable: false, - invisible: flags & 0x08 != 0, - discard_padding: pad, - })) - } - _ => Ok(None), - } -} - -/// Expand a block into frames with per-lace timestamps. -fn expand(b: &BlockRef, t: &Track, scale: u64, skip: u16, out: &mut impl Extend) { - let n = b.frames.len() as u64; - // total block duration in ns - let block_ns: Option = match (b.block_duration, t.default_duration_ns) { - (Some(d), _) => Some(d as u128 * scale as u128), - (None, Some(dd)) => Some(dd as u128 * n as u128), - _ => None, - }; - let scale_i = scale as i128; - // round half away from zero - let to_ticks = |ns: i128| -> i64 { (if ns >= 0 { (ns + scale_i / 2) / scale_i } else { (ns - scale_i / 2) / scale_i }) as i64 }; - // CodecDelay is subtracted from every timestamp (RFC 9559 §5.1.4.1.25); in ticks it is rounded - // once, so tick timestamps stay on the block grid. - let delay_ns = t.codec_delay_ns as i128; - let delay_ticks = to_ticks(delay_ns); - let base_ns = b.ts as i128 * scale_i; - out.extend(b.frames.iter().enumerate().skip(skip as usize).map(|(i, &(offset, size))| { - let (start, end) = match block_ns { - Some(total) => ((total * i as u128 / n as u128) as i128, Some((total * (i as u128 + 1) / n as u128) as i128)), - None => (0, None), - }; - let pts_ns = base_ns + start - delay_ns; - let pts = to_ticks(base_ns + start) - delay_ticks; - let duration = end.map_or(0, |e| (to_ticks(e) - to_ticks(start)).max(0) as u64); - FrameRef { - track: b.track, - offset, - size, - pts, - pts_ns: pts_ns as i64, - duration, - duration_ns: end.map_or(0, |e| (e - start).max(0) as u64), - keyframe: b.keyframe, - discardable: b.discardable, - invisible: b.invisible, - discard_padding: b.discard_padding, - block_offset: b.block_offset, - lace: i as u16, - } - })); -} - -/// Scan a whole cluster (headers only), returning its info and blocks. -fn scan_cluster( - io: &mut Io<'_, S>, - h: &Hdr, - limit: u64, - tracks: &[Track], - warnings: &mut Vec, -) -> Result<(ClusterInfo, Vec)> { - let lookup = track_lookup(tracks); - let ds = h.data_start(); - let known_end = match h.size { - Size::Known(_) => Some(h.end(limit)), - Size::Unknown => None, - }; - let lim = known_end.unwrap_or(limit); - let mut ts: i64 = 0; - let mut blocks = Vec::new(); - let mut pos = ds; - let end = loop { - if pos >= lim { - break lim; - } - let Some(c) = io.header(pos, lim)? else { - warnings.push(format!("damaged cluster at {}: undecodable element at {pos}", h.pos)); - break io.resync(pos + 1, limit)?.unwrap_or(limit); - }; - if known_end.is_none() && is_top_level(c.id) { - break pos; - } - if c.size == Size::Unknown || c.overruns(lim) { - warnings.push(format!("damaged cluster at {}: element 0x{:X} at {pos} overruns", h.pos, c.id)); - if c.overruns(lim) && matches!(c.id, SIMPLE_BLOCK | BLOCK_GROUP) && io.len <= c.data_start() + c.size.known().unwrap_or(0) { - break lim; // truncated file: drop the partial block - } - break io.resync(pos + 1, limit)?.unwrap_or(limit); - } - let ce = c.end(lim); - match c.id { - TIMESTAMP => ts = cluster_timestamp(ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?))?, - SIMPLE_BLOCK | BLOCK_GROUP => { - if let Some(b) = parse_block_elem(io, &c, ce, ts, &lookup)? { - blocks.push(b); - } - } - _ => {} - } - pos = ce; - }; - Ok((ClusterInfo { offset: h.pos, data_start: ds, end, timestamp: ts.max(0) as u64 }, blocks)) -} - -// --------------------------------------------------------------------------------------------- -// Open -// --------------------------------------------------------------------------------------------- - -/// Open a Matroska/WebM file with default options (full sample index). -pub fn open(src: S) -> Result { - open_with(src, &OpenOptions::default()) -} - -/// Open with explicit options. -pub fn open_with(src: S, opts: &OpenOptions) -> Result { - let mut cache = Cache::default(); - open_impl(&src, opts, &mut cache) -} - -fn open_impl(src: &S, opts: &OpenOptions, cache: &mut Cache) -> Result { - let mut io = Io::new(src, cache); - let len = io.len; - // EBML header (tolerate leading junk in the first 64 KiB) - let start = { - let b = io.peek(0, WIN)?; - let magic = [0x1A, 0x45, 0xDF, 0xA3]; - b.windows(4).position(|w| w == magic).ok_or_else(|| Error::NotMatroska("no EBML header".into()))? as u64 - }; - let eh = io.header(start, len)?.ok_or_else(|| Error::NotMatroska("bad EBML header".into()))?; - let hdr_end = eh.end(len); - let hdr = io.read_vec(eh.data_start(), hdr_end - eh.data_start())?; - let (mut doc_type, mut dtv, mut dtrv) = ("matroska".to_string(), 1, 1); - for e in Children::new(&hdr) { - match e.id { - DOC_TYPE => doc_type = ebml::string(e.data), - DOC_TYPE_VERSION => dtv = ebml::uint(e.data), - DOC_TYPE_READ_VERSION => dtrv = ebml::uint(e.data), - EBML_MAX_ID_LENGTH if ebml::uint(e.data) > 4 => return Err(Error::Unsupported("EBMLMaxIDLength > 4".into())), - EBML_MAX_SIZE_LENGTH if ebml::uint(e.data) > 8 => return Err(Error::Unsupported("EBMLMaxSizeLength > 8".into())), - _ => {} - } - } - if doc_type != "matroska" && doc_type != "webm" { - return Err(Error::NotMatroska(format!("DocType {doc_type:?}"))); - } - // Segment - let mut pos = hdr_end; - let seg = loop { - let h = io.header(pos, len)?.ok_or_else(|| Error::Invalid("no Segment".into()))?; - if h.id == SEGMENT { - break h; - } - if h.size == Size::Unknown { - return Err(Error::Invalid("unknown-size element before Segment".into())); - } - pos = h.end(len); - }; - let mut file = MkvFile { - doc_type, - doc_type_version: dtv, - doc_type_read_version: dtrv, - segment_data_start: seg.data_start(), - segment_end: seg.end(len), - live: seg.size == Size::Unknown, - info: SegmentInfo::default(), - tracks: Vec::new(), - seek_head: Vec::new(), - cues: Vec::new(), - clusters: Vec::new(), - chapters: Vec::new(), - tags: Vec::new(), - attachments: Vec::new(), - first_cluster: None, - indexed: false, - crc_checked: 0, - crc_errors: Vec::new(), - warnings: Vec::new(), - }; - if seg.overruns(len) { - file.warnings.push("Segment extends past end of file (truncated)".into()); - } - let mut w = Walker { opts, parsed: HashSet::new(), blocks_seen: false }; - w.walk(&mut io, &mut file)?; - finish_tracks(&mut file); - Ok(file) -} - -struct Walker<'o> { - opts: &'o OpenOptions, - parsed: HashSet, - blocks_seen: bool, -} - -impl Walker<'_> { - fn walk(&mut self, io: &mut Io<'_, S>, file: &mut MkvFile) -> Result<()> { - let seg_end = file.segment_end; - let mut pos = file.segment_data_start; - while pos < seg_end { - let Some(h) = io.header(pos, seg_end)? else { - match io.resync(pos + 1, seg_end)? { - Some(p) => { - file.warnings.push(format!("undecodable element at {pos}; resynced at {p}")); - pos = p; - continue; - } - None => break, - } - }; - match h.id { - CLUSTER => { - if file.first_cluster.is_none() { - file.first_cluster = Some(h.pos); - // make sure Info/Tracks are known before interpreting blocks - self.follow_seek_head(io, file, &[INFO, TRACKS])?; - if !self.opts.index { - break; - } - } - let (ci, blocks) = scan_cluster(io, &h, seg_end, &file.tracks, &mut file.warnings)?; - if self.opts.verify_crc && h.size != Size::Unknown { - let data = io.read_vec(h.data_start(), ci.end - h.data_start())?; - self.check_crc(file, h.pos, &data); - } - add_blocks(file, file.clusters.len() as u32, &blocks); - self.blocks_seen = true; - file.clusters.push(ci); - pos = ci.end.max(h.data_start()); - } - SEEK_HEAD | INFO | TRACKS | CUES | CHAPTERS | TAGS | ATTACHMENTS => { - pos = self.level1(io, file, &h, seg_end)?; - } - EBML | SEGMENT => break, // chained segment: only the first one is read - _ => { - if h.size == Size::Unknown { - pos = io.unknown_end(h.data_start(), seg_end)?; - } else if h.overruns(seg_end) && h.id != VOID { - match io.resync(pos + 1, seg_end)? { - Some(p) => { - file.warnings.push(format!("oversized element 0x{:X} at {pos}; resynced at {p}", h.id)); - pos = p; - } - None => break, - } - } else { - pos = h.end(seg_end); - } - } - } - } - self.follow_seek_head(io, file, &[INFO, TRACKS, CUES, CHAPTERS, TAGS, ATTACHMENTS, SEEK_HEAD])?; - // follow a second-level SeekHead reached through the first one - self.follow_seek_head(io, file, &[CUES, CHAPTERS, TAGS, ATTACHMENTS])?; - file.cues.sort_by_key(|c| c.time); - file.indexed = self.opts.index; - Ok(()) - } - - fn follow_seek_head(&mut self, io: &mut Io<'_, S>, file: &mut MkvFile, ids: &[u32]) -> Result<()> { - let entries = file.seek_head.clone(); - for e in entries { - if !ids.contains(&e.id) { - continue; - } - let Some(abs) = file.segment_data_start.checked_add(e.position) else { continue }; - if self.parsed.contains(&abs) || abs >= file.segment_end { - continue; - } - match io.header(abs, file.segment_end)? { - Some(h) if h.id == e.id => { - self.level1(io, file, &h, file.segment_end)?; - } - _ => file.warnings.push(format!("SeekHead entry 0x{:X} -> {abs} does not point at that element", e.id)), - } - } - Ok(()) - } - - fn check_crc(&self, file: &mut MkvFile, pos: u64, data: &[u8]) { - if let Some(ok) = ebml::verify_crc(data) { - file.crc_checked += 1; - if !ok { - file.crc_errors.push(pos); - file.warnings.push(format!("CRC-32 mismatch in element at {pos}")); - } - } - } - - /// Parse a level-1 metadata element; returns its end. - fn level1(&mut self, io: &mut Io<'_, S>, file: &mut MkvFile, h: &Hdr, limit: u64) -> Result { - let end = if h.size == Size::Unknown { io.unknown_end(h.data_start(), limit)? } else { h.end(limit) }; - if h.overruns(limit) { - file.warnings.push(format!("element 0x{:X} at {} truncated", h.id, h.pos)); - } - if !self.parsed.insert(h.pos) { - return Ok(end); - } - if h.id == ATTACHMENTS { - self.attachments(io, file, h.data_start(), end)?; - return Ok(end); - } - let n = end - h.data_start(); - if n > MAX_META { - file.warnings.push(format!("element 0x{:X} at {} too large ({n} bytes); skipped", h.id, h.pos)); - return Ok(end); - } - let data = io.read_vec(h.data_start(), n)?; - if self.opts.verify_crc { - self.check_crc(file, h.pos, &data); - } - match h.id { - SEEK_HEAD => file.seek_head.extend(parse_seekhead(&data)), - INFO => file.info = parse_info(&data), - TRACKS => { - if file.tracks.is_empty() { - file.tracks = parse_tracks(&data, file.info.timestamp_scale); - } - } - CUES => parse_cues(&data, &mut file.cues), - CHAPTERS => parse_chapters(&data, &mut file.chapters), - TAGS => parse_tags(&data, &mut file.tags), - _ => {} - } - Ok(end) - } - - fn attachments(&mut self, io: &mut Io<'_, S>, file: &mut MkvFile, start: u64, end: u64) -> Result<()> { - let mut pos = start; - while pos < end { - let Some(h) = io.header(pos, end)? else { break }; - let he = h.end(end); - if h.id == ATTACHED_FILE { - let mut a = Attachment::default(); - let mut p = h.data_start(); - while p < he { - let Some(c) = io.header(p, he)? else { break }; - let ce = c.end(he); - let n = ce - c.data_start(); - if c.id == FILE_DATA { - a.data_offset = c.data_start(); - a.data_size = n; - } else if n <= 4096 { - let d = io.peek(c.data_start(), n as usize)?; - match c.id { - FILE_NAME => a.name = ebml::string(d), - FILE_MEDIA_TYPE => a.media_type = ebml::string(d), - FILE_DESCRIPTION => a.description = Some(ebml::string(d)), - FILE_UID => a.uid = ebml::uint(d), - _ => {} - } - } - p = ce; - } - file.attachments.push(a); - } - pos = he; - } - Ok(()) - } -} - -fn add_blocks(file: &mut MkvFile, cluster: u32, blocks: &[BlockRef]) { - let scale = file.info.timestamp_scale; - let mut frames: Vec = Vec::new(); - for b in blocks { - frames.clear(); - let t = &file.tracks[b.track]; - expand(b, t, scale, 0, &mut frames); - let t = &mut file.tracks[b.track]; - for f in &frames { - if f.keyframe { - t.sync.push(t.samples.len() as u32); - } - t.samples.push(Sample { - offset: f.offset, - size: f.size, - pts: f.pts, - duration: f.duration, - keyframe: f.keyframe, - cluster, - block_offset: f.block_offset, - lace: f.lace, - }); - } - } -} - -fn finish_tracks(file: &mut MkvFile) { - let scale = file.info.timestamp_scale; - let g = gcd(scale, 1_000_000_000); - for t in &mut file.tracks { - t.timebase = (scale / g, 1_000_000_000 / g); - } -} - -// --------------------------------------------------------------------------------------------- -// MkvFile helpers -// --------------------------------------------------------------------------------------------- - -impl MkvFile { - /// True for WebM (`DocType` = `webm`). - pub fn is_webm(&self) -> bool { - self.doc_type == "webm" - } - - /// Segment duration in ns: `Info/Duration` if present, else the end of the last indexed sample. - pub fn duration_ns(&self) -> Option { - if let Some(d) = self.info.duration { - return Some((d * self.info.timestamp_scale as f64).round() as u64); - } - let end = self.tracks.iter().flat_map(|t| t.samples.iter().map(|s| s.pts + s.duration as i64)).max()?; - Some((end.max(0) as u64).saturating_mul(self.info.timestamp_scale)) - } - - /// Index of the track with this `TrackNumber`. - pub fn track_by_number(&self, number: u64) -> Option { - self.tracks.iter().position(|t| t.number == number) - } - - /// Index of the first track of the given kind (preferring the default flag). - pub fn track_of_kind(&self, kind: TrackKind) -> Option { - let mut it = self.tracks.iter().enumerate().filter(|(_, t)| t.kind == kind); - let first = it.clone().next().map(|x| x.0); - it.find(|(_, t)| t.default).map(|x| x.0).or(first) - } - - /// Read sample `index` of `track` (header stripping undone). - pub fn read_sample(&self, src: &S, track: usize, index: usize) -> Result> { - let t = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?; - let s = t.samples.get(index).ok_or(Error::NoSuchSample(index))?; - if !t.frames_readable() { - return Err(Error::Unsupported(format!("content encoding of track {} (compression/encryption)", t.number))); - } - let mut data = t.frame_prefix(s.size as u64); - let at = data.len(); - data.resize(at + s.size as usize, 0); - src.read_at(s.offset, &mut data[at..])?; - Ok(data) - } - - /// Keyframe sample of `track` with the greatest pts ≤ `time_ns` (indexed files only). - pub fn keyframe_before(&self, track: usize, time_ns: i64) -> Option { - let t = self.tracks.get(track)?; - t.keyframe_before_pts(t.ns_to_ticks(time_ns)) - } -} - -// --------------------------------------------------------------------------------------------- -// Demuxer (streaming packets + seeking) -// --------------------------------------------------------------------------------------------- - -#[derive(Clone, Copy, Debug)] -struct InCluster { - ts: i64, - end: Option, -} - -/// Packet reader over a [`ByteSource`]: iterates frames in file order and seeks to keyframes. -pub struct Demuxer { - src: S, - file: MkvFile, - opts: OpenOptions, - cache: Cache, - pos: u64, - cluster: Option, - pending: VecDeque, - skip_laces: Option<(u64, u16)>, - done: bool, -} - -impl Demuxer> { - /// Open from any `Read + Seek` stream. - pub fn from_reader(r: R) -> Result { - Self::new(ReadSeekSource::new(r)?) - } -} - -impl<'a> Demuxer<&'a [u8]> { - /// Open from an in-memory byte slice. - pub fn from_slice(data: &'a [u8]) -> Result { - Self::new(data) - } -} - -impl Demuxer { - /// Open with default options (full index). - pub fn new(src: S) -> Result { - Self::with_options(src, OpenOptions::default()) - } - - pub fn with_options(src: S, opts: OpenOptions) -> Result { - let mut cache = Cache::default(); - let file = open_impl(&src, &opts, &mut cache)?; - let pos = file.first_cluster.unwrap_or(file.segment_end); - Ok(Self { src, file, opts, cache, pos, cluster: None, pending: VecDeque::new(), skip_laces: None, done: false }) - } - - pub fn file(&self) -> &MkvFile { - &self.file - } - pub fn tracks(&self) -> &[Track] { - &self.file.tracks - } - pub fn source(&self) -> &S { - &self.src - } - pub fn into_parts(self) -> (S, MkvFile) { - (self.src, self.file) - } - /// Segment duration in ns. - pub fn duration_ns(&self) -> Option { - self.file.duration_ns() - } - - /// Scan all clusters and build sample tables (no-op if already indexed). - pub fn build_index(&mut self) -> Result<()> { - if self.file.indexed { - return Ok(()); - } - let opts = OpenOptions { index: true, verify_crc: self.opts.verify_crc }; - self.file = open_impl(&self.src, &opts, &mut self.cache)?; - Ok(()) - } - - /// Keyframes of `track` (builds the index if needed). - pub fn keyframe_index(&mut self, track: usize) -> Result> { - self.build_index()?; - let t = self.file.tracks.get(track).ok_or(Error::NoSuchTrack(track))?; - Ok(t.keyframes() - .iter() - .map(|&k| { - let s = &t.samples[k as usize]; - Keyframe { sample: k as usize, pts: s.pts, pts_ns: t.ticks_to_ns(s.pts), block_offset: s.block_offset } - }) - .collect()) - } - - /// Rewind to the first cluster. - pub fn rewind(&mut self) { - self.pos = self.file.first_cluster.unwrap_or(self.file.segment_end); - self.cluster = None; - self.pending.clear(); - self.skip_laces = None; - self.done = false; - } - - /// Read sample `index` of `track` (requires an index). - pub fn read_sample(&self, track: usize, index: usize) -> Result> { - self.file.read_sample(&self.src, track, index) - } - - /// Position the packet stream at the keyframe of `track` with the greatest pts ≤ `time_ns` - /// (or the track's first keyframe if none precedes it). The next [`Demuxer::next_packet`] - /// call returns packets starting at that keyframe's block (all tracks). - /// - /// Uses the sample index when present, else Cues (refined by scanning the cued cluster), else - /// builds the index first. - pub fn seek(&mut self, track: usize, time_ns: i64) -> Result { - let t = self.file.tracks.get(track).ok_or(Error::NoSuchTrack(track))?; - let ticks = t.ns_to_ticks(time_ns); - if !self.file.indexed { - let tn = t.number; - let cue = self - .file - .cues - .iter() - .filter(|c| c.time as i64 <= ticks) - .filter_map(|c| c.positions.iter().find(|p| p.track == tn).map(|p| (c.time, *p))) - .next_back() - .or_else(|| self.file.cues.iter().find_map(|c| c.positions.iter().find(|p| p.track == tn).map(|p| (c.time, *p)))); - if let Some((_, cp)) = cue - && let Some(sp) = self.seek_via_cue(track, ticks, cp.cluster_position)? - { - return Ok(sp); - } - self.build_index()?; - } - let t = &self.file.tracks[track]; - let k = t.keyframe_before_pts(ticks).ok_or_else(|| Error::Invalid(format!("track {} has no keyframes", t.number)))?; - let s = t.samples[k]; - let c = self.file.clusters[s.cluster as usize]; - let sp = - SeekPoint { track, pts: s.pts, pts_ns: t.ticks_to_ns(s.pts), sample: Some(k), cluster_offset: c.offset, block_offset: s.block_offset }; - self.position_at(c, s.block_offset, s.lace); - Ok(sp) - } - - fn position_at(&mut self, c: ClusterInfo, block_offset: u64, lace: u16) { - self.pending.clear(); - self.done = false; - self.pos = block_offset; - let known = c.end; - self.cluster = Some(InCluster { ts: c.timestamp as i64, end: Some(known) }); - self.skip_laces = (lace > 0).then_some((block_offset, lace)); - } - - fn seek_via_cue(&mut self, track: usize, ticks: i64, cluster_position: u64) -> Result> { - let abs = self.file.segment_data_start + cluster_position; - let mut io = Io::new(&self.src, &mut self.cache); - let Some(h) = io.header(abs, self.file.segment_end)? else { return Ok(None) }; - if h.id != CLUSTER { - self.file.warnings.push(format!("cue points at {abs}, which is not a Cluster")); - return Ok(None); - } - let mut warnings = Vec::new(); - let (ci, blocks) = scan_cluster(&mut io, &h, self.file.segment_end, &self.file.tracks, &mut warnings)?; - let t = &self.file.tracks[track]; - let mut frames = Vec::new(); - for b in blocks.iter().filter(|b| b.track == track && b.keyframe) { - expand(b, t, self.file.info.timestamp_scale, 0, &mut frames); - } - let best = frames.iter().filter(|f| f.pts <= ticks).max_by_key(|f| f.pts).or_else(|| frames.iter().min_by_key(|f| f.pts)).copied(); - let Some(f) = best else { return Ok(None) }; - let sp = SeekPoint { track, pts: f.pts, pts_ns: f.pts_ns, sample: None, cluster_offset: ci.offset, block_offset: f.block_offset }; - self.position_at(ci, f.block_offset, f.lace); - Ok(Some(sp)) - } - - /// Next frame in file order, or `None` at the end of the Segment. - pub fn next_packet(&mut self) -> Result> { - loop { - if let Some(f) = self.pending.pop_front() { - return self.materialize(f).map(Some); - } - if self.done || !self.fill()? { - self.done = true; - return Ok(None); - } - } - } - - fn materialize(&mut self, f: FrameRef) -> Result { - let t = &self.file.tracks[f.track]; - let mut data = if t.frames_readable() { t.frame_prefix(f.size as u64) } else { Vec::new() }; - let at = data.len(); - data.resize(at + f.size as usize, 0); - let mut io = Io::new(&self.src, &mut self.cache); - if (f.size as usize) <= WIN / 2 { - let b = io.peek(f.offset, f.size as usize)?; - if b.len() != f.size as usize { - return Err(Error::Invalid("frame data past end of file".into())); - } - data[at..].copy_from_slice(b); - } else { - self.src.read_at(f.offset, &mut data[at..])?; - } - Ok(Packet { - track: f.track, - track_number: t.number, - pts: f.pts, - pts_ns: f.pts_ns, - duration: f.duration, - duration_ns: f.duration_ns, - keyframe: f.keyframe, - discardable: f.discardable, - invisible: f.invisible, - discard_padding_ns: f.discard_padding, - offset: f.offset, - lace: f.lace, - data, - }) - } - - /// Advance over elements until at least one frame is pending. Returns false at the end. - fn fill(&mut self) -> Result { - let seg_end = self.file.segment_end; - let scale = self.file.info.timestamp_scale; - let mut io = Io::new(&self.src, &mut self.cache); - let lookup = track_lookup(&self.file.tracks); - loop { - if let Some(c) = self.cluster { - let lim = c.end.unwrap_or(seg_end); - if self.pos >= lim { - self.cluster = None; - continue; - } - let Some(h) = io.header(self.pos, lim)? else { - self.cluster = None; - match io.resync(self.pos + 1, seg_end)? { - Some(p) => { - self.pos = p; - continue; - } - None => return Ok(false), - } - }; - if c.end.is_none() && is_top_level(h.id) { - self.cluster = None; - continue; - } - if h.size == Size::Unknown || h.overruns(lim) { - self.cluster = None; - match io.resync(self.pos + 1, seg_end)? { - Some(p) => { - self.pos = p; - continue; - } - None => return Ok(false), - } - } - let he = h.end(lim); - self.pos = he; - match h.id { - TIMESTAMP => { - let v = cluster_timestamp(ebml::uint(io.peek(h.data_start(), (he - h.data_start()) as usize)?))?; - self.cluster = Some(InCluster { ts: v, end: c.end }); - } - SIMPLE_BLOCK | BLOCK_GROUP => { - if let Some(b) = parse_block_elem(&mut io, &h, he, c.ts, &lookup)? { - let skip = match self.skip_laces.take() { - Some((off, n)) if off == h.pos => n, - _ => 0, - }; - expand(&b, &self.file.tracks[b.track], scale, skip, &mut self.pending); - if !self.pending.is_empty() { - return Ok(true); - } - } - } - _ => {} - } - continue; - } - if self.pos >= seg_end { - return Ok(false); - } - let Some(h) = io.header(self.pos, seg_end)? else { - match io.resync(self.pos + 1, seg_end)? { - Some(p) => { - self.pos = p; - continue; - } - None => return Ok(false), - } - }; - match h.id { - CLUSTER => { - self.cluster = Some(InCluster { ts: 0, end: h.size.known().map(|_| h.end(seg_end)) }); - self.pos = h.data_start(); - } - EBML | SEGMENT => return Ok(false), - _ => { - self.pos = if h.size == Size::Unknown { io.unknown_end(h.data_start(), seg_end)? } else { h.end(seg_end) }; - if self.pos <= h.pos { - return Ok(false); - } - } - } - } - } -} - -impl Iterator for Demuxer { - type Item = Result; - fn next(&mut self) -> Option> { - match self.next_packet() { - Ok(Some(p)) => Some(Ok(p)), - Ok(None) => None, - Err(e) => { - self.done = true; - Some(Err(e)) - } - } - } -} diff --git a/crates/matroska/src/ebml.rs b/crates/matroska/src/ebml.rs deleted file mode 100644 index 6c2db76d..00000000 --- a/crates/matroska/src/ebml.rs +++ /dev/null @@ -1,476 +0,0 @@ -//! EBML primitives (RFC 8794): variable-length integers, element headers, typed values, CRC-32, -//! and Matroska block lacing (RFC 9559 §10.3). - -use crate::error::{Result, invalid}; - -/// Length in bytes of a VINT whose first byte is `b` (1..=8), or `None` for a zero first byte. -#[inline] -pub fn vint_len(b: u8) -> Option { - if b == 0 { None } else { Some(b.leading_zeros() as usize + 1) } -} - -/// Element data size: known, or "unknown" (all value bits set). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Size { - Known(u64), - Unknown, -} - -impl Size { - pub fn known(self) -> Option { - match self { - Size::Known(n) => Some(n), - Size::Unknown => None, - } - } -} - -/// Decode an unsigned VINT (marker bit stripped). Returns `(value, length, all_ones)`. -pub fn read_vint(data: &[u8]) -> Option<(u64, usize, bool)> { - let first = *data.first()?; - let len = vint_len(first)?; - if data.len() < len { - return None; - } - let mask = if len == 8 { 0 } else { 0xFFu8 >> len }; - let mut v = (first & mask) as u64; - for &b in &data[1..len] { - v = (v << 8) | b as u64; - } - let all_ones = v == (1u64 << (7 * len)) - 1; - Some((v, len, all_ones)) -} - -/// Decode an element data size. All-ones is the reserved "unknown size". -pub fn read_size(data: &[u8]) -> Option<(Size, usize)> { - let (v, len, ones) = read_vint(data)?; - Some((if ones { Size::Unknown } else { Size::Known(v) }, len)) -} - -/// Decode an element ID (marker bits kept, 1..=4 bytes). Rejects all-ones value bits (reserved). -/// All-zero value bits are accepted, because Matroska's `ChapterDisplay` uses ID `0x80`. -pub fn read_id(data: &[u8]) -> Option<(u32, usize)> { - let first = *data.first()?; - let len = vint_len(first)?; - if len > 4 || data.len() < len { - return None; - } - let (v, _, ones) = read_vint(&data[..len])?; - if ones || (v == 0 && len > 1) { - return None; - } - let mut id = 0u32; - for &b in &data[..len] { - id = (id << 8) | b as u32; - } - Some((id, len)) -} - -/// Decode a signed VINT as used by EBML lacing (value − (2^(7·len−1) − 1)). -pub fn read_svint(data: &[u8]) -> Option<(i64, usize)> { - let (v, len, _) = read_vint(data)?; - let bias = (1i64 << (7 * len - 1)) - 1; - Some((v as i64 - bias, len)) -} - -/// Encode an element ID (already carrying its marker bits). -pub fn write_id(out: &mut Vec, id: u32) { - let n = if id >= 0x100_0000 { - 4 - } else if id >= 0x1_0000 { - 3 - } else if id >= 0x100 { - 2 - } else { - 1 - }; - out.extend_from_slice(&id.to_be_bytes()[4 - n..]); -} - -/// Encode a data size as a VINT using the shortest length (or `min_len` if larger). -pub fn write_size(out: &mut Vec, size: u64, min_len: usize) { - let mut len = 1; - while len < 8 && size >= (1u64 << (7 * len)) - 1 { - len += 1; - } - let len = len.max(min_len).min(8); - let v = size | (1u64 << (7 * len)); - out.extend_from_slice(&v.to_be_bytes()[8 - len..]); -} - -/// Encode the "unknown size" marker of the given length. -pub fn write_unknown_size(out: &mut Vec, len: usize) { - out.push(0xFFu8 >> (len - 1)); - out.extend(std::iter::repeat_n(0xFF, len - 1)); -} - -/// Big-endian unsigned integer of 0..=8 bytes. -pub fn uint(data: &[u8]) -> u64 { - data.iter().take(8).fold(0u64, |a, &b| (a << 8) | b as u64) -} - -/// Big-endian two's-complement signed integer of 0..=8 bytes. -pub fn int(data: &[u8]) -> i64 { - let n = data.len().min(8); - if n == 0 { - return 0; - } - let v = uint(&data[..n]); - let shift = 64 - 8 * n as u32; - ((v << shift) as i64) >> shift -} - -/// IEEE float of 0, 4 or 8 bytes. -pub fn float(data: &[u8]) -> f64 { - match data.len() { - 4 => f32::from_be_bytes(data.try_into().unwrap_or([0; 4])) as f64, - 8 => f64::from_be_bytes(data.try_into().unwrap_or([0; 8])), - _ => 0.0, - } -} - -/// String / UTF-8 value: bytes up to the first NUL, lossily decoded. -pub fn string(data: &[u8]) -> String { - let end = data.iter().position(|&b| b == 0).unwrap_or(data.len()); - String::from_utf8_lossy(&data[..end]).into_owned() -} - -/// One element inside an in-memory master element. -#[derive(Clone, Copy, Debug)] -pub struct Elem<'a> { - pub id: u32, - /// Offset of the element header, relative to the start of the parent's data. - pub offset: usize, - pub header_len: usize, - pub data: &'a [u8], - /// The size field was "unknown" (data runs to the end of the parent). - pub unknown_size: bool, - /// The declared size overran the parent and was truncated. - pub truncated: bool, -} - -/// Iterator over the children of an in-memory master element. -/// -/// Damaged data ends the iteration (see [`Children::damaged`]); oversize elements are truncated to -/// the parent's end. -pub struct Children<'a> { - data: &'a [u8], - pos: usize, - damaged: bool, -} - -impl<'a> Children<'a> { - pub fn new(data: &'a [u8]) -> Self { - Self { data, pos: 0, damaged: false } - } - /// True if iteration stopped on an undecodable header. - pub fn damaged(&self) -> bool { - self.damaged - } -} - -impl<'a> Iterator for Children<'a> { - type Item = Elem<'a>; - fn next(&mut self) -> Option> { - let rest = self.data.get(self.pos..)?; - if rest.is_empty() { - return None; - } - let Some((id, il)) = read_id(rest) else { - self.damaged = true; - self.pos = self.data.len(); - return None; - }; - let Some((size, sl)) = read_size(&rest[il..]) else { - self.damaged = true; - self.pos = self.data.len(); - return None; - }; - let hl = il + sl; - let avail = (rest.len() - hl) as u64; - let (len, unknown, truncated) = match size { - Size::Known(n) if n <= avail => (n as usize, false, false), - Size::Known(_) => (avail as usize, false, true), - Size::Unknown => (avail as usize, true, false), - }; - let e = Elem { id, offset: self.pos, header_len: hl, data: &rest[hl..hl + len], unknown_size: unknown, truncated }; - self.pos += hl + len; - Some(e) - } -} - -/// CRC-32 (ISO 3309 / IEEE 802.3, reflected polynomial 0xEDB88320), as used by EBML CRC-32 elements. -pub fn crc32(data: &[u8]) -> u32 { - crc32_update(0xFFFF_FFFF, data) ^ 0xFFFF_FFFF -} - -/// Incremental form: start with `0xFFFF_FFFF`, finish with `^ 0xFFFF_FFFF`. -pub fn crc32_update(mut crc: u32, data: &[u8]) -> u32 { - static TABLE: std::sync::OnceLock<[u32; 256]> = std::sync::OnceLock::new(); - let t = TABLE.get_or_init(|| { - let mut t = [0u32; 256]; - for (i, e) in t.iter_mut().enumerate() { - let mut c = i as u32; - for _ in 0..8 { - c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }; - } - *e = c; - } - t - }); - for &b in data { - crc = t[((crc ^ b as u32) & 0xFF) as usize] ^ (crc >> 8); - } - crc -} - -/// Check a master element's leading CRC-32 child, if present. `Some(true/false)` if there is one. -pub fn verify_crc(master_data: &[u8]) -> Option { - let mut it = Children::new(master_data); - let first = it.next()?; - if first.id != crate::ids::CRC32 || first.data.len() != 4 { - return None; - } - let stored = u32::from_le_bytes(first.data.try_into().ok()?); - let rest = &master_data[first.offset + first.header_len + 4..]; - Some(crc32(rest) == stored) -} - -/// Block lacing mode (flags bits 1–2). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Lacing { - None, - Xiph, - FixedSize, - Ebml, -} - -impl Lacing { - pub fn from_flags(flags: u8) -> Lacing { - match (flags >> 1) & 3 { - 0 => Lacing::None, - 1 => Lacing::Xiph, - 2 => Lacing::FixedSize, - _ => Lacing::Ebml, - } - } -} - -/// Split laced block payload. `data` starts right after the block's flags byte and runs to the end -/// of the block. Returns `(offset, len)` of each frame relative to `data`. -pub fn parse_lacing(lacing: Lacing, data: &[u8]) -> Result> { - if lacing == Lacing::None { - return Ok(vec![(0, data.len())]); - } - let Some(&n1) = data.first() else { return invalid("laced block without frame count") }; - let count = n1 as usize + 1; - let mut pos = 1usize; - let mut sizes = Vec::with_capacity(count); - match lacing { - Lacing::None => return Ok(vec![(0, data.len())]), - Lacing::Xiph => { - for _ in 0..count - 1 { - let mut s = 0usize; - loop { - let Some(&b) = data.get(pos) else { return invalid("truncated Xiph lace header") }; - pos += 1; - s += b as usize; - if b != 255 { - break; - } - } - sizes.push(s); - } - } - Lacing::Ebml => { - if count > 1 { - let Some((first, l, _)) = read_vint(&data[pos..]) else { return invalid("bad EBML lace size") }; - pos += l; - let mut prev = first as i64; - sizes.push(first as usize); - for _ in 1..count - 1 { - let Some((d, l)) = read_svint(&data[pos..]) else { return invalid("bad EBML lace delta") }; - pos += l; - prev += d; - if prev < 0 { - return invalid("negative EBML lace size"); - } - sizes.push(prev as usize); - } - } - } - Lacing::FixedSize => { - let rest = data.len() - pos; - if !rest.is_multiple_of(count) { - return invalid("fixed-size lacing does not divide block"); - } - sizes.extend(std::iter::repeat_n(rest / count, count - 1)); - } - } - let used: usize = sizes.iter().try_fold(0usize, |a, &s| a.checked_add(s)).unwrap_or(usize::MAX); - if pos > data.len() || used > data.len() - pos { - return invalid("lace sizes exceed block"); - } - sizes.push(data.len() - pos - used); - let mut out = Vec::with_capacity(count); - for s in sizes { - out.push((pos, s)); - pos += s; - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn vint_roundtrip() { - for &v in &[0u64, 1, 126, 127, 128, 16382, 16383, 16384, 1 << 20, (1 << 56) - 2] { - let mut b = Vec::new(); - write_size(&mut b, v, 1); - let (s, l) = read_size(&b).unwrap(); - assert_eq!(s, Size::Known(v), "value {v}"); - assert_eq!(l, b.len()); - } - // 127 needs 2 bytes because 0xFF is "unknown" - let mut b = Vec::new(); - write_size(&mut b, 127, 1); - assert_eq!(b, [0x40, 0x7F]); - // padded length - let mut b = Vec::new(); - write_size(&mut b, 5, 8); - assert_eq!(b, [1, 0, 0, 0, 0, 0, 0, 5]); - assert_eq!(read_size(&b).unwrap(), (Size::Known(5), 8)); - } - - #[test] - fn vint_unknown_and_invalid() { - assert_eq!(read_size(&[0xFF]).unwrap(), (Size::Unknown, 1)); - assert_eq!(read_size(&[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]).unwrap(), (Size::Unknown, 8)); - for l in 1..=8 { - let mut b = Vec::new(); - write_unknown_size(&mut b, l); - assert_eq!(read_size(&b).unwrap(), (Size::Unknown, l)); - } - assert!(read_size(&[0x00, 1, 2]).is_none()); - assert!(read_size(&[0x40]).is_none()); // truncated - assert_eq!(read_vint(&[0x81]).unwrap(), (1, 1, false)); - assert_eq!(read_vint(&[0x10, 0x00, 0x00, 0x01]).unwrap(), (1, 4, false)); - } - - #[test] - fn ids() { - assert_eq!(read_id(&[0x1A, 0x45, 0xDF, 0xA3, 0x99]).unwrap(), (crate::ids::EBML, 4)); - assert_eq!(read_id(&[0xA3]).unwrap(), (0xA3, 1)); - assert_eq!(read_id(&[0x42, 0x86]).unwrap(), (0x4286, 2)); - assert!(read_id(&[0xFF]).is_none()); // reserved - assert_eq!(read_id(&[0x80]).unwrap(), (0x80, 1)); // ChapterDisplay - assert!(read_id(&[0x40, 0x00]).is_none()); // zero value - assert!(read_id(&[0x08, 0, 0, 0, 1]).is_none()); // 5-byte ID - for id in [0xA3u32, 0x4286, 0x2AD7B1, 0x1F43B675] { - let mut b = Vec::new(); - write_id(&mut b, id); - assert_eq!(read_id(&b).unwrap(), (id, b.len())); - } - } - - #[test] - fn signed_vint() { - assert_eq!(read_svint(&[0xBF]).unwrap(), (0, 1)); // 63 - 63 - assert_eq!(read_svint(&[0x80]).unwrap(), (-63, 1)); - assert_eq!(read_svint(&[0xC0]).unwrap(), (1, 1)); - assert_eq!(read_svint(&[0x5F, 0xFF]).unwrap(), (0, 2)); - assert_eq!(read_svint(&[0x60, 0x00]).unwrap(), (1, 2)); - } - - #[test] - fn values() { - assert_eq!(uint(&[]), 0); - assert_eq!(uint(&[1, 0]), 256); - assert_eq!(int(&[0xFF]), -1); - assert_eq!(int(&[0xFF, 0xFE]), -2); - assert_eq!(int(&[0x7F]), 127); - assert_eq!(float(&1.5f32.to_be_bytes()), 1.5); - assert_eq!(float(&2.25f64.to_be_bytes()), 2.25); - assert_eq!(string(b"abc\0\0"), "abc"); - } - - #[test] - fn crc() { - assert_eq!(crc32(b"123456789"), 0xCBF4_3926); - let payload = [0x42u8, 0x86, 0x81, 0x01]; - let mut m = vec![0xBF, 0x84]; - m.extend_from_slice(&crc32(&payload).to_le_bytes()); - m.extend_from_slice(&payload); - assert_eq!(verify_crc(&m), Some(true)); - m[7] ^= 1; - assert_eq!(verify_crc(&m), Some(false)); - assert_eq!(verify_crc(&payload), None); - } - - #[test] - fn children_iter() { - // two elements, then one whose size overruns - let d = [0x42, 0x86, 0x81, 0x01, 0xEC, 0x82, 0, 0, 0xA3, 0x85, 1, 2]; - let v: Vec<_> = Children::new(&d).collect(); - assert_eq!(v.len(), 3); - assert_eq!(v[0].id, 0x4286); - assert_eq!(v[0].data, &[1]); - assert_eq!(v[1].id, 0xEC); - assert_eq!(v[2].data, &[1, 2]); - assert!(v[2].truncated); - let mut it = Children::new(&[0x00, 0x01]); - assert!(it.next().is_none()); - assert!(it.damaged()); - } - - fn frames<'a>(d: &'a [u8], f: &[(usize, usize)]) -> Vec<&'a [u8]> { - f.iter().map(|&(o, l)| &d[o..o + l]).collect() - } - - #[test] - fn lacing_xiph() { - // 3 frames: 300, 2, rest(3) - let mut d = vec![2u8, 255, 45, 2]; - d.extend(std::iter::repeat_n(7u8, 300)); - d.extend_from_slice(&[1, 2]); - d.extend_from_slice(&[3, 4, 5]); - let f = parse_lacing(Lacing::Xiph, &d).unwrap(); - assert_eq!(f.iter().map(|x| x.1).collect::>(), [300, 2, 3]); - assert_eq!(frames(&d, &f)[2], &[3, 4, 5]); - // exact multiple of 255 needs a terminating 0 - let mut d = vec![1u8, 255, 0]; - d.extend(std::iter::repeat_n(1u8, 255)); - d.push(9); - let f = parse_lacing(Lacing::Xiph, &d).unwrap(); - assert_eq!(f.iter().map(|x| x.1).collect::>(), [255, 1]); - assert!(parse_lacing(Lacing::Xiph, &[1, 200, 1]).is_err()); - } - - #[test] - fn lacing_ebml() { - // 4 frames: 5, 3 (delta -2), 4 (delta +1), rest 2 - let mut d = vec![3u8, 0x85, 0xBF - 2, 0xBF + 1]; - d.extend_from_slice(&[1; 5]); - d.extend_from_slice(&[2; 3]); - d.extend_from_slice(&[3; 4]); - d.extend_from_slice(&[4; 2]); - let f = parse_lacing(Lacing::Ebml, &d).unwrap(); - assert_eq!(f.iter().map(|x| x.1).collect::>(), [5, 3, 4, 2]); - assert_eq!(frames(&d, &f), [&[1u8; 5][..], &[2; 3], &[3; 4], &[4; 2]]); - assert!(parse_lacing(Lacing::Ebml, &[1, 0x90]).is_err()); - } - - #[test] - fn lacing_fixed_and_none() { - let d = [2u8, 1, 1, 2, 2, 3, 3]; - let f = parse_lacing(Lacing::FixedSize, &d).unwrap(); - assert_eq!(frames(&d, &f), [&[1u8, 1][..], &[2, 2], &[3, 3]]); - assert!(parse_lacing(Lacing::FixedSize, &[2, 1, 1]).is_err()); - assert_eq!(parse_lacing(Lacing::None, &[9, 9]).unwrap(), [(0, 2)]); - assert_eq!(Lacing::from_flags(0x80 | 0x06), Lacing::Ebml); - assert_eq!(Lacing::from_flags(0x02), Lacing::Xiph); - assert_eq!(Lacing::from_flags(0x04), Lacing::FixedSize); - } -} diff --git a/crates/matroska/src/error.rs b/crates/matroska/src/error.rs deleted file mode 100644 index 718c5c50..00000000 --- a/crates/matroska/src/error.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::fmt; -use std::io; - -/// Errors produced by the demuxer. -#[derive(Debug)] -pub enum Error { - /// Underlying I/O failure. - Io(io::Error), - /// Structurally invalid data that could not be recovered from. - Invalid(String), - /// Valid but unsupported feature (e.g. zlib-compressed or encrypted tracks when reading data). - Unsupported(String), - /// Not an EBML file, or an EBML file whose DocType is not `matroska`/`webm`. - NotMatroska(String), - /// A track index was out of range. - NoSuchTrack(usize), - /// A sample index was out of range. - NoSuchSample(usize), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Io(e) => write!(f, "I/O error: {e}"), - Error::Invalid(s) => write!(f, "invalid data: {s}"), - Error::Unsupported(s) => write!(f, "unsupported: {s}"), - Error::NotMatroska(s) => write!(f, "not a Matroska/WebM file: {s}"), - Error::NoSuchTrack(i) => write!(f, "track index {i} out of range"), - Error::NoSuchSample(i) => write!(f, "sample index {i} out of range"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(e) => Some(e), - _ => None, - } - } -} - -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(e) - } -} - -pub type Result = std::result::Result; - -pub(crate) fn invalid(msg: impl Into) -> Result { - Err(Error::Invalid(msg.into())) -} diff --git a/crates/matroska/src/ids.rs b/crates/matroska/src/ids.rs deleted file mode 100644 index 6334f451..00000000 --- a/crates/matroska/src/ids.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! EBML / Matroska element IDs (RFC 8794, RFC 9559). IDs keep their VINT marker bits. -#![allow(dead_code)] - -// EBML header + global elements -pub const EBML: u32 = 0x1A45_DFA3; -pub const EBML_VERSION: u32 = 0x4286; -pub const EBML_READ_VERSION: u32 = 0x42F7; -pub const EBML_MAX_ID_LENGTH: u32 = 0x42F2; -pub const EBML_MAX_SIZE_LENGTH: u32 = 0x42F3; -pub const DOC_TYPE: u32 = 0x4282; -pub const DOC_TYPE_VERSION: u32 = 0x4287; -pub const DOC_TYPE_READ_VERSION: u32 = 0x4285; -pub const CRC32: u32 = 0xBF; -pub const VOID: u32 = 0xEC; - -// Segment and level-1 elements -pub const SEGMENT: u32 = 0x1853_8067; -pub const SEEK_HEAD: u32 = 0x114D_9B74; -pub const INFO: u32 = 0x1549_A966; -pub const TRACKS: u32 = 0x1654_AE6B; -pub const CLUSTER: u32 = 0x1F43_B675; -pub const CUES: u32 = 0x1C53_BB6B; -pub const ATTACHMENTS: u32 = 0x1941_A469; -pub const CHAPTERS: u32 = 0x1043_A770; -pub const TAGS: u32 = 0x1254_C367; - -/// True for IDs that can only appear directly inside a Segment (or are the EBML/Segment headers): -/// used to find the end of unknown-size elements. -pub fn is_top_level(id: u32) -> bool { - matches!(id, EBML | SEGMENT | SEEK_HEAD | INFO | TRACKS | CLUSTER | CUES | ATTACHMENTS | CHAPTERS | TAGS) -} - -// SeekHead -pub const SEEK: u32 = 0x4DBB; -pub const SEEK_ID: u32 = 0x53AB; -pub const SEEK_POSITION: u32 = 0x53AC; - -// Info -pub const SEGMENT_UUID: u32 = 0x73A4; -pub const SEGMENT_FILENAME: u32 = 0x7384; -pub const TIMESTAMP_SCALE: u32 = 0x2A_D7B1; -pub const DURATION: u32 = 0x4489; -pub const DATE_UTC: u32 = 0x4461; -pub const TITLE: u32 = 0x7BA9; -pub const MUXING_APP: u32 = 0x4D80; -pub const WRITING_APP: u32 = 0x5741; - -// Cluster -pub const TIMESTAMP: u32 = 0xE7; -pub const POSITION: u32 = 0xA7; -pub const PREV_SIZE: u32 = 0xAB; -pub const SIMPLE_BLOCK: u32 = 0xA3; -pub const BLOCK_GROUP: u32 = 0xA0; -pub const ENCRYPTED_BLOCK: u32 = 0xAF; -pub const SILENT_TRACKS: u32 = 0x5854; -pub const BLOCK: u32 = 0xA1; -pub const BLOCK_ADDITIONS: u32 = 0x75A1; -pub const BLOCK_DURATION: u32 = 0x9B; -pub const REFERENCE_PRIORITY: u32 = 0xFA; -pub const REFERENCE_BLOCK: u32 = 0xFB; -pub const CODEC_STATE: u32 = 0xA4; -pub const DISCARD_PADDING: u32 = 0x75A2; - -pub fn is_cluster_child(id: u32) -> bool { - matches!(id, TIMESTAMP | POSITION | PREV_SIZE | SIMPLE_BLOCK | BLOCK_GROUP | ENCRYPTED_BLOCK | SILENT_TRACKS | CRC32 | VOID) -} - -// Tracks -pub const TRACK_ENTRY: u32 = 0xAE; -pub const TRACK_NUMBER: u32 = 0xD7; -pub const TRACK_UID: u32 = 0x73C5; -pub const TRACK_TYPE: u32 = 0x83; -pub const FLAG_ENABLED: u32 = 0xB9; -pub const FLAG_DEFAULT: u32 = 0x88; -pub const FLAG_FORCED: u32 = 0x55AA; -pub const FLAG_HEARING_IMPAIRED: u32 = 0x55AB; -pub const FLAG_VISUAL_IMPAIRED: u32 = 0x55AC; -pub const FLAG_ORIGINAL: u32 = 0x55AE; -pub const FLAG_COMMENTARY: u32 = 0x55AF; -pub const FLAG_LACING: u32 = 0x9C; -pub const DEFAULT_DURATION: u32 = 0x23_E383; -pub const TRACK_TIMESTAMP_SCALE: u32 = 0x23_314F; -pub const NAME: u32 = 0x536E; -pub const LANGUAGE: u32 = 0x22_B59C; -pub const LANGUAGE_BCP47: u32 = 0x22_B59D; -pub const CODEC_ID: u32 = 0x86; -pub const CODEC_PRIVATE: u32 = 0x63A2; -pub const CODEC_NAME: u32 = 0x25_8688; -pub const CODEC_DELAY: u32 = 0x56AA; -pub const SEEK_PRE_ROLL: u32 = 0x56BB; - -// Video -pub const VIDEO: u32 = 0xE0; -pub const FLAG_INTERLACED: u32 = 0x9A; -pub const FIELD_ORDER: u32 = 0x9D; -pub const STEREO_MODE: u32 = 0x53B8; -pub const ALPHA_MODE: u32 = 0x53C0; -pub const PIXEL_WIDTH: u32 = 0xB0; -pub const PIXEL_HEIGHT: u32 = 0xBA; -pub const PIXEL_CROP_BOTTOM: u32 = 0x54AA; -pub const PIXEL_CROP_TOP: u32 = 0x54BB; -pub const PIXEL_CROP_LEFT: u32 = 0x54CC; -pub const PIXEL_CROP_RIGHT: u32 = 0x54DD; -pub const DISPLAY_WIDTH: u32 = 0x54B0; -pub const DISPLAY_HEIGHT: u32 = 0x54BA; -pub const DISPLAY_UNIT: u32 = 0x54B2; -pub const COLOUR_SPACE: u32 = 0x2E_B524; -pub const COLOUR: u32 = 0x55B0; -pub const PROJECTION: u32 = 0x7670; -pub const PROJECTION_TYPE: u32 = 0x7671; -pub const PROJECTION_POSE_YAW: u32 = 0x7673; -pub const PROJECTION_POSE_PITCH: u32 = 0x7674; -pub const PROJECTION_POSE_ROLL: u32 = 0x7675; - -// Colour -pub const MATRIX_COEFFICIENTS: u32 = 0x55B1; -pub const BITS_PER_CHANNEL: u32 = 0x55B2; -pub const CHROMA_SUBSAMPLING_HORZ: u32 = 0x55B3; -pub const CHROMA_SUBSAMPLING_VERT: u32 = 0x55B4; -pub const CB_SUBSAMPLING_HORZ: u32 = 0x55B5; -pub const CB_SUBSAMPLING_VERT: u32 = 0x55B6; -pub const CHROMA_SITING_HORZ: u32 = 0x55B7; -pub const CHROMA_SITING_VERT: u32 = 0x55B8; -pub const RANGE: u32 = 0x55B9; -pub const TRANSFER_CHARACTERISTICS: u32 = 0x55BA; -pub const PRIMARIES: u32 = 0x55BB; -pub const MAX_CLL: u32 = 0x55BC; -pub const MAX_FALL: u32 = 0x55BD; -pub const MASTERING_METADATA: u32 = 0x55D0; -pub const PRIMARY_R_X: u32 = 0x55D1; -pub const PRIMARY_R_Y: u32 = 0x55D2; -pub const PRIMARY_G_X: u32 = 0x55D3; -pub const PRIMARY_G_Y: u32 = 0x55D4; -pub const PRIMARY_B_X: u32 = 0x55D5; -pub const PRIMARY_B_Y: u32 = 0x55D6; -pub const WHITE_POINT_X: u32 = 0x55D7; -pub const WHITE_POINT_Y: u32 = 0x55D8; -pub const LUMINANCE_MAX: u32 = 0x55D9; -pub const LUMINANCE_MIN: u32 = 0x55DA; - -// Audio -pub const AUDIO: u32 = 0xE1; -pub const SAMPLING_FREQUENCY: u32 = 0xB5; -pub const OUTPUT_SAMPLING_FREQUENCY: u32 = 0x78B5; -pub const CHANNELS: u32 = 0x9F; -pub const BIT_DEPTH: u32 = 0x6264; - -// ContentEncodings -pub const CONTENT_ENCODINGS: u32 = 0x6D80; -pub const CONTENT_ENCODING: u32 = 0x6240; -pub const CONTENT_ENCODING_ORDER: u32 = 0x5031; -pub const CONTENT_ENCODING_SCOPE: u32 = 0x5032; -pub const CONTENT_ENCODING_TYPE: u32 = 0x5033; -pub const CONTENT_COMPRESSION: u32 = 0x5034; -pub const CONTENT_COMP_ALGO: u32 = 0x4254; -pub const CONTENT_COMP_SETTINGS: u32 = 0x4255; -pub const CONTENT_ENCRYPTION: u32 = 0x5035; - -// Cues -pub const CUE_POINT: u32 = 0xBB; -pub const CUE_TIME: u32 = 0xB3; -pub const CUE_TRACK_POSITIONS: u32 = 0xB7; -pub const CUE_TRACK: u32 = 0xF7; -pub const CUE_CLUSTER_POSITION: u32 = 0xF1; -pub const CUE_RELATIVE_POSITION: u32 = 0xF0; -pub const CUE_DURATION: u32 = 0xB2; -pub const CUE_BLOCK_NUMBER: u32 = 0x5378; - -// Attachments -pub const ATTACHED_FILE: u32 = 0x61A7; -pub const FILE_DESCRIPTION: u32 = 0x467E; -pub const FILE_NAME: u32 = 0x466E; -pub const FILE_MEDIA_TYPE: u32 = 0x4660; -pub const FILE_DATA: u32 = 0x465C; -pub const FILE_UID: u32 = 0x46AE; - -// Chapters -pub const EDITION_ENTRY: u32 = 0x45B9; -pub const EDITION_UID: u32 = 0x45BC; -pub const EDITION_FLAG_HIDDEN: u32 = 0x45BD; -pub const EDITION_FLAG_DEFAULT: u32 = 0x45DB; -pub const EDITION_FLAG_ORDERED: u32 = 0x45DD; -pub const CHAPTER_ATOM: u32 = 0xB6; -pub const CHAPTER_UID: u32 = 0x73C4; -pub const CHAPTER_STRING_UID: u32 = 0x5654; -pub const CHAPTER_TIME_START: u32 = 0x91; -pub const CHAPTER_TIME_END: u32 = 0x92; -pub const CHAPTER_FLAG_HIDDEN: u32 = 0x98; -pub const CHAPTER_FLAG_ENABLED: u32 = 0x4598; -pub const CHAPTER_DISPLAY: u32 = 0x80; -pub const CHAP_STRING: u32 = 0x85; -pub const CHAP_LANGUAGE: u32 = 0x437C; -pub const CHAP_LANGUAGE_BCP47: u32 = 0x437D; -pub const CHAP_COUNTRY: u32 = 0x437E; - -// Tags -pub const TAG: u32 = 0x7373; -pub const TARGETS: u32 = 0x63C0; -pub const TARGET_TYPE_VALUE: u32 = 0x68CA; -pub const TARGET_TYPE: u32 = 0x63CA; -pub const TAG_TRACK_UID: u32 = 0x63C5; -pub const TAG_EDITION_UID: u32 = 0x63C9; -pub const TAG_CHAPTER_UID: u32 = 0x63C4; -pub const TAG_ATTACHMENT_UID: u32 = 0x63C6; -pub const SIMPLE_TAG: u32 = 0x67C8; -pub const TAG_NAME: u32 = 0x45A3; -pub const TAG_LANGUAGE: u32 = 0x447A; -pub const TAG_LANGUAGE_BCP47: u32 = 0x447B; -pub const TAG_DEFAULT: u32 = 0x4484; -pub const TAG_STRING: u32 = 0x4487; -pub const TAG_BINARY: u32 = 0x4485; diff --git a/crates/matroska/src/lib.rs b/crates/matroska/src/lib.rs index 0f7deb1c..94428f61 100644 --- a/crates/matroska/src/lib.rs +++ b/crates/matroska/src/lib.rs @@ -1,40 +1,6 @@ -//! Clean-room Matroska (MKV) and WebM demuxer, written from RFC 8794 (EBML), RFC 9559 (Matroska) -//! and the WebM container guidelines. -//! -//! - [`open`] / [`open_with`] parse the EBML header and the first Segment (Info, Tracks, SeekHead, -//! Cues, Chapters, Tags, Attachments) and, by default, scan every Cluster's block headers to build -//! per-track [`Sample`] tables — the same shape as `deckcraft-isobmff`, so callers can treat both -//! containers alike. Frame data is read on demand with [`MkvFile::read_sample`]. -//! - [`Demuxer`] iterates [`Packet`]s in file order (from any [`ByteSource`], `Read + Seek`, or a -//! byte slice) and seeks to the keyframe preceding a time, via the index, Cues, or a cluster scan. -//! -//! Layer L0: no dependencies beyond `std`, no `unsafe`, builds for `wasm32-unknown-unknown`. +//! Matroska (MKV) and WebM demuxer and muxer: a re-export of [`craft_media::matroska`] from +//! craft-libs. Supported elements and limitations are in the craft-media README. #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))] -mod codec; -mod demux; -pub mod ebml; -mod error; -mod ids; -mod meta; -mod mux; -mod source; -#[cfg(test)] -mod synthetic_tests; -mod track; - -pub use codec::Codec; -pub use demux::{Demuxer, Keyframe, MkvFile, OpenOptions, Packet, SeekPoint, open, open_with}; -pub use error::{Error, Result}; -pub use meta::{ - Attachment, Chapter, ChapterDisplay, ClusterInfo, CuePoint, CuePosition, Edition, SeekEntry, SegmentInfo, SimpleTag, Tag, TagTargets, -}; -pub use mux::{LacingMode, MkvWriter, MuxOptions, TrackSpec}; -pub use source::{ByteSource, ReadSeekSource}; -pub use track::{AudioInfo, Colour, ContentEncoding, MasteringMetadata, Projection, Sample, Track, TrackKind, VideoInfo}; - -/// Element IDs used by the demuxer (marker bits included), for callers inspecting raw elements. -pub mod element_ids { - pub use crate::ids::*; -} +pub use craft_media::matroska::*; diff --git a/crates/matroska/src/meta.rs b/crates/matroska/src/meta.rs deleted file mode 100644 index 698a2cb5..00000000 --- a/crates/matroska/src/meta.rs +++ /dev/null @@ -1,612 +0,0 @@ -//! Segment-level metadata: Info, SeekHead, Cues, Chapters, Tags, Attachments — types and parsers -//! for their in-memory element data. - -use crate::codec::map_codec; -use crate::ebml::{Children, float, int, string, uint}; -use crate::ids::*; -use crate::track::{AudioInfo, Colour, ContentEncoding, MasteringMetadata, Projection, Track, TrackKind, VideoInfo, gcd}; - -/// `Info` element. -#[derive(Clone, Debug, PartialEq)] -pub struct SegmentInfo { - /// Nanoseconds per timestamp tick (default 1 000 000 = 1 ms). - pub timestamp_scale: u64, - /// `Duration` in timestamp ticks (float), if present. - pub duration: Option, - pub title: Option, - pub muxing_app: String, - pub writing_app: String, - /// `DateUTC`: nanoseconds since 2001-01-01T00:00:00 UTC. - pub date_utc: Option, - pub segment_uuid: Option<[u8; 16]>, -} - -impl Default for SegmentInfo { - fn default() -> Self { - Self { - timestamp_scale: 1_000_000, - duration: None, - title: None, - muxing_app: String::new(), - writing_app: String::new(), - date_utc: None, - segment_uuid: None, - } - } -} - -/// One `SeekHead` entry: level-1 element ID and its position relative to the Segment data start. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SeekEntry { - pub id: u32, - pub position: u64, -} - -/// One `CueTrackPositions` of a `CuePoint`. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct CuePosition { - pub track: u64, - /// Cluster position relative to the Segment data start. - pub cluster_position: u64, - /// Block position relative to the Cluster data start. - pub relative_position: Option, - pub block_number: Option, - pub duration: Option, -} - -/// One `CuePoint`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CuePoint { - /// In timestamp ticks. - pub time: u64, - pub positions: Vec, -} - -/// A cluster found while scanning. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ClusterInfo { - /// Absolute offset of the Cluster element header. - pub offset: u64, - /// Absolute offset of the first child. - pub data_start: u64, - /// Absolute end offset (for unknown-size clusters, where the next top-level element starts). - pub end: u64, - /// Cluster `Timestamp` in ticks. - pub timestamp: u64, -} - -/// One `ChapterDisplay`. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct ChapterDisplay { - pub string: String, - pub languages: Vec, - pub languages_bcp47: Vec, - pub countries: Vec, -} - -/// `ChapterAtom` (possibly nested). -#[derive(Clone, Debug, PartialEq)] -pub struct Chapter { - pub uid: u64, - pub string_uid: Option, - pub start_ns: u64, - pub end_ns: Option, - pub hidden: bool, - pub enabled: bool, - pub displays: Vec, - pub children: Vec, -} - -impl Chapter { - /// First display string, if any. - pub fn title(&self) -> Option<&str> { - self.displays.first().map(|d| d.string.as_str()) - } -} - -/// `EditionEntry`. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Edition { - pub uid: Option, - pub hidden: bool, - pub default: bool, - pub ordered: bool, - pub chapters: Vec, -} - -/// `Targets` of a `Tag`. Empty UID lists mean "the whole segment". -#[derive(Clone, Debug, PartialEq)] -pub struct TagTargets { - pub type_value: u64, - pub type_name: Option, - pub track_uids: Vec, - pub edition_uids: Vec, - pub chapter_uids: Vec, - pub attachment_uids: Vec, -} - -impl Default for TagTargets { - fn default() -> Self { - Self { - type_value: 50, - type_name: None, - track_uids: Vec::new(), - edition_uids: Vec::new(), - chapter_uids: Vec::new(), - attachment_uids: Vec::new(), - } - } -} - -/// `SimpleTag` (possibly nested). -#[derive(Clone, Debug, PartialEq)] -pub struct SimpleTag { - pub name: String, - pub language: String, - pub default: bool, - pub string: Option, - pub binary: Option>, - pub children: Vec, -} - -/// `Tag`. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Tag { - pub targets: TagTargets, - pub simple: Vec, -} - -/// `AttachedFile` (listed only; the payload is located by `data_offset`/`data_size`). -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Attachment { - pub uid: u64, - pub name: String, - pub media_type: String, - pub description: Option, - pub data_offset: u64, - pub data_size: u64, -} - -pub(crate) fn parse_info(d: &[u8]) -> SegmentInfo { - let mut info = SegmentInfo::default(); - for e in Children::new(d) { - match e.id { - TIMESTAMP_SCALE => { - let v = uint(e.data); - if v > 0 { - info.timestamp_scale = v; - } - } - DURATION => info.duration = Some(float(e.data)).filter(|v| v.is_finite() && *v >= 0.0), - TITLE => info.title = Some(string(e.data)), - MUXING_APP => info.muxing_app = string(e.data), - WRITING_APP => info.writing_app = string(e.data), - DATE_UTC => info.date_utc = Some(int(e.data)), - SEGMENT_UUID => info.segment_uuid = e.data.try_into().ok(), - _ => {} - } - } - info -} - -pub(crate) fn parse_seekhead(d: &[u8]) -> Vec { - let mut out = Vec::new(); - for s in Children::new(d).filter(|e| e.id == SEEK) { - let (mut id, mut pos) = (None, None); - for e in Children::new(s.data) { - match e.id { - SEEK_ID => id = Some(uint(e.data) as u32), - SEEK_POSITION => pos = Some(uint(e.data)), - _ => {} - } - } - if let (Some(id), Some(position)) = (id, pos) { - out.push(SeekEntry { id, position }); - } - } - out -} - -pub(crate) fn parse_cues(d: &[u8], out: &mut Vec) { - for cp in Children::new(d).filter(|e| e.id == CUE_POINT) { - let mut time = None; - let mut positions = Vec::new(); - for e in Children::new(cp.data) { - match e.id { - CUE_TIME => time = Some(uint(e.data)), - CUE_TRACK_POSITIONS => { - let mut p = CuePosition { track: 0, cluster_position: u64::MAX, relative_position: None, block_number: None, duration: None }; - for c in Children::new(e.data) { - match c.id { - CUE_TRACK => p.track = uint(c.data), - CUE_CLUSTER_POSITION => p.cluster_position = uint(c.data), - CUE_RELATIVE_POSITION => p.relative_position = Some(uint(c.data)), - CUE_BLOCK_NUMBER => p.block_number = Some(uint(c.data)), - CUE_DURATION => p.duration = Some(uint(c.data)), - _ => {} - } - } - if p.track != 0 && p.cluster_position != u64::MAX { - positions.push(p); - } - } - _ => {} - } - } - if let Some(time) = time - && !positions.is_empty() - { - out.push(CuePoint { time, positions }); - } - } -} - -/// Deepest nesting of `ChapterAtom` / `SimpleTag` we follow. Deeper children are ignored so a hostile file cannot overflow the stack. -const MAX_NESTING_DEPTH: usize = 32; - -fn parse_chapter(d: &[u8], depth: usize) -> Chapter { - let mut c = - Chapter { uid: 0, string_uid: None, start_ns: 0, end_ns: None, hidden: false, enabled: true, displays: Vec::new(), children: Vec::new() }; - for e in Children::new(d) { - match e.id { - CHAPTER_UID => c.uid = uint(e.data), - CHAPTER_STRING_UID => c.string_uid = Some(string(e.data)), - CHAPTER_TIME_START => c.start_ns = uint(e.data), - CHAPTER_TIME_END => c.end_ns = Some(uint(e.data)), - CHAPTER_FLAG_HIDDEN => c.hidden = uint(e.data) != 0, - CHAPTER_FLAG_ENABLED => c.enabled = uint(e.data) != 0, - CHAPTER_DISPLAY => { - let mut disp = ChapterDisplay::default(); - for x in Children::new(e.data) { - match x.id { - CHAP_STRING => disp.string = string(x.data), - CHAP_LANGUAGE => disp.languages.push(string(x.data)), - CHAP_LANGUAGE_BCP47 => disp.languages_bcp47.push(string(x.data)), - CHAP_COUNTRY => disp.countries.push(string(x.data)), - _ => {} - } - } - if disp.languages.is_empty() { - disp.languages.push("eng".into()); - } - c.displays.push(disp); - } - CHAPTER_ATOM if depth + 1 < MAX_NESTING_DEPTH => c.children.push(parse_chapter(e.data, depth + 1)), - _ => {} - } - } - c -} - -pub(crate) fn parse_chapters(d: &[u8], out: &mut Vec) { - for ed in Children::new(d).filter(|e| e.id == EDITION_ENTRY) { - let mut edition = Edition::default(); - for e in Children::new(ed.data) { - match e.id { - EDITION_UID => edition.uid = Some(uint(e.data)), - EDITION_FLAG_HIDDEN => edition.hidden = uint(e.data) != 0, - EDITION_FLAG_DEFAULT => edition.default = uint(e.data) != 0, - EDITION_FLAG_ORDERED => edition.ordered = uint(e.data) != 0, - CHAPTER_ATOM => edition.chapters.push(parse_chapter(e.data, 0)), - _ => {} - } - } - out.push(edition); - } -} - -fn parse_simple_tag(d: &[u8], depth: usize) -> SimpleTag { - let mut t = SimpleTag { name: String::new(), language: "und".into(), default: true, string: None, binary: None, children: Vec::new() }; - for e in Children::new(d) { - match e.id { - TAG_NAME => t.name = string(e.data), - TAG_LANGUAGE => t.language = string(e.data), - TAG_DEFAULT => t.default = uint(e.data) != 0, - TAG_STRING => t.string = Some(string(e.data)), - TAG_BINARY => t.binary = Some(e.data.to_vec()), - SIMPLE_TAG if depth + 1 < MAX_NESTING_DEPTH => t.children.push(parse_simple_tag(e.data, depth + 1)), - _ => {} - } - } - t -} - -pub(crate) fn parse_tags(d: &[u8], out: &mut Vec) { - for tg in Children::new(d).filter(|e| e.id == TAG) { - let mut tag = Tag::default(); - for e in Children::new(tg.data) { - match e.id { - TARGETS => { - for x in Children::new(e.data) { - match x.id { - TARGET_TYPE_VALUE => tag.targets.type_value = uint(x.data), - TARGET_TYPE => tag.targets.type_name = Some(string(x.data)), - TAG_TRACK_UID => tag.targets.track_uids.push(uint(x.data)), - TAG_EDITION_UID => tag.targets.edition_uids.push(uint(x.data)), - TAG_CHAPTER_UID => tag.targets.chapter_uids.push(uint(x.data)), - TAG_ATTACHMENT_UID => tag.targets.attachment_uids.push(uint(x.data)), - _ => {} - } - } - } - SIMPLE_TAG => tag.simple.push(parse_simple_tag(e.data, 0)), - _ => {} - } - } - out.push(tag); - } -} - -fn parse_colour(d: &[u8]) -> Colour { - let mut c = Colour::default(); - for e in Children::new(d) { - let v = || Some(uint(e.data)); - match e.id { - MATRIX_COEFFICIENTS => c.matrix_coefficients = v(), - BITS_PER_CHANNEL => c.bits_per_channel = v(), - CHROMA_SUBSAMPLING_HORZ => c.chroma_subsampling_horz = v(), - CHROMA_SUBSAMPLING_VERT => c.chroma_subsampling_vert = v(), - CB_SUBSAMPLING_HORZ => c.cb_subsampling_horz = v(), - CB_SUBSAMPLING_VERT => c.cb_subsampling_vert = v(), - CHROMA_SITING_HORZ => c.chroma_siting_horz = v(), - CHROMA_SITING_VERT => c.chroma_siting_vert = v(), - RANGE => c.range = v(), - TRANSFER_CHARACTERISTICS => c.transfer_characteristics = v(), - PRIMARIES => c.primaries = v(), - MAX_CLL => c.max_cll = v(), - MAX_FALL => c.max_fall = v(), - MASTERING_METADATA => { - let mut m = MasteringMetadata::default(); - for x in Children::new(e.data) { - let f = float(x.data); - match x.id { - PRIMARY_R_X => m.primaries[0].0 = f, - PRIMARY_R_Y => m.primaries[0].1 = f, - PRIMARY_G_X => m.primaries[1].0 = f, - PRIMARY_G_Y => m.primaries[1].1 = f, - PRIMARY_B_X => m.primaries[2].0 = f, - PRIMARY_B_Y => m.primaries[2].1 = f, - WHITE_POINT_X => m.white_point.0 = f, - WHITE_POINT_Y => m.white_point.1 = f, - LUMINANCE_MAX => m.luminance_max = f, - LUMINANCE_MIN => m.luminance_min = f, - _ => {} - } - } - c.mastering = Some(m); - } - _ => {} - } - } - c -} - -fn parse_video(d: &[u8]) -> VideoInfo { - let mut v = VideoInfo::default(); - for e in Children::new(d) { - let u = uint(e.data); - match e.id { - PIXEL_WIDTH => v.pixel_width = u as u32, - PIXEL_HEIGHT => v.pixel_height = u as u32, - DISPLAY_WIDTH => v.display_width = Some(u as u32), - DISPLAY_HEIGHT => v.display_height = Some(u as u32), - DISPLAY_UNIT => v.display_unit = u, - PIXEL_CROP_TOP => v.crop.0 = u as u32, - PIXEL_CROP_BOTTOM => v.crop.1 = u as u32, - PIXEL_CROP_LEFT => v.crop.2 = u as u32, - PIXEL_CROP_RIGHT => v.crop.3 = u as u32, - FLAG_INTERLACED => v.flag_interlaced = u, - FIELD_ORDER => v.field_order = Some(u), - STEREO_MODE => v.stereo_mode = Some(u), - ALPHA_MODE => v.alpha_mode = u, - COLOUR_SPACE => v.colour_space = e.data.try_into().ok(), - COLOUR => v.colour = Some(parse_colour(e.data)), - PROJECTION => { - let mut p = Projection::default(); - for x in Children::new(e.data) { - match x.id { - PROJECTION_TYPE => p.projection_type = uint(x.data), - PROJECTION_POSE_YAW => p.yaw = float(x.data), - PROJECTION_POSE_PITCH => p.pitch = float(x.data), - PROJECTION_POSE_ROLL => p.roll = float(x.data), - _ => {} - } - } - v.projection = Some(p); - } - _ => {} - } - } - v -} - -fn parse_audio(d: &[u8]) -> AudioInfo { - let mut a = AudioInfo::default(); - for e in Children::new(d) { - match e.id { - SAMPLING_FREQUENCY => a.sampling_frequency = float(e.data), - OUTPUT_SAMPLING_FREQUENCY => a.output_sampling_frequency = Some(float(e.data)), - CHANNELS => a.channels = uint(e.data), - BIT_DEPTH => a.bit_depth = Some(uint(e.data)), - _ => {} - } - } - a -} - -fn parse_encodings(d: &[u8]) -> Vec { - let mut list: Vec<(u64, ContentEncoding)> = Vec::new(); - for ce in Children::new(d).filter(|e| e.id == CONTENT_ENCODING) { - let (mut order, mut scope, mut ty) = (0u64, 1u64, 0u64); - let mut comp: Option<(u64, Vec)> = None; - for e in Children::new(ce.data) { - match e.id { - CONTENT_ENCODING_ORDER => order = uint(e.data), - CONTENT_ENCODING_SCOPE => scope = uint(e.data), - CONTENT_ENCODING_TYPE => ty = uint(e.data), - CONTENT_COMPRESSION => { - let (mut algo, mut settings) = (0u64, Vec::new()); - for x in Children::new(e.data) { - match x.id { - CONTENT_COMP_ALGO => algo = uint(x.data), - CONTENT_COMP_SETTINGS => settings = x.data.to_vec(), - _ => {} - } - } - comp = Some((algo, settings)); - } - _ => {} - } - } - let enc = if ty == 1 { - ContentEncoding::Encryption { scope } - } else { - let (algo, settings) = comp.unwrap_or((0, Vec::new())); - if algo == 3 { - ContentEncoding::HeaderStripping { bytes: settings, scope } - } else { - ContentEncoding::Compression { algo, settings, scope } - } - }; - list.push((order, enc)); - } - // highest order is applied last on muxing → first to undo; keep decode order - list.sort_by_key(|e| std::cmp::Reverse(e.0)); - list.into_iter().map(|e| e.1).collect() -} - -pub(crate) fn parse_tracks(d: &[u8], timestamp_scale: u64) -> Vec { - let g = gcd(timestamp_scale, 1_000_000_000); - let timebase = (timestamp_scale / g, 1_000_000_000 / g); - let mut out = Vec::new(); - for te in Children::new(d).filter(|e| e.id == TRACK_ENTRY) { - let mut t = Track { - number: 0, - uid: 0, - kind: TrackKind::Other(0), - enabled: true, - default: true, - forced: false, - hearing_impaired: false, - lacing: true, - default_duration_ns: None, - name: String::new(), - language: "eng".into(), - language_bcp47: None, - codec_id: String::new(), - codec_private: Vec::new(), - codec_name: String::new(), - codec: crate::Codec::Other(String::new()), - codec_delay_ns: 0, - seek_pre_roll_ns: 0, - video: None, - audio: None, - encodings: Vec::new(), - timebase, - samples: Vec::new(), - sync: Vec::new(), - }; - for e in Children::new(te.data) { - match e.id { - TRACK_NUMBER => t.number = uint(e.data), - TRACK_UID => t.uid = uint(e.data), - TRACK_TYPE => t.kind = TrackKind::from_type(uint(e.data)), - FLAG_ENABLED => t.enabled = uint(e.data) != 0, - FLAG_DEFAULT => t.default = uint(e.data) != 0, - FLAG_FORCED => t.forced = uint(e.data) != 0, - FLAG_HEARING_IMPAIRED => t.hearing_impaired = uint(e.data) != 0, - FLAG_LACING => t.lacing = uint(e.data) != 0, - DEFAULT_DURATION => t.default_duration_ns = Some(uint(e.data)).filter(|&v| v > 0), - NAME => t.name = string(e.data), - LANGUAGE => t.language = string(e.data), - LANGUAGE_BCP47 => t.language_bcp47 = Some(string(e.data)), - CODEC_ID => t.codec_id = string(e.data), - CODEC_PRIVATE => t.codec_private = e.data.to_vec(), - CODEC_NAME => t.codec_name = string(e.data), - CODEC_DELAY => t.codec_delay_ns = uint(e.data), - SEEK_PRE_ROLL => t.seek_pre_roll_ns = uint(e.data), - VIDEO => t.video = Some(parse_video(e.data)), - AUDIO => t.audio = Some(parse_audio(e.data)), - CONTENT_ENCODINGS => t.encodings = parse_encodings(e.data), - _ => {} - } - } - if t.number == 0 { - continue; - } - if t.kind == TrackKind::Audio && t.audio.is_none() { - t.audio = Some(AudioInfo::default()); - } - // CodecPrivate may itself be header-stripped (scope bit 2 = private data) - for enc in &t.encodings { - if let ContentEncoding::HeaderStripping { bytes, scope } = enc - && scope & 2 != 0 - { - let mut p = bytes.clone(); - p.extend_from_slice(&t.codec_private); - t.codec_private = p; - } - } - t.codec = map_codec(&t.codec_id, &t.codec_private, t.audio.as_ref()); - out.push(t); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ebml::{write_id, write_size}; - - /// `levels` elements with id `id`, each holding the next; the innermost is empty. - fn nested(id: u32, levels: usize) -> Vec { - let mut inner = Vec::new(); - for _ in 0..levels { - let mut e = Vec::new(); - write_id(&mut e, id); - write_size(&mut e, inner.len() as u64, 1); - e.extend_from_slice(&inner); - inner = e; - } - inner - } - - fn chapter_depth(c: &Chapter) -> usize { - 1 + c.children.iter().map(chapter_depth).max().unwrap_or(0) - } - - fn tag_depth(t: &SimpleTag) -> usize { - 1 + t.children.iter().map(tag_depth).max().unwrap_or(0) - } - - fn chapters_of(levels: usize) -> usize { - let mut edition = Vec::new(); - write_id(&mut edition, EDITION_ENTRY); - let atoms = nested(CHAPTER_ATOM, levels); - write_size(&mut edition, atoms.len() as u64, 1); - edition.extend_from_slice(&atoms); - let mut out = Vec::new(); - parse_chapters(&edition, &mut out); - chapter_depth(&out[0].chapters[0]) - } - - #[test] - fn chapter_nesting_is_bounded() { - assert_eq!(chapters_of(MAX_NESTING_DEPTH), MAX_NESTING_DEPTH); - assert_eq!(chapters_of(MAX_NESTING_DEPTH + 50), MAX_NESTING_DEPTH); - } - - #[test] - fn simple_tag_nesting_is_bounded() { - for (levels, expected) in [(MAX_NESTING_DEPTH, MAX_NESTING_DEPTH), (MAX_NESTING_DEPTH + 50, MAX_NESTING_DEPTH)] { - let mut tag = Vec::new(); - write_id(&mut tag, TAG); - let simple = nested(SIMPLE_TAG, levels); - write_size(&mut tag, simple.len() as u64, 1); - tag.extend_from_slice(&simple); - let mut out = Vec::new(); - parse_tags(&tag, &mut out); - assert_eq!(tag_depth(&out[0].simple[0]), expected); - } - } -} diff --git a/crates/matroska/src/mux.rs b/crates/matroska/src/mux.rs deleted file mode 100644 index 462c155c..00000000 --- a/crates/matroska/src/mux.rs +++ /dev/null @@ -1,539 +0,0 @@ -//! Minimal Matroska / WebM muxer: SimpleBlocks (optionally laced), BlockGroups for explicit -//! durations, Cues, SeekHead, Info with Duration. Output must be `Write + Seek` (sizes and the -//! SeekHead are back-patched on [`MkvWriter::finish`]). - -use std::io::{Seek, SeekFrom, Write}; - -use crate::ebml::{write_id, write_size, write_unknown_size}; -use crate::error::{Error, Result}; -use crate::ids::*; -use crate::track::TrackKind; - -/// Lacing used for audio tracks. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum LacingMode { - None, - Xiph, - Ebml, - /// Fixed-size lacing when consecutive frames have equal sizes, otherwise EBML lacing. - FixedOrEbml, -} - -/// Track description for [`MkvWriter`]. -#[derive(Clone, Debug)] -pub struct TrackSpec { - pub kind: TrackKind, - pub codec_id: String, - pub codec_private: Vec, - pub default_duration_ns: Option, - pub language: Option, - pub name: Option, - /// Pixel width/height for video tracks. - pub video_size: Option<(u32, u32)>, - /// (sampling frequency, channels, bit depth) for audio tracks. - pub audio: Option<(f64, u64, Option)>, - pub codec_delay_ns: u64, - pub seek_pre_roll_ns: u64, -} - -impl TrackSpec { - pub fn new(kind: TrackKind, codec_id: impl Into) -> Self { - Self { - kind, - codec_id: codec_id.into(), - codec_private: Vec::new(), - default_duration_ns: None, - language: None, - name: None, - video_size: None, - audio: None, - codec_delay_ns: 0, - seek_pre_roll_ns: 0, - } - } -} - -/// Muxer options. -#[derive(Clone, Debug)] -pub struct MuxOptions { - /// `webm` or `matroska`. - pub doc_type: String, - /// Nanoseconds per tick (default 1 000 000). - pub timestamp_scale: u64, - /// Start a new cluster at the next video keyframe (or any frame without video) after this long. - pub cluster_duration_ns: u64, - pub lacing: LacingMode, - /// Maximum frames per laced block. - pub max_lace_frames: usize, - pub title: Option, - pub writing_app: String, -} - -impl Default for MuxOptions { - fn default() -> Self { - Self { - doc_type: "matroska".into(), - timestamp_scale: 1_000_000, - cluster_duration_ns: 1_000_000_000, - lacing: LacingMode::None, - max_lace_frames: 8, - title: None, - writing_app: "deckcraft".into(), - } - } -} - -pub(crate) fn el(out: &mut Vec, id: u32, data: &[u8]) { - write_id(out, id); - write_size(out, data.len() as u64, 1); - out.extend_from_slice(data); -} - -pub(crate) fn el_uint(out: &mut Vec, id: u32, v: u64) { - let n = (8 - v.leading_zeros() as usize / 8).max(1); - el(out, id, &v.to_be_bytes()[8 - n..]); -} - -pub(crate) fn el_float(out: &mut Vec, id: u32, v: f64) { - el(out, id, &v.to_be_bytes()); -} - -pub(crate) fn el_str(out: &mut Vec, id: u32, s: &str) { - el(out, id, s.as_bytes()); -} - -fn void(out: &mut Vec, total: usize) { - debug_assert!(total >= 2); - let sl = if total - 2 <= 126 { 1 } else { 8 }; - write_id(out, VOID); - write_size(out, (total - 1 - sl) as u64, sl); - out.resize(out.len() + total - 1 - sl, 0); -} - -#[derive(Clone, Debug)] -struct Pending { - track: usize, - ts: i64, - frames: Vec>, -} - -const SEEK_HEAD_SPACE: usize = 128; - -/// Streaming Matroska/WebM writer. -pub struct MkvWriter { - w: W, - opts: MuxOptions, - tracks: Vec, - segment_size_pos: u64, - segment_data_start: u64, - seek_head_pos: u64, - duration_pos: u64, - info_pos: u64, - tracks_pos: u64, - cluster: Vec, - cluster_ts: Option, - cluster_start_ns: i64, - pending: Option, - cues: Vec<(u64, u64, u64)>, // (time ticks, track number, cluster position) - cued_tracks_in_cluster: Vec, - end_ns: i64, - has_video: bool, -} - -impl MkvWriter { - pub fn new(mut w: W, tracks: Vec, opts: MuxOptions) -> Result { - if tracks.is_empty() || opts.timestamp_scale == 0 { - return Err(Error::Invalid("muxer needs at least one track and a non-zero timestamp scale".into())); - } - let mut out = Vec::new(); - let mut h = Vec::new(); - el_uint(&mut h, EBML_VERSION, 1); - el_uint(&mut h, EBML_READ_VERSION, 1); - el_uint(&mut h, EBML_MAX_ID_LENGTH, 4); - el_uint(&mut h, EBML_MAX_SIZE_LENGTH, 8); - el_str(&mut h, DOC_TYPE, &opts.doc_type); - el_uint(&mut h, DOC_TYPE_VERSION, 4); - el_uint(&mut h, DOC_TYPE_READ_VERSION, 2); - el(&mut out, EBML, &h); - write_id(&mut out, SEGMENT); - let base = w.stream_position()?; - let segment_size_pos = base + out.len() as u64; - write_unknown_size(&mut out, 8); - let segment_data_start = base + out.len() as u64; - let seek_head_pos = segment_data_start; - void(&mut out, SEEK_HEAD_SPACE); - // Info - let info_pos = base + out.len() as u64; - let mut info = Vec::new(); - el_uint(&mut info, TIMESTAMP_SCALE, opts.timestamp_scale); - el_str(&mut info, MUXING_APP, "deckcraft-matroska"); - el_str(&mut info, WRITING_APP, &opts.writing_app); - if let Some(t) = &opts.title { - el_str(&mut info, TITLE, t); - } - let dur_off_in_info = info.len() + 2 + 1; // ID (2) + size (1) - el_float(&mut info, DURATION, 0.0); - write_id(&mut out, INFO); - write_size(&mut out, info.len() as u64, 1); - let duration_pos = base + out.len() as u64 + dur_off_in_info as u64; - out.extend_from_slice(&info); - // Tracks - let tracks_pos = base + out.len() as u64; - let mut tr = Vec::new(); - for (i, t) in tracks.iter().enumerate() { - let mut e = Vec::new(); - el_uint(&mut e, TRACK_NUMBER, i as u64 + 1); - el_uint(&mut e, TRACK_UID, 0x1000 + i as u64 + 1); - let ty = match t.kind { - TrackKind::Video => 1, - TrackKind::Audio => 2, - TrackKind::Subtitle => 0x11, - TrackKind::Complex => 3, - TrackKind::Logo => 0x10, - TrackKind::Buttons => 0x12, - TrackKind::Control => 0x20, - TrackKind::Metadata => 0x21, - TrackKind::Other(n) => n as u64, - }; - el_uint(&mut e, TRACK_TYPE, ty); - el_uint(&mut e, FLAG_LACING, (t.kind == TrackKind::Audio && opts.lacing != LacingMode::None) as u64); - if let Some(l) = &t.language { - el_str(&mut e, LANGUAGE, l); - } - if let Some(n) = &t.name { - el_str(&mut e, NAME, n); - } - el_str(&mut e, CODEC_ID, &t.codec_id); - if !t.codec_private.is_empty() { - el(&mut e, CODEC_PRIVATE, &t.codec_private); - } - if let Some(d) = t.default_duration_ns { - el_uint(&mut e, DEFAULT_DURATION, d); - } - if t.codec_delay_ns > 0 { - el_uint(&mut e, CODEC_DELAY, t.codec_delay_ns); - } - if t.seek_pre_roll_ns > 0 { - el_uint(&mut e, SEEK_PRE_ROLL, t.seek_pre_roll_ns); - } - if let Some((pw, ph)) = t.video_size { - let mut v = Vec::new(); - el_uint(&mut v, PIXEL_WIDTH, pw as u64); - el_uint(&mut v, PIXEL_HEIGHT, ph as u64); - el(&mut e, VIDEO, &v); - } - if let Some((rate, ch, bits)) = t.audio { - let mut a = Vec::new(); - el_float(&mut a, SAMPLING_FREQUENCY, rate); - el_uint(&mut a, CHANNELS, ch); - if let Some(b) = bits { - el_uint(&mut a, BIT_DEPTH, b); - } - el(&mut e, AUDIO, &a); - } - el(&mut tr, TRACK_ENTRY, &e); - } - el(&mut out, TRACKS, &tr); - w.write_all(&out)?; - let has_video = tracks.iter().any(|t| t.kind == TrackKind::Video); - let n = tracks.len(); - Ok(Self { - w, - opts, - tracks, - segment_size_pos, - segment_data_start, - seek_head_pos, - duration_pos, - info_pos, - tracks_pos, - cluster: Vec::new(), - cluster_ts: None, - cluster_start_ns: 0, - pending: None, - cues: Vec::new(), - cued_tracks_in_cluster: vec![false; n], - end_ns: 0, - has_video, - }) - } - - /// Add one frame. `pts_ns` is the presentation time (codec delay is added back for storage); - /// frames must be supplied in the desired file order. `duration_ns` forces a BlockGroup with a - /// BlockDuration when it differs from the track's DefaultDuration. - pub fn write_frame(&mut self, track: usize, pts_ns: i64, keyframe: bool, data: &[u8], duration_ns: Option) -> Result<()> { - let spec = self.tracks.get(track).ok_or(Error::NoSuchTrack(track))?; - // the Matroska ProRes mapping stores frames without the 8-byte `size`+`icpf` header - let data = if spec.codec_id == "V_PRORES" && data.get(4..8) == Some(b"icpf") { &data[8..] } else { data }; - let scale = self.opts.timestamp_scale as i64; - let stored_ns = pts_ns + spec.codec_delay_ns as i64; - if stored_ns < 0 { - return Err(Error::Invalid("negative timestamp".into())); - } - let ts = (stored_ns + scale / 2) / scale; - let dur_end = pts_ns + duration_ns.or(spec.default_duration_ns).unwrap_or(0) as i64; - self.end_ns = self.end_ns.max(dur_end).max(pts_ns); - let is_video = spec.kind == TrackKind::Video; - let lacing = spec.kind == TrackKind::Audio && self.opts.lacing != LacingMode::None; - let explicit_dur = duration_ns.filter(|&d| Some(d) != spec.default_duration_ns); - // new cluster? - let start_new = match self.cluster_ts { - None => true, - Some(cts) => { - let long = stored_ns - self.cluster_start_ns >= self.opts.cluster_duration_ns as i64; - (long && (!self.has_video || (is_video && keyframe))) || ts - cts > 30_000 || ts < cts - } - }; - if start_new { - self.flush_cluster()?; - self.cluster_ts = Some(ts); - self.cluster_start_ns = stored_ns; - el_uint(&mut self.cluster, TIMESTAMP, ts as u64); - } - let cts = self.cluster_ts.unwrap_or(0); - // cue: video keyframes, or the first frame of each track per cluster when there is no video - if (is_video && keyframe) || (!self.has_video && !self.cued_tracks_in_cluster[track]) { - self.cued_tracks_in_cluster[track] = true; - let pos = self.w.stream_position()? - self.segment_data_start; - if self.cues.last().is_none_or(|c| c.2 != pos || c.1 != track as u64 + 1) { - self.cues.push((ts as u64, track as u64 + 1, pos)); - } - } - if lacing && explicit_dur.is_none() { - let same = self.pending.as_ref().is_some_and(|p| p.track == track && p.frames.len() < self.opts.max_lace_frames); - if !same { - self.flush_pending(); - } - match &mut self.pending { - Some(p) => p.frames.push(data.to_vec()), - None => self.pending = Some(Pending { track, ts: ts - cts, frames: vec![data.to_vec()] }), - } - return Ok(()); - } - self.flush_pending(); - let mut block = Vec::new(); - write_size(&mut block, track as u64 + 1, 1); - block.extend_from_slice(&((ts - cts) as i16).to_be_bytes()); - if let Some(d) = explicit_dur { - block.push(0); - block.extend_from_slice(data); - let mut g = Vec::new(); - el(&mut g, BLOCK, &block); - el_uint(&mut g, BLOCK_DURATION, (d as i64 / scale) as u64); - if !keyframe { - el(&mut g, REFERENCE_BLOCK, &[0xFF]); - } - el(&mut self.cluster, BLOCK_GROUP, &g); - } else { - block.push(if keyframe { 0x80 } else { 0 }); - block.extend_from_slice(data); - el(&mut self.cluster, SIMPLE_BLOCK, &block); - } - Ok(()) - } - - fn flush_pending(&mut self) { - let Some(p) = self.pending.take() else { return }; - let mut block = Vec::new(); - write_size(&mut block, p.track as u64 + 1, 1); - block.extend_from_slice(&(p.ts as i16).to_be_bytes()); - let n = p.frames.len(); - let mode = if n == 1 { LacingMode::None } else { self.opts.lacing }; - let fixed = p.frames.iter().all(|f| f.len() == p.frames[0].len()); - match mode { - LacingMode::None => block.push(0x80), - LacingMode::Xiph => { - block.push(0x80 | 0x02); - block.push((n - 1) as u8); - for f in &p.frames[..n - 1] { - let mut s = f.len(); - while s >= 255 { - block.push(255); - s -= 255; - } - block.push(s as u8); - } - } - LacingMode::FixedOrEbml if fixed => { - block.push(0x80 | 0x04); - block.push((n - 1) as u8); - } - LacingMode::Ebml | LacingMode::FixedOrEbml => { - block.push(0x80 | 0x06); - block.push((n - 1) as u8); - write_size(&mut block, p.frames[0].len() as u64, 1); - for i in 1..n - 1 { - let d = p.frames[i].len() as i64 - p.frames[i - 1].len() as i64; - // signed VINT: smallest length whose range holds d - let mut len = 1; - while len < 8 && d.unsigned_abs() >= (1u64 << (7 * len - 1)) - 1 { - len += 1; - } - let v = (d + (1i64 << (7 * len - 1)) - 1) as u64 | (1u64 << (7 * len)); - block.extend_from_slice(&v.to_be_bytes()[8 - len..]); - } - } - } - for f in &p.frames { - block.extend_from_slice(f); - } - el(&mut self.cluster, SIMPLE_BLOCK, &block); - } - - fn flush_cluster(&mut self) -> Result<()> { - self.flush_pending(); - if self.cluster_ts.is_some() { - let mut out = Vec::new(); - el(&mut out, CLUSTER, &self.cluster); - self.w.write_all(&out)?; - } - self.cluster.clear(); - self.cluster_ts = None; - self.cued_tracks_in_cluster.iter_mut().for_each(|c| *c = false); - Ok(()) - } - - /// Write Cues, back-patch SeekHead / Segment size / Duration, and return the writer. - pub fn finish(mut self) -> Result { - self.flush_cluster()?; - let cues_pos = self.w.stream_position()?; - let mut cues = Vec::new(); - for &(time, track, pos) in &self.cues { - let mut ctp = Vec::new(); - el_uint(&mut ctp, CUE_TRACK, track); - el_uint(&mut ctp, CUE_CLUSTER_POSITION, pos); - let mut cp = Vec::new(); - el_uint(&mut cp, CUE_TIME, time); - el(&mut cp, CUE_TRACK_POSITIONS, &ctp); - el(&mut cues, CUE_POINT, &cp); - } - let has_cues = !cues.is_empty(); - if has_cues { - let mut out = Vec::new(); - el(&mut out, CUES, &cues); - self.w.write_all(&out)?; - } - let end = self.w.stream_position()?; - // SeekHead - let mut sh = Vec::new(); - let mut entries = vec![(INFO, self.info_pos), (TRACKS, self.tracks_pos)]; - if has_cues { - entries.push((CUES, cues_pos)); - } - for (id, pos) in entries { - let mut s = Vec::new(); - let mut idb = Vec::new(); - write_id(&mut idb, id); - el(&mut s, SEEK_ID, &idb); - el_uint(&mut s, SEEK_POSITION, pos - self.segment_data_start); - el(&mut sh, SEEK, &s); - } - let mut out = Vec::new(); - el(&mut out, SEEK_HEAD, &sh); - if out.len() + 2 > SEEK_HEAD_SPACE { - return Err(Error::Invalid("SeekHead does not fit its reserved space".into())); - } - let rest = SEEK_HEAD_SPACE - out.len(); - void(&mut out, rest); - self.w.seek(SeekFrom::Start(self.seek_head_pos))?; - self.w.write_all(&out)?; - // Duration - let dur = self.end_ns as f64 / self.opts.timestamp_scale as f64; - self.w.seek(SeekFrom::Start(self.duration_pos))?; - self.w.write_all(&dur.to_be_bytes())?; - // Segment size - let mut sz = Vec::new(); - write_size(&mut sz, end - self.segment_data_start, 8); - self.w.seek(SeekFrom::Start(self.segment_size_pos))?; - self.w.write_all(&sz)?; - self.w.seek(SeekFrom::Start(end))?; - self.w.flush()?; - Ok(self.w) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Demuxer, TrackKind}; - use std::io::Cursor; - - fn roundtrip(lacing: LacingMode) { - let mut v = TrackSpec::new(TrackKind::Video, "V_MJPEG"); - v.video_size = Some((16, 16)); - v.default_duration_ns = Some(40_000_000); - let mut a = TrackSpec::new(TrackKind::Audio, "A_PCM/INT/LIT"); - a.audio = Some((48000.0, 1, Some(16))); - a.default_duration_ns = Some(10_000_000); - let mut s = TrackSpec::new(TrackKind::Subtitle, "S_TEXT/UTF8"); - s.language = Some("fre".into()); - let opts = MuxOptions { lacing, ..Default::default() }; - let mut m = MkvWriter::new(Cursor::new(Vec::new()), vec![v, a, s], opts).unwrap(); - let mut expect = Vec::new(); - for i in 0..50i64 { - let vf = vec![i as u8; 100 + i as usize]; - m.write_frame(0, i * 40_000_000, i % 10 == 0, &vf, None).unwrap(); - expect.push((0usize, i * 40, vf, i % 10 == 0)); - for k in 0..4 { - let n = if lacing == LacingMode::FixedOrEbml { 960 } else { 900 + (i as usize * 7 + k * 13) % 300 }; - let af = vec![(i * 4 + k as i64) as u8; n]; - let t = i * 40 + k as i64 * 10; - m.write_frame(1, t * 1_000_000, true, &af, None).unwrap(); - expect.push((1, t, af, true)); - } - if i % 20 == 5 { - m.write_frame(2, i * 40_000_000, true, b"hello", Some(500_000_000)).unwrap(); - expect.push((2, i * 40, b"hello".to_vec(), true)); - } - } - let bytes = m.finish().unwrap().into_inner(); - let mut d = Demuxer::from_slice(&bytes).unwrap(); - assert!(d.file().warnings.is_empty(), "{:?}", d.file().warnings); - assert_eq!(d.tracks()[2].language, "fre"); - assert!(!d.file().cues.is_empty()); - assert_eq!(d.duration_ns(), Some(2_300_000_000)); - let got: Vec<_> = d.by_ref().map(|p| p.unwrap()).collect(); - assert_eq!(got.len(), expect.len()); - for (g, e) in got.iter().zip(&expect) { - assert_eq!((g.track, g.pts, &g.data, g.keyframe), (e.0, e.1, &e.2, e.3)); - } - let sub = got.iter().find(|p| p.track == 2).unwrap(); - assert_eq!(sub.duration, 500); - // the index agrees with the packet stream - let f = d.file(); - for (ti, t) in f.tracks.iter().enumerate() { - let pk: Vec<_> = got.iter().filter(|p| p.track == ti).collect(); - assert_eq!(t.samples.len(), pk.len()); - for (i, p) in pk.iter().enumerate() { - assert_eq!(f.read_sample(&bytes[..], ti, i).unwrap(), p.data); - assert_eq!(t.samples[i].pts, p.pts); - } - } - // seeking - let sp = d.seek(0, 1_230_000_000).unwrap(); - assert_eq!(sp.pts, 1200); - let p = d.next_packet().unwrap().unwrap(); - assert_eq!((p.track, p.pts, p.keyframe), (0, 1200, true)); - let sp = d.seek(1, 1_235_000_000).unwrap(); - assert_eq!(sp.pts, 1230); - let p = d.next_packet().unwrap().unwrap(); - assert_eq!((p.track, p.pts), (1, 1230)); - } - - #[test] - fn roundtrip_unlaced() { - roundtrip(LacingMode::None); - } - #[test] - fn roundtrip_xiph() { - roundtrip(LacingMode::Xiph); - } - #[test] - fn roundtrip_ebml() { - roundtrip(LacingMode::Ebml); - } - #[test] - fn roundtrip_fixed() { - roundtrip(LacingMode::FixedOrEbml); - } -} diff --git a/crates/matroska/src/source.rs b/crates/matroska/src/source.rs deleted file mode 100644 index 41aabe7c..00000000 --- a/crates/matroska/src/source.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! Random-access byte sources for the demuxer. - -use std::cell::RefCell; -use std::io::{self, Read, Seek, SeekFrom}; -use std::sync::Arc; - -/// Random-access, read-only byte source (file, in-memory buffer, web Blob…). -/// -/// Same shape as `deckcraft_isobmff::ByteSource`, so one adapter can serve both demuxers. -pub trait ByteSource { - /// Total length in bytes. - fn len(&self) -> u64; - /// Fill `buf` entirely with bytes starting at `offset`; fail with `UnexpectedEof` if past the end. - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()>; - /// True if the source holds no bytes. - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -fn eof() -> io::Error { - io::Error::new(io::ErrorKind::UnexpectedEof, "read past end of byte source") -} - -impl ByteSource for [u8] { - fn len(&self) -> u64 { - <[u8]>::len(self) as u64 - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - let start = usize::try_from(offset).map_err(|_| eof())?; - let end = start.checked_add(buf.len()).ok_or_else(eof)?; - let src = self.get(start..end).ok_or_else(eof)?; - buf.copy_from_slice(src); - Ok(()) - } -} - -impl ByteSource for Vec { - fn len(&self) -> u64 { - self.as_slice().len() as u64 - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - self.as_slice().read_at(offset, buf) - } -} - -impl ByteSource for &T { - fn len(&self) -> u64 { - (**self).len() - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - (**self).read_at(offset, buf) - } -} - -impl ByteSource for Arc { - fn len(&self) -> u64 { - (**self).len() - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - (**self).read_at(offset, buf) - } -} - -impl ByteSource for Box { - fn len(&self) -> u64 { - (**self).len() - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - (**self).read_at(offset, buf) - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl ByteSource for std::fs::File { - fn len(&self) -> u64 { - self.metadata().map(|m| m.len()).unwrap_or(0) - } - #[cfg(unix)] - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - std::os::unix::fs::FileExt::read_exact_at(self, buf, offset) - } - #[cfg(windows)] - fn read_at(&self, mut offset: u64, mut buf: &mut [u8]) -> io::Result<()> { - use std::os::windows::fs::FileExt; - while !buf.is_empty() { - let n = self.seek_read(buf, offset)?; - if n == 0 { - return Err(eof()); - } - offset += n as u64; - buf = &mut buf[n..]; - } - Ok(()) - } - #[cfg(not(any(unix, windows)))] - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - let mut f = self; - f.seek(SeekFrom::Start(offset))?; - f.read_exact(buf) - } -} - -/// Adapts any `Read + Seek` (e.g. `BufReader`, `Cursor>`) into a [`ByteSource`]. -/// -/// The length is taken once at construction; the stream position is tracked so sequential reads -/// do not issue redundant seeks. -pub struct ReadSeekSource { - inner: RefCell<(R, u64)>, - len: u64, -} - -impl ReadSeekSource { - /// Wrap a reader; seeks to the end once to learn the length. - pub fn new(mut r: R) -> io::Result { - let len = r.seek(SeekFrom::End(0))?; - Ok(Self { inner: RefCell::new((r, len)), len }) - } - /// Unwrap the reader. - pub fn into_inner(self) -> R { - self.inner.into_inner().0 - } -} - -impl ByteSource for ReadSeekSource { - fn len(&self) -> u64 { - self.len - } - fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> { - if offset.checked_add(buf.len() as u64).is_none_or(|e| e > self.len) { - return Err(eof()); - } - let mut g = self.inner.borrow_mut(); - let (r, pos) = &mut *g; - if *pos != offset { - r.seek(SeekFrom::Start(offset))?; - *pos = offset; - } - let res = r.read_exact(buf); - match res { - Ok(()) => { - *pos = offset + buf.len() as u64; - Ok(()) - } - Err(e) => { - *pos = u64::MAX; // unknown; force a seek next time - Err(e) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn slice_and_reader_sources() { - let v = vec![1u8, 2, 3, 4]; - let mut b = [0u8; 2]; - v.read_at(1, &mut b).unwrap(); - assert_eq!(b, [2, 3]); - assert!(v.read_at(3, &mut b).is_err()); - assert!(v.read_at(u64::MAX, &mut b).is_err()); - let r = ReadSeekSource::new(io::Cursor::new(v.clone())).unwrap(); - assert_eq!(r.len(), 4); - r.read_at(2, &mut b).unwrap(); - assert_eq!(b, [3, 4]); - r.read_at(0, &mut b).unwrap(); - assert_eq!(b, [1, 2]); - assert!(r.read_at(3, &mut b).is_err()); - } -} diff --git a/crates/matroska/src/synthetic_tests.rs b/crates/matroska/src/synthetic_tests.rs deleted file mode 100644 index 0ed86e4c..00000000 --- a/crates/matroska/src/synthetic_tests.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Hand-built files exercising features FFmpeg does not write: unknown-size Segment/Clusters, -//! header stripping, BlockGroups, Colour + mastering metadata, Chapters, Tags, Attachments, CRC-32. - -use crate::codec::Codec; -use crate::ebml::{self, write_id, write_unknown_size}; -use crate::ids::*; -use crate::mux::{el, el_float, el_str, el_uint}; -use crate::track::ContentEncoding; -use crate::{Demuxer, OpenOptions, Packet, open_with}; - -fn block(track: u8, rel: i16, flags: u8, data: &[u8]) -> Vec { - let mut b = vec![0x80 | track]; - b.extend_from_slice(&rel.to_be_bytes()); - b.push(flags); - b.extend_from_slice(data); - b -} - -fn synthetic() -> Vec { - let mut f = Vec::new(); - let mut h = Vec::new(); - el_str(&mut h, DOC_TYPE, "matroska"); - el_uint(&mut h, DOC_TYPE_VERSION, 4); - el(&mut f, EBML, &h); - write_id(&mut f, SEGMENT); - write_unknown_size(&mut f, 8); - // Info with a correct CRC-32 and a Void - let mut info = Vec::new(); - el_uint(&mut info, TIMESTAMP_SCALE, 1_000_000); - el_str(&mut info, TITLE, "Synthetic"); - el(&mut info, VOID, &[0; 3]); - let mut crc_info = Vec::new(); - el(&mut crc_info, CRC32, &ebml::crc32(&info).to_le_bytes()); - crc_info.extend_from_slice(&info); - el(&mut f, INFO, &crc_info); - // Tracks - let mut v = Vec::new(); - el_uint(&mut v, TRACK_NUMBER, 1); - el_uint(&mut v, TRACK_UID, 11); - el_uint(&mut v, TRACK_TYPE, 1); - el_str(&mut v, CODEC_ID, "V_MPEG4/ISO/AVC"); - el(&mut v, CODEC_PRIVATE, &[1, 0x64, 0, 0x1F]); - el_uint(&mut v, DEFAULT_DURATION, 40_000_000); - let mut mm = Vec::new(); - for (id, val) in - [(PRIMARY_R_X, 0.708), (PRIMARY_R_Y, 0.292), (PRIMARY_G_X, 0.17), (PRIMARY_G_Y, 0.797), (PRIMARY_B_X, 0.131), (PRIMARY_B_Y, 0.046)] - { - el_float(&mut mm, id, val); - } - el_float(&mut mm, WHITE_POINT_X, 0.3127); - el_float(&mut mm, WHITE_POINT_Y, 0.329); - el_float(&mut mm, LUMINANCE_MAX, 1000.0); - el_float(&mut mm, LUMINANCE_MIN, 0.0001); - let mut col = Vec::new(); - el_uint(&mut col, MATRIX_COEFFICIENTS, 9); - el_uint(&mut col, RANGE, 2); - el_uint(&mut col, TRANSFER_CHARACTERISTICS, 16); - el_uint(&mut col, PRIMARIES, 9); - el_uint(&mut col, MAX_CLL, 1000); - el_uint(&mut col, MAX_FALL, 400); - el(&mut col, MASTERING_METADATA, &mm); - let mut vid = Vec::new(); - el_uint(&mut vid, PIXEL_WIDTH, 1440); - el_uint(&mut vid, PIXEL_HEIGHT, 1080); - el_uint(&mut vid, DISPLAY_WIDTH, 1920); - el_uint(&mut vid, DISPLAY_HEIGHT, 1080); - el(&mut vid, COLOUR, &col); - // rectangular, a quarter turn clockwise (portrait phone video) - let mut proj = Vec::new(); - el_uint(&mut proj, PROJECTION_TYPE, 0); - el_float(&mut proj, PROJECTION_POSE_ROLL, -90.0); - el(&mut vid, PROJECTION, &proj); - el(&mut v, VIDEO, &vid); - // header stripping: every frame starts with 00 00 00 01 - let mut comp = Vec::new(); - el_uint(&mut comp, CONTENT_COMP_ALGO, 3); - el(&mut comp, CONTENT_COMP_SETTINGS, &[0, 0, 0, 1]); - let mut enc = Vec::new(); - el(&mut enc, CONTENT_COMPRESSION, &comp); - let mut encs = Vec::new(); - el(&mut encs, CONTENT_ENCODING, &enc); - el(&mut v, CONTENT_ENCODINGS, &encs); - let mut a = Vec::new(); - el_uint(&mut a, TRACK_NUMBER, 2); - el_uint(&mut a, TRACK_TYPE, 2); - el_str(&mut a, CODEC_ID, "A_AAC/MPEG4/LC/SBR"); - el_str(&mut a, LANGUAGE, "ger"); - let mut au = Vec::new(); - el_float(&mut au, SAMPLING_FREQUENCY, 24000.0); - el_float(&mut au, OUTPUT_SAMPLING_FREQUENCY, 48000.0); - el_uint(&mut au, CHANNELS, 2); - el(&mut a, AUDIO, &au); - let mut tr = Vec::new(); - el(&mut tr, TRACK_ENTRY, &v); - el(&mut tr, TRACK_ENTRY, &a); - el(&mut f, TRACKS, &tr); - // Chapters - let mut disp = Vec::new(); - el_str(&mut disp, CHAP_STRING, "Intro"); - let mut atom = Vec::new(); - el_uint(&mut atom, CHAPTER_UID, 5); - el_uint(&mut atom, CHAPTER_TIME_START, 0); - el_uint(&mut atom, CHAPTER_TIME_END, 40_000_000); - el(&mut atom, CHAPTER_DISPLAY, &disp); - let mut ed = Vec::new(); - el(&mut ed, CHAPTER_ATOM, &atom); - let mut ch = Vec::new(); - el(&mut ch, EDITION_ENTRY, &ed); - el(&mut f, CHAPTERS, &ch); - // Attachments - let mut af = Vec::new(); - el_str(&mut af, FILE_NAME, "font.ttf"); - el_str(&mut af, FILE_MEDIA_TYPE, "font/ttf"); - el_uint(&mut af, FILE_UID, 77); - el(&mut af, FILE_DATA, &[9; 100]); - let mut att = Vec::new(); - el(&mut att, ATTACHED_FILE, &af); - el(&mut f, ATTACHMENTS, &att); - // Cluster 1 (unknown size): keyframe SimpleBlock, BlockGroup with ReferenceBlock + duration - write_id(&mut f, CLUSTER); - write_unknown_size(&mut f, 1); - el_uint(&mut f, TIMESTAMP, 1000); - el(&mut f, SIMPLE_BLOCK, &block(1, 0, 0x80, &[0x65, 1, 2])); - el(&mut f, SIMPLE_BLOCK, &block(2, 5, 0x80, &[0xAA; 10])); - let mut g = Vec::new(); - el(&mut g, BLOCK, &block(1, 40, 0, &[0x41, 3])); - el(&mut g, REFERENCE_BLOCK, &[0xD8]); - el_uint(&mut g, BLOCK_DURATION, 80); - el(&mut f, BLOCK_GROUP, &g); - el(&mut f, SIMPLE_BLOCK, &block(9, 0, 0x80, &[1])); // unknown track: ignored - // Cluster 2 (unknown size), ended by Tags - write_id(&mut f, CLUSTER); - write_unknown_size(&mut f, 8); - el_uint(&mut f, TIMESTAMP, 1120); - el(&mut f, SIMPLE_BLOCK, &block(1, 0, 0x80, &[0x65, 4])); - let mut st = Vec::new(); - el_str(&mut st, TAG_NAME, "ARTIST"); - el_str(&mut st, TAG_STRING, "Nobody"); - let mut tg = Vec::new(); - let mut tgt = Vec::new(); - el_uint(&mut tgt, TAG_TRACK_UID, 11); - el(&mut tg, TARGETS, &tgt); - el(&mut tg, SIMPLE_TAG, &st); - let mut tags = Vec::new(); - el(&mut tags, TAG, &tg); - el(&mut f, TAGS, &tags); - f -} - -#[test] -fn synthetic_file() { - let bytes = synthetic(); - let f = open_with(&bytes[..], &OpenOptions { index: true, verify_crc: true }).unwrap(); - assert!(f.live); - assert!(f.warnings.is_empty(), "{:?}", f.warnings); - assert_eq!((f.crc_checked, f.crc_errors.len()), (1, 0)); - assert_eq!(f.info.title.as_deref(), Some("Synthetic")); - let v = &f.tracks[0]; - assert!(matches!(&v.codec, Codec::Avc { avcc } if avcc[1] == 0x64)); - let vi = v.video.as_ref().unwrap(); - assert_eq!(vi.pixel_aspect(), (4, 3)); - assert_eq!(vi.projection.as_ref().map(|p| p.roll), Some(-90.0)); - assert_eq!(vi.display_rotation(), Some(1)); - let c = vi.colour.as_ref().unwrap(); - assert!(c.full_range()); - assert_eq!( - (c.matrix_coefficients, c.transfer_characteristics, c.primaries, c.max_cll, c.max_fall), - (Some(9), Some(16), Some(9), Some(1000), Some(400)) - ); - let m = c.mastering.as_ref().unwrap(); - assert_eq!(m.primaries[1], (0.17, 0.797)); - assert_eq!(m.white_point, (0.3127, 0.329)); - assert_eq!((m.luminance_max, m.luminance_min), (1000.0, 0.0001)); - assert_eq!(v.encodings, vec![ContentEncoding::HeaderStripping { bytes: vec![0, 0, 0, 1], scope: 1 }]); - // legacy AAC ID: ASC synthesised with explicit SBR signalling (LC, 24 kHz, stereo → 48 kHz) - assert_eq!(f.tracks[1].codec, Codec::Aac { asc: vec![0x13, 0x10, 0x56, 0xE5, 0x98] }); - assert_eq!(f.tracks[1].language, "ger"); - assert_eq!(f.chapters[0].chapters[0].title(), Some("Intro")); - assert_eq!(f.chapters[0].chapters[0].end_ns, Some(40_000_000)); - assert_eq!(f.attachments.len(), 1); - let at = &f.attachments[0]; - assert_eq!((at.name.as_str(), at.media_type.as_str(), at.uid), ("font.ttf", "font/ttf", 77)); - assert_eq!(&bytes[at.data_offset as usize..][..100], &[9; 100]); - assert_eq!(f.tags[0].targets.track_uids, vec![11]); - assert_eq!(f.tags[0].simple[0].string.as_deref(), Some("Nobody")); - assert_eq!(f.clusters.len(), 2); - assert_eq!(v.samples.len(), 3); - assert_eq!(v.keyframes(), &[0, 2]); - - let d = Demuxer::from_slice(&bytes).unwrap(); - let p: Vec = d.map(|p| p.unwrap()).collect(); - assert_eq!(p.len(), 4); - assert_eq!((p[0].track, p[0].pts, p[0].keyframe, p[0].duration), (0, 1000, true, 40)); - assert_eq!(p[0].data, [0, 0, 0, 1, 0x65, 1, 2]); - assert_eq!((p[1].track, p[1].pts, p[1].data.len()), (1, 1005, 10)); - assert_eq!((p[2].pts, p[2].keyframe, p[2].duration), (1040, false, 80)); - assert_eq!(p[2].data, [0, 0, 0, 1, 0x41, 3]); - assert_eq!((p[3].pts, p[3].pts_ns), (1120, 1_120_000_000)); - assert_eq!(f.read_sample(&bytes[..], 0, 1).unwrap(), p[2].data); - - // CRC mismatch is reported, not fatal - let mut bad = bytes.clone(); - let title = bad.windows(9).position(|w| w == b"Synthetic").unwrap(); - bad[title] = b's'; - let f = open_with(&bad[..], &OpenOptions { index: true, verify_crc: true }).unwrap(); - assert_eq!(f.crc_errors.len(), 1); - - // seeking without index or cues builds the index - let mut d = Demuxer::with_options(&bytes[..], OpenOptions { index: false, verify_crc: false }).unwrap(); - assert_eq!(d.file().tracks.len(), 2); - assert_eq!(d.seek(0, 1_100_000_000).unwrap().pts, 1000); - assert_eq!(d.seek(0, 1_130_000_000).unwrap().pts, 1120); - assert_eq!(d.next_packet().unwrap().unwrap().pts, 1120); - assert!(d.file().indexed); -} - -fn one_block(ts: u64, rel: i16) -> Vec { - let mut f = Vec::new(); - let mut h = Vec::new(); - el_str(&mut h, DOC_TYPE, "matroska"); - el(&mut f, EBML, &h); - write_id(&mut f, SEGMENT); - write_unknown_size(&mut f, 8); - let mut info = Vec::new(); - el_uint(&mut info, TIMESTAMP_SCALE, 1_000_000); - el(&mut f, INFO, &info); - let mut v = Vec::new(); - el_uint(&mut v, TRACK_NUMBER, 1); - el_uint(&mut v, TRACK_UID, 1); - el_uint(&mut v, TRACK_TYPE, 1); - el_str(&mut v, CODEC_ID, "V_UNCOMPRESSED"); - let mut tr = Vec::new(); - el(&mut tr, TRACK_ENTRY, &v); - el(&mut f, TRACKS, &tr); - write_id(&mut f, CLUSTER); - write_unknown_size(&mut f, 8); - el_uint(&mut f, TIMESTAMP, ts); - el(&mut f, SIMPLE_BLOCK, &block(1, rel, 0x80, &[0])); - f -} - -#[test] -fn block_timestamp_addition_does_not_overflow() { - let bytes = one_block(1000, 5); - let d = Demuxer::from_slice(&bytes).unwrap(); - let p: Vec = d.map(|p| p.unwrap()).collect(); - assert_eq!(p[0].pts, 1005); - let edge = one_block(i64::MAX as u64, 0); - let d = Demuxer::from_slice(&edge).unwrap(); - let p: Vec = d.map(|p| p.unwrap()).collect(); - assert_eq!(p[0].pts, i64::MAX); - assert!(Demuxer::from_slice(&one_block(i64::MAX as u64, 1)).is_err()); - assert!(Demuxer::from_slice(&one_block(u64::MAX, 0)).is_err()); -} diff --git a/crates/matroska/src/track.rs b/crates/matroska/src/track.rs deleted file mode 100644 index 4317d9a9..00000000 --- a/crates/matroska/src/track.rs +++ /dev/null @@ -1,348 +0,0 @@ -//! Track descriptions (RFC 9559 §5.1.4). - -use crate::codec::Codec; - -/// Track type (`TrackType`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TrackKind { - Video, - Audio, - Complex, - Logo, - Subtitle, - Buttons, - Control, - Metadata, - Other(u8), -} - -impl TrackKind { - pub(crate) fn from_type(t: u64) -> TrackKind { - match t { - 1 => TrackKind::Video, - 2 => TrackKind::Audio, - 3 => TrackKind::Complex, - 0x10 => TrackKind::Logo, - 0x11 => TrackKind::Subtitle, - 0x12 => TrackKind::Buttons, - 0x20 => TrackKind::Control, - 0x21 => TrackKind::Metadata, - n => TrackKind::Other(n as u8), - } - } -} - -/// SMPTE ST 2086 mastering display metadata (`MasteringMetadata`). Chromaticities are CIE 1931 xy, -/// luminance in cd/m². -#[derive(Clone, Debug, Default, PartialEq)] -pub struct MasteringMetadata { - /// Red, green, blue primaries as (x, y). - pub primaries: [(f64, f64); 3], - pub white_point: (f64, f64), - pub luminance_max: f64, - pub luminance_min: f64, -} - -/// `Colour` element. Code points use ITU-T H.273 values (as Matroska does). -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Colour { - /// `MatrixCoefficients` (H.273; 2 = unspecified). - pub matrix_coefficients: Option, - pub bits_per_channel: Option, - pub chroma_subsampling_horz: Option, - pub chroma_subsampling_vert: Option, - pub cb_subsampling_horz: Option, - pub cb_subsampling_vert: Option, - pub chroma_siting_horz: Option, - pub chroma_siting_vert: Option, - /// `Range`: 0 unspecified, 1 broadcast (limited), 2 full, 3 defined by matrix/transfer. - pub range: Option, - /// `TransferCharacteristics` (H.273). - pub transfer_characteristics: Option, - /// `Primaries` (H.273). - pub primaries: Option, - /// Maximum content light level (cd/m²). - pub max_cll: Option, - /// Maximum frame-average light level (cd/m²). - pub max_fall: Option, - pub mastering: Option, -} - -impl Colour { - /// True if `Range` says full range. - pub fn full_range(&self) -> bool { - self.range == Some(2) - } -} - -/// `Video` element. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct VideoInfo { - pub pixel_width: u32, - pub pixel_height: u32, - /// `DisplayWidth`/`DisplayHeight`, in `display_unit` (0 = pixels, 3 = display aspect ratio). - pub display_width: Option, - pub display_height: Option, - pub display_unit: u64, - /// Crop (top, bottom, left, right) in pixels. - pub crop: (u32, u32, u32, u32), - /// `FlagInterlaced`: 0 undetermined, 1 interlaced, 2 progressive. - pub flag_interlaced: u64, - /// `FieldOrder` (0 progressive, 1 tff, 2 undetermined, 6 bff, 9/14 interleaved). - pub field_order: Option, - pub stereo_mode: Option, - /// `AlphaMode`: 1 if BlockAdditions carry alpha. - pub alpha_mode: u64, - /// `ColourSpace` FourCC (uncompressed video). - pub colour_space: Option<[u8; 4]>, - pub colour: Option, - pub projection: Option, -} - -/// `Projection` element (RFC 9559 §5.1.4.1.28.41): pose angles in degrees. -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Projection { - /// 0 rectangular, 1 equirectangular, 2 cubemap, 3 mesh. - pub projection_type: u64, - /// Clockwise around the up vector. - pub yaw: f64, - /// Counter-clockwise around the right vector. - pub pitch: f64, - /// Counter-clockwise around the forward vector. - pub roll: f64, -} - -impl VideoInfo { - /// Clockwise quarter turns (0–3) to display a rectangular video, from `ProjectionPoseRoll` - /// (a counter-clockwise angle, so −90 is a quarter turn clockwise). None for other - /// projections, flips (non-zero yaw / pitch) or angles that are not a multiple of 90°. - pub fn display_rotation(&self) -> Option { - let Some(p) = &self.projection else { return Some(0) }; - if p.projection_type != 0 || p.yaw != 0.0 || p.pitch != 0.0 || p.roll % 90.0 != 0.0 { - return None; - } - Some((-(p.roll / 90.0) as i64).rem_euclid(4) as u8) - } - - /// Pixel aspect ratio derived from the display size (pixels or aspect-ratio units), reduced. - pub fn pixel_aspect(&self) -> (u32, u32) { - let (pw, ph) = - (self.pixel_width.saturating_sub(self.crop.2 + self.crop.3) as u64, self.pixel_height.saturating_sub(self.crop.0 + self.crop.1) as u64); - let (Some(dw), Some(dh)) = (self.display_width, self.display_height) else { return (1, 1) }; - if pw == 0 || ph == 0 || dw == 0 || dh == 0 || !matches!(self.display_unit, 0 | 3) { - return (1, 1); - } - // PAR = (dw/dh) / (pw/ph) - let (n, d) = (dw as u64 * ph, dh as u64 * pw); - let g = gcd(n, d); - ((n / g) as u32, (d / g) as u32) - } -} - -pub(crate) fn gcd(mut a: u64, mut b: u64) -> u64 { - while b != 0 { - (a, b) = (b, a % b); - } - a.max(1) -} - -/// `Audio` element. -#[derive(Clone, Debug, PartialEq)] -pub struct AudioInfo { - /// `SamplingFrequency` (Hz). - pub sampling_frequency: f64, - /// `OutputSamplingFrequency` (e.g. SBR output rate). - pub output_sampling_frequency: Option, - pub channels: u64, - pub bit_depth: Option, -} - -impl Default for AudioInfo { - fn default() -> Self { - Self { sampling_frequency: 8000.0, output_sampling_frequency: None, channels: 1, bit_depth: None } - } -} - -/// One `ContentEncoding`. -#[derive(Clone, Debug, PartialEq)] -pub enum ContentEncoding { - /// `ContentCompAlgo` 3: the given bytes were stripped from the start of every frame. - HeaderStripping { bytes: Vec, scope: u64 }, - /// Other compression (0 zlib, 1 bzlib, 2 lzo1x): not supported for reading frame data. - Compression { algo: u64, settings: Vec, scope: u64 }, - /// Encryption (not supported for reading frame data). - Encryption { scope: u64 }, -} - -/// One entry of a track's sample (frame) table. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Sample { - /// Byte offset of the (stored) frame data in the file. - pub offset: u64, - /// Stored size (excluding header-stripped bytes; see [`Track::sample_size`]). - pub size: u32, - /// Presentation timestamp in the track timebase (`TimestampScale` ticks). - pub pts: i64, - /// Duration in timebase ticks (from `BlockDuration` or `DefaultDuration`), 0 if unknown. - pub duration: u64, - pub keyframe: bool, - /// Index of the owning cluster in [`crate::MkvFile::clusters`]. - pub cluster: u32, - /// Offset of the block element (SimpleBlock / BlockGroup) header. - pub block_offset: u64, - /// Index of this frame within a laced block. - pub lace: u16, -} - -/// A Matroska track (`TrackEntry`) plus, once indexed, its sample table. -#[derive(Clone, Debug)] -pub struct Track { - pub number: u64, - pub uid: u64, - pub kind: TrackKind, - pub enabled: bool, - pub default: bool, - pub forced: bool, - pub hearing_impaired: bool, - pub lacing: bool, - /// `DefaultDuration` in nanoseconds. - pub default_duration_ns: Option, - pub name: String, - /// `Language` (ISO 639-2, default `eng`). - pub language: String, - pub language_bcp47: Option, - pub codec_id: String, - pub codec_private: Vec, - pub codec_name: String, - pub codec: Codec, - /// `CodecDelay` in nanoseconds. - pub codec_delay_ns: u64, - /// `SeekPreRoll` in nanoseconds. - pub seek_pre_roll_ns: u64, - pub video: Option, - pub audio: Option, - pub encodings: Vec, - /// Timebase numerator/denominator in seconds: `TimestampScale / 1e9`, reduced. - pub timebase: (u64, u64), - /// Frame table (empty unless the file was opened with indexing). - pub samples: Vec, - pub(crate) sync: Vec, -} - -impl Track { - /// Bytes prepended to every frame by header-stripping compression, if any. - pub fn stripped_header(&self) -> Option<&[u8]> { - self.encodings.iter().find_map(|e| match e { - ContentEncoding::HeaderStripping { bytes, scope } if scope & 1 != 0 => Some(bytes.as_slice()), - _ => None, - }) - } - - /// True if frames can be returned verbatim or with header stripping undone. - pub fn frames_readable(&self) -> bool { - self.encodings.iter().all(|e| match e { - ContentEncoding::HeaderStripping { .. } => true, - ContentEncoding::Compression { scope, .. } | ContentEncoding::Encryption { scope } => scope & 1 == 0, - }) - } - - /// Bytes to put in front of a stored frame of `stored_size` bytes to get the codec frame: - /// header-stripped bytes, or for `V_PRORES` the 8-byte frame header (`size` + `icpf`) that the - /// Matroska ProRes mapping omits. - pub fn frame_prefix(&self, stored_size: u64) -> Vec { - let mut p = self.stripped_header().map(<[u8]>::to_vec).unwrap_or_default(); - if matches!(self.codec, Codec::ProRes { .. }) { - let total = stored_size + p.len() as u64 + 8; - let mut h = (total as u32).to_be_bytes().to_vec(); - h.extend_from_slice(b"icpf"); - h.extend_from_slice(&p); - p = h; - } - p - } - - /// Frame size of sample `i` as returned by `read_sample` / packets (stored size plus prefix). - pub fn sample_size(&self, i: usize) -> Option { - let s = self.samples.get(i)?; - Some(s.size as u64 + self.frame_prefix(s.size as u64).len() as u64) - } - - /// Indices of keyframe samples, ascending by file order. - pub fn keyframes(&self) -> &[u32] { - &self.sync - } - - /// Timebase ticks → nanoseconds. - pub fn ticks_to_ns(&self, t: i64) -> i64 { - (t as i128 * self.timebase.0 as i128 * 1_000_000_000 / self.timebase.1 as i128) as i64 - } - - /// Nanoseconds → timebase ticks (floor). - pub fn ns_to_ticks(&self, ns: i64) -> i64 { - let n = ns as i128 * self.timebase.1 as i128; - let d = self.timebase.0 as i128 * 1_000_000_000; - n.div_euclid(d) as i64 - } - - /// Index of the last sample with `pts <= t` (in file order), or the first sample. - pub fn sample_at_pts(&self, t: i64) -> Option { - if self.samples.is_empty() { - return None; - } - let mut best: Option = None; - for (i, s) in self.samples.iter().enumerate() { - if s.pts <= t && best.is_none_or(|b| s.pts >= self.samples[b].pts) { - best = Some(i); - } - } - Some(best.unwrap_or(0)) - } - - /// Index of the keyframe with the greatest `pts <= t` (the first keyframe if none precedes `t`). - pub fn keyframe_before_pts(&self, t: i64) -> Option { - let mut best: Option = None; - for &k in &self.sync { - let s = &self.samples[k as usize]; - if s.pts <= t && best.is_none_or(|b| s.pts >= self.samples[b].pts) { - best = Some(k as usize); - } - } - best.or_else(|| self.sync.first().map(|&k| k as usize)) - } - - /// Index of the nearest keyframe at or before sample `index` in file order. - pub fn sync_sample_before(&self, index: usize) -> usize { - match self.sync.binary_search(&(index as u32)) { - Ok(i) => self.sync[i] as usize, - Err(0) => 0, - Err(i) => self.sync[i - 1] as usize, - } - } -} - -#[cfg(test)] -mod rotation_tests { - use super::{Projection, VideoInfo}; - - fn with_roll(projection_type: u64, yaw: f64, pitch: f64, roll: f64) -> VideoInfo { - VideoInfo { projection: Some(Projection { projection_type, yaw, pitch, roll }), ..Default::default() } - } - - #[test] - fn display_rotation_maps_rectangular_roll_to_clockwise_quarter_turns() { - assert_eq!(VideoInfo::default().display_rotation(), Some(0), "no Projection: as stored"); - // ProjectionPoseRoll is counter-clockwise: -90 is a quarter turn clockwise - for (roll, turns) in [(0.0, 0), (-90.0, 1), (180.0, 2), (-180.0, 2), (90.0, 3), (-270.0, 3), (270.0, 1), (360.0, 0)] { - assert_eq!(with_roll(0, 0.0, 0.0, roll).display_rotation(), Some(turns), "roll {roll}"); - } - } - - #[test] - fn display_rotation_leaves_non_rectangular_flipped_and_odd_angles_alone() { - assert_eq!(with_roll(1, 0.0, 0.0, -90.0).display_rotation(), None, "equirectangular"); - assert_eq!(with_roll(0, 180.0, 0.0, 0.0).display_rotation(), None, "yaw flip"); - assert_eq!(with_roll(0, 0.0, 180.0, 0.0).display_rotation(), None, "pitch flip"); - assert_eq!(with_roll(0, 0.0, 0.0, 45.0).display_rotation(), None, "not a quarter turn"); - assert_eq!(with_roll(0, 0.0, 0.0, f64::NAN).display_rotation(), None, "malformed angle"); - } -} diff --git a/crates/media/src/video.rs b/crates/media/src/video.rs index bd40c446..5670817e 100644 --- a/crates/media/src/video.rs +++ b/crates/media/src/video.rs @@ -330,6 +330,7 @@ pub(crate) fn codec_name(c: &CodecConfig) -> String { CodecConfig::Hevc(_) => "HEVC".into(), CodecConfig::Av1(_) => "AV1".into(), CodecConfig::Vp9(_) => "VP9".into(), + CodecConfig::Apv(_) => "APV".into(), CodecConfig::ProRes { .. } => "Apple ProRes".into(), CodecConfig::Jpeg { .. } => "Motion JPEG".into(), CodecConfig::Dnx { .. } => "DNxHD".into(),