diff --git a/Cargo.lock b/Cargo.lock index d98205d1..8f57bedb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1484,6 +1484,13 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "filmcraft-matroska" +version = "0.1.0" +dependencies = [ + "serde_json", +] + [[package]] name = "filmcraft-media" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 66e04659..95cff0fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ filmcraft-geom = { path = "crates/geom" } filmcraft-color = { path = "crates/color" } filmcraft-bitstream = { path = "crates/bitstream" } filmcraft-isobmff = { path = "crates/isobmff" } +filmcraft-matroska = { path = "crates/matroska" } filmcraft-h264 = { path = "crates/h264" } filmcraft-prores = { path = "crates/prores" } filmcraft-aac = { path = "crates/aac" } diff --git a/crates/matroska/Cargo.toml b/crates/matroska/Cargo.toml new file mode 100644 index 00000000..c95f34c4 --- /dev/null +++ b/crates/matroska/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "filmcraft-matroska" +description = "Clean-room Matroska (MKV) and WebM demuxer" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/crates/matroska/src/codec.rs b/crates/matroska/src/codec.rs new file mode 100644 index 00000000..75b7a7e2 --- /dev/null +++ b/crates/matroska/src/codec.rs @@ -0,0 +1,225 @@ +//! 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`. + 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" => 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 new file mode 100644 index 00000000..b4e03cde --- /dev/null +++ b/crates/matroska/src/demux.rs @@ -0,0 +1,1156 @@ +//! 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, + /// 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))) +} + +/// 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: cluster_ts + rel as i64, + 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: cluster_ts + rel as i64, + 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 }; + let base_ns = b.ts as i128 * scale_i - t.codec_delay_ns as i128; + 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; + let duration = end.map_or(0, |e| (to_ticks(e) - to_ticks(start)).max(0) as u64); + FrameRef { + track: b.track, + offset, + size, + pts: to_ticks(pts_ns), + 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 = ebml::uint(io.peek(c.data_start(), (ce - c.data_start()) as usize)?) as i64, + 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.stripped_header().map(<[u8]>::to_vec).unwrap_or_default(); + 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.stripped_header().map(<[u8]>::to_vec).unwrap_or_default() } 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, + 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 = ebml::uint(io.peek(h.data_start(), (he - h.data_start()) as usize)?) as i64; + 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 new file mode 100644 index 00000000..9fe599ca --- /dev/null +++ b/crates/matroska/src/ebml.rs @@ -0,0 +1,474 @@ +//! 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-zero / all-ones value bits. +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 { + 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 => unreachable!(), + 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!(read_id(&[0x80]).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 new file mode 100644 index 00000000..718c5c50 --- /dev/null +++ b/crates/matroska/src/error.rs @@ -0,0 +1,53 @@ +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 new file mode 100644 index 00000000..be1ef47e --- /dev/null +++ b/crates/matroska/src/ids.rs @@ -0,0 +1,206 @@ +//! 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; + +// 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 new file mode 100644 index 00000000..c2ada3e6 --- /dev/null +++ b/crates/matroska/src/lib.rs @@ -0,0 +1,32 @@ +//! 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 `filmcraft-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`. + +mod codec; +mod demux; +pub mod ebml; +mod error; +mod ids; +mod meta; +mod source; +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 source::{ByteSource, ReadSeekSource}; +pub use track::{AudioInfo, Colour, ContentEncoding, MasteringMetadata, 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::*; +} diff --git a/crates/matroska/src/meta.rs b/crates/matroska/src/meta.rs new file mode 100644 index 00000000..adac699f --- /dev/null +++ b/crates/matroska/src/meta.rs @@ -0,0 +1,526 @@ +//! 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, 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 }); + } + } +} + +fn parse_chapter(d: &[u8]) -> 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 => c.children.push(parse_chapter(e.data)), + _ => {} + } + } + 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)), + _ => {} + } + } + out.push(edition); + } +} + +fn parse_simple_tag(d: &[u8]) -> 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 => t.children.push(parse_simple_tag(e.data)), + _ => {} + } + } + 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)), + _ => {} + } + } + 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)), + _ => {} + } + } + 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 +} diff --git a/crates/matroska/src/source.rs b/crates/matroska/src/source.rs new file mode 100644 index 00000000..da10d89f --- /dev/null +++ b/crates/matroska/src/source.rs @@ -0,0 +1,173 @@ +//! 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 `filmcraft_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/track.rs b/crates/matroska/src/track.rs new file mode 100644 index 00000000..25c0267b --- /dev/null +++ b/crates/matroska/src/track.rs @@ -0,0 +1,280 @@ +//! 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, +} + +impl VideoInfo { + /// 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, + }) + } + + /// Decoded frame size of sample `i` (stored size plus stripped header). + pub fn sample_size(&self, i: usize) -> Option { + let s = self.samples.get(i)?; + Some(s.size as u64 + self.stripped_header().map_or(0, |h| h.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, + } + } +}