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}