Skip to main content

container/
streaming.rs

1//! Pull-based streaming demuxer (Squad streaming-migration-55 P1).
2//!
3//! Replaces the materialize-everything-upfront `demux()` shape with a
4//! `next_video_sample()` iterator. Each per-format implementation
5//! holds only the reader state it needs to produce ONE sample at a
6//! time; nothing accumulates across samples. The legacy `demux()` is
7//! preserved as a thin adapter that drains the iterator into a `Vec`
8//! so existing callers keep working unchanged.
9//!
10//! Memory characteristic: peak heap from any one `next_video_sample()`
11//! call is bounded by the sample size + the reader's internal cursor
12//! state (mp4 0.14 keeps stbl indexes in the `Mp4Reader`; matroska-
13//! demuxer keeps its own cluster cursor; the TS / AVI walks track
14//! only an offset). Audio passthrough remains buffered per the
15//! pinned contract — Squad-18's pattern is unchanged.
16
17use anyhow::{Result, bail};
18use codec::frame::StreamInfo;
19
20use crate::avi::demux_avi_streaming_init;
21use crate::demux::{AudioTrack, demux_mkv_streaming_init, demux_mp4_streaming_init};
22use crate::ts::demux_ts_streaming_init;
23
24/// Header information for a demuxed stream — codec label + the
25/// `StreamInfo` shape every existing caller already consumes.
26/// Available immediately after `demux_streaming()` returns; parsed
27/// from the container header before any video samples are pulled.
28#[derive(Debug, Clone)]
29pub struct DemuxHeader {
30    pub codec: String,
31    pub info: StreamInfo,
32}
33
34/// One demuxed video sample with its container-level timing.
35///
36/// `data` is the codec-native bitstream for the sample — Annex-B for
37/// AVC/HEVC (after AVCC→Annex-B conversion + Squad-14 parameter-set
38/// tracking), raw OBU stream for AV1, IVF/raw frame for VP8/VP9,
39/// self-contained frame for ProRes.
40///
41/// `pts_ticks` is in the container's native timescale (mp4 mvhd
42/// timescale, MKV TimecodeScale-derived, TS 90 kHz, AVI samples-since-
43/// start). The pipeline today does NOT consume per-sample PTS for
44/// decode (decoders pull frames at their own cadence) — it's surfaced
45/// for the muxer/QA bench to attribute durations.
46///
47/// `duration_ticks` defaults to 0 when the container does not record a
48/// per-sample duration (TS PES, AVI movi walk). Callers should fall
49/// back to `1 / frame_rate` from the header in that case.
50#[derive(Debug, Clone)]
51pub struct Sample {
52    pub data: Vec<u8>,
53    pub pts_ticks: i64,
54    pub duration_ticks: u32,
55}
56
57/// Pull-based per-format demuxer. The trait is `Send` so the pipeline
58/// can move the demuxer onto its dedicated decode thread (the existing
59/// transcode pump pattern).
60pub trait StreamingDemuxer: Send {
61    /// Header info parsed from the container header. Cheap to call —
62    /// returns a borrow of the cached `DemuxHeader` populated at
63    /// construction time.
64    fn header(&self) -> &DemuxHeader;
65
66    /// Pull the next video sample. Returns `Ok(None)` at EOF.
67    /// Allocates a fresh `Vec` per sample; nothing is retained
68    /// internally beyond the reader's per-format cursor state.
69    fn next_video_sample(&mut self) -> Result<Option<Sample>>;
70
71    /// Audio is a single buffered slab populated at construction time
72    /// (Squad-18/23/27 passthrough pattern). Streaming audio is out of
73    /// scope for this sprint per the pinned design.
74    fn audio(&self) -> Option<&AudioTrack>;
75}
76
77/// Magic-byte detect the container and dispatch to a per-format
78/// streaming reader. Mirrors `demux::detect_container` exactly so the
79/// streaming and legacy paths agree on every input.
80pub fn demux_streaming(data: &[u8]) -> Result<Box<dyn StreamingDemuxer>> {
81    match detect_container(data) {
82        "mp4" => Ok(Box::new(demux_mp4_streaming_init(data)?)),
83        "mkv" => Ok(Box::new(demux_mkv_streaming_init(data)?)),
84        "avi" => Ok(Box::new(demux_avi_streaming_init(data)?)),
85        "ts" => Ok(Box::new(demux_ts_streaming_init(data)?)),
86        other => bail!("unsupported container: {other}"),
87    }
88}
89
90/// Container magic-byte detector. Kept module-private + duplicated
91/// from `demux::detect_container` so the streaming dispatch doesn't
92/// reach into `demux::`'s private surface and so a future change to
93/// either path stays a one-file edit.
94fn detect_container(data: &[u8]) -> &'static str {
95    if data.len() < 12 {
96        return "unknown";
97    }
98    if &data[4..8] == b"ftyp" || &data[4..8] == b"moov" || &data[4..8] == b"mdat" {
99        return "mp4";
100    }
101    if data[0] == 0x1A && data[1] == 0x45 && data[2] == 0xDF && data[3] == 0xA3 {
102        return "mkv";
103    }
104    if &data[..4] == b"RIFF" && &data[8..12] == b"AVI " {
105        return "avi";
106    }
107    if data[0] == 0x47
108        && data.len() > 188
109        && data[188] == 0x47
110        && (data.len() <= 376 || data[376] == 0x47)
111    {
112        return "ts";
113    }
114    "unknown"
115}