Skip to main content

container/demux/
mod.rs

1/// Demux dispatch + shared types + box-walking primitives.
2///
3/// The full demux implementation is split across concern-scoped submodules:
4///   - `mp4`  — ISOBMFF / MP4 / MOV demux, fragmented MP4, streaming init
5///   - `mkv`  — Matroska / WebM demux, Colour mapping, EBML scanner, streaming init
6///   - `audio` — audio track extraction for all containers (AAC, Opus, AC-3, …)
7///   - `hdr`  — HDR static metadata (`mdcv`/`clli`) pulled from visual sample entries
8///   - `tests` — unit tests (compiled only under `#[cfg(test)]`)
9use anyhow::{bail, Result};
10use codec::frame::StreamInfo;
11
12use crate::avi::demux_avi;
13use crate::ts::demux_ts;
14
15pub mod mp4;
16pub mod mkv;
17pub(crate) mod audio;
18pub(crate) mod hdr;
19
20#[cfg(test)]
21mod tests;
22
23// Re-export every item that was `pub` on the old flat `demux` module so
24// all existing `use crate::demux::…` call-sites remain valid.
25// Public surface (matches the original flat module's `pub` items).
26pub use mp4::{demux_mp4, Mp4StreamingDemuxer};
27pub use mkv::{demux_mkv, probe_mkv_color_info, MkvStreamingDemuxer};
28// Crate-internal entry points for the streaming dispatcher.
29pub(crate) use mkv::demux_mkv_streaming_init;
30pub(crate) use mp4::demux_mp4_streaming_init;
31// The remaining helpers (has_av01_sample_entry, prores_sample_entry_fourcc,
32// parse_avcc_param_sets, FragSample, mkv_codec_needs_annexb, extract_*_audio,
33// {ac3,eac3}_sample_rate_channels_*) were private in the original flat module
34// and stay internal — siblings reach them via `super::<sub>::`.
35
36// ---------------------------------------------------------------------------
37// Public shared types
38// ---------------------------------------------------------------------------
39
40pub struct DemuxResult {
41    pub codec: String,
42    pub info: StreamInfo,
43    pub samples: Vec<Vec<u8>>,
44    /// Optional audio track carried through for passthrough muxing. Populated
45    /// when the input has an AAC track (MP4: `mp4a` sample entry; MKV codec
46    /// id `A_AAC`). Other audio codecs log a warning and are dropped.
47    pub audio: Option<AudioTrack>,
48}
49
50/// Audio track extracted for passthrough or transcode. Supports two codec
51/// families today (Squad-18 + Squad-23):
52/// - **AAC-LC**: `codec = "aac"`, `asc` holds the verbatim
53///   AudioSpecificConfig bytes sourced from the MP4 esds descriptor (not
54///   the mp4 crate's rebuilt form) or MKV `CodecPrivate`, so HE-AAC /
55///   xHE-AAC signaling survives the copy. `codec_private` is empty.
56/// - **Opus**: `codec = "opus"`, `codec_private` holds the RFC 7845 §5.1
57///   `OpusHead` body verbatim — for MKV/WebM that's exactly the
58///   `CodecPrivate` element bytes (post-magic — RFC 7845 §5.2 specifies
59///   no magic prefix for the MKV CodecPrivate); for MP4-Opus that's the
60///   `dOps` body re-serialised in OpusHead's LE numeric convention. `asc`
61///   is empty.
62///
63/// `samples` are codec-native packets (AAC: ADTS-stripped raw access
64/// units; Opus: TOC-prefixed Opus packets, one per frame). `durations`
65/// are per-sample in `timescale` units.
66#[derive(Debug, Clone)]
67pub struct AudioTrack {
68    pub codec: String,
69    pub samples: Vec<Vec<u8>>,
70    pub sample_rate: u32,
71    pub channels: u16,
72    /// AAC-only: AudioSpecificConfig bytes. Empty for non-AAC codecs.
73    pub asc: Vec<u8>,
74    /// Opus-only: OpusHead body bytes (RFC 7845 §5.1). Empty for non-Opus
75    /// codecs. The 8-byte 'OpusHead' magic prefix is NOT included — only
76    /// the post-magic body.
77    pub codec_private: Vec<u8>,
78    pub timescale: u32,
79    pub durations: Vec<u32>,
80}
81
82// ---------------------------------------------------------------------------
83// Public dispatch entry point
84// ---------------------------------------------------------------------------
85
86/// Dispatch to the right demuxer based on container magic bytes.
87pub fn demux(data: &[u8]) -> Result<DemuxResult> {
88    match detect_container(data) {
89        // MOV shares its demuxer with MP4 — same ISOBMFF box tree, same
90        // sample-entry structure. `detect_container` returns "mp4" for
91        // both `ftyp mp4*` and `ftyp qt  ` / bare-moov MOVs.
92        "mp4" => demux_mp4(data),
93        "mkv" => demux_mkv(data),
94        "avi" => demux_avi(data),
95        "ts" => demux_ts(data),
96        other => bail!("unsupported container: {other}"),
97    }
98}
99
100pub(crate) fn detect_container(data: &[u8]) -> &'static str {
101    if data.len() < 12 {
102        return "unknown";
103    }
104    // ISOBMFF: MP4 (`ftyp mp41`/`mp42`/`isom`/...) and MOV (`ftyp qt  `)
105    // both land here. Older MOV files sometimes ship without a top-level
106    // `ftyp` and lead with `moov` or `mdat` directly — accept those too.
107    if &data[4..8] == b"ftyp" || &data[4..8] == b"moov" || &data[4..8] == b"mdat" {
108        return "mp4";
109    }
110    // Matroska/WebM: EBML signature.
111    if data[0] == 0x1A && data[1] == 0x45 && data[2] == 0xDF && data[3] == 0xA3 {
112        return "mkv";
113    }
114    // RIFF-based AVI: "RIFF" <size> "AVI ".
115    if &data[..4] == b"RIFF" && &data[8..12] == b"AVI " {
116        return "avi";
117    }
118    // MPEG-TS: 0x47 sync byte at offset 0 AND at offset 188 (and 376 if
119    // we have the bytes). A single 0x47 appears routinely in random
120    // payloads, so require two confirming hits before committing.
121    if data[0] == 0x47
122        && data.len() > 188
123        && data[188] == 0x47
124        && (data.len() <= 376 || data[376] == 0x47)
125    {
126        return "ts";
127    }
128    "unknown"
129}
130
131// ---------------------------------------------------------------------------
132// Shared box-walking primitives (used by mp4.rs, hdr.rs, audio.rs)
133// ---------------------------------------------------------------------------
134
135/// Follow a box type path from `data` (top level) down and return the body
136/// bytes (payload, excluding the 8-byte box header) of the last box in the
137/// path, or None if any hop is missing. Handles 32-bit box sizes only —
138/// adequate for moov/trak/stsd which are ~KB in practice.
139pub(super) fn find_box_body<'a>(data: &'a [u8], path: &[&[u8; 4]]) -> Option<&'a [u8]> {
140    let mut slice = data;
141    for (i, target) in path.iter().enumerate() {
142        let found = find_direct_child(slice, target)?;
143        if i + 1 == path.len() {
144            return Some(found);
145        }
146        slice = found;
147    }
148    None
149}
150
151pub(super) fn find_direct_child<'a>(data: &'a [u8], target: &[u8; 4]) -> Option<&'a [u8]> {
152    let mut pos = 0;
153    while pos + 8 <= data.len() {
154        let size =
155            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
156        let btype = &data[pos + 4..pos + 8];
157        if size < 8 || pos.checked_add(size).is_none_or(|end| end > data.len()) {
158            return None;
159        }
160        if btype == target {
161            return Some(&data[pos + 8..pos + size]);
162        }
163        pos += size;
164    }
165    None
166}