Skip to main content

ebml_webm/
mux.rs

1//! Sans-IO `WebM`/Matroska muxer: `add_track` → `begin` → `push_frame` →
2//! `poll_bytes`.
3//!
4//! Scope and design: see `adr/0003-webm-mux.md`. Mirrors `iso_bmff::mux`'s
5//! typestate shape (`Open` → `Live`) and `output`/`poll_bytes` drain pattern.
6
7#![forbid(unsafe_code)]
8
9use crate::types::TrackInfo;
10use crate::{INLINE_TRACKS, MuxError, ids, vint};
11use smallvec::SmallVec;
12use std::marker::PhantomData;
13
14/// Track registration phase — call [`Muxer::begin`] before pushing frames.
15#[derive(Debug, Clone, Copy, Default)]
16pub struct Open;
17
18/// Streaming phase — `push_frame`/`poll_bytes` API.
19#[derive(Debug, Clone, Copy, Default)]
20pub struct Live;
21
22/// Default frames buffered per `Cluster` before it is flushed to `output`.
23pub const DEFAULT_CLUSTER_BATCH: usize = 32;
24
25/// Matroska/`WebM` default `TimecodeScale` (ns per tick) — matches the
26/// demux side's [`crate::demux`] default when a file omits it.
27const DEFAULT_TIMECODE_SCALE: u64 = 1_000_000;
28
29/// A `SimpleBlock`'s relative timecode is a signed 16-bit offset from its
30/// `Cluster`'s `Timecode` — a frame whose offset would overflow this range
31/// forces a new `Cluster` early, regardless of the batch size.
32const fn fits_relative_timecode(delta: i64) -> bool {
33    delta >= i16::MIN as i64 && delta <= i16::MAX as i64
34}
35
36/// Sans-IO `WebM` muxer. `S` is the typestate ([`Open`] or [`Live`]).
37#[derive(Debug)]
38pub struct Muxer<S = Open> {
39    tracks: SmallVec<[TrackInfo; INLINE_TRACKS]>,
40    timecode_scale: u64,
41    output: Vec<u8>,
42    output_consumed: usize,
43    /// Buffered `SimpleBlock` element bytes for the still-open `Cluster`.
44    cluster: Vec<u8>,
45    /// Absolute timecode of the open `Cluster`'s first frame, if any.
46    cluster_timecode: Option<i64>,
47    cluster_frames: usize,
48    batch: usize,
49    _state: PhantomData<S>,
50}
51
52impl Muxer<Open> {
53    /// Empty muxer in track-registration state, default `TimecodeScale`
54    /// (1ms/tick) and [`DEFAULT_CLUSTER_BATCH`].
55    #[must_use]
56    pub fn new() -> Self {
57        Self::with_options(DEFAULT_TIMECODE_SCALE, DEFAULT_CLUSTER_BATCH)
58    }
59
60    /// Muxer with a custom `TimecodeScale` (ns/tick) and `Cluster` batch size.
61    #[must_use]
62    pub fn with_options(timecode_scale: u64, batch: usize) -> Self {
63        Self {
64            tracks: SmallVec::new(),
65            timecode_scale: timecode_scale.max(1),
66            output: Vec::with_capacity(4 * 1024),
67            output_consumed: 0,
68            cluster: Vec::new(),
69            cluster_timecode: None,
70            cluster_frames: 0,
71            batch: batch.max(1),
72            _state: PhantomData,
73        }
74    }
75
76    /// Registered tracks so far.
77    #[must_use]
78    pub fn tracks(&self) -> &[TrackInfo] {
79        &self.tracks
80    }
81
82    /// Register a track. `track.track_number` must be non-zero and unique.
83    ///
84    /// # Errors
85    ///
86    /// [`MuxError::InvalidTrackNumber`] for `track_number == 0`;
87    /// [`MuxError::DuplicateTrack`] if already registered.
88    pub fn add_track(&mut self, track: TrackInfo) -> Result<(), MuxError> {
89        if track.track_number == 0 {
90            return Err(MuxError::InvalidTrackNumber);
91        }
92        if self
93            .tracks
94            .iter()
95            .any(|t| t.track_number == track.track_number)
96        {
97            return Err(MuxError::DuplicateTrack(track.track_number));
98        }
99        self.tracks.push(track);
100        Ok(())
101    }
102
103    /// Lock tracks, write the `EBML` header + `Segment`/`Info`/`Tracks`
104    /// headers, and enter the live streaming state.
105    #[must_use]
106    pub fn begin(mut self) -> Muxer<Live> {
107        write_ebml_header(&mut self.output);
108        write_id(&mut self.output, ids::SEGMENT);
109        vint::encode_unknown_size(4, &mut self.output); // streaming: total length unknown upfront
110        write_segment_info(&mut self.output, self.timecode_scale);
111        write_tracks(&mut self.output, &self.tracks);
112        Muxer {
113            tracks: self.tracks,
114            timecode_scale: self.timecode_scale,
115            output: self.output,
116            output_consumed: self.output_consumed,
117            cluster: self.cluster,
118            cluster_timecode: self.cluster_timecode,
119            cluster_frames: self.cluster_frames,
120            batch: self.batch,
121            _state: PhantomData,
122        }
123    }
124}
125
126impl Default for Muxer<Open> {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl Muxer<Live> {
133    /// Registered tracks.
134    #[must_use]
135    pub fn tracks(&self) -> &[TrackInfo] {
136        &self.tracks
137    }
138
139    /// `TimecodeScale` in effect (ns per tick) — frame `timecode` values are
140    /// in these ticks, same unit [`crate::Demuxer::time_base`] reports.
141    #[must_use]
142    pub const fn timecode_scale(&self) -> u64 {
143        self.timecode_scale
144    }
145
146    /// Push one frame. `timecode` is absolute, in `TimecodeScale` ticks, and
147    /// must be non-decreasing per track (not enforced — an out-of-order
148    /// timecode either still fits the open `Cluster`'s relative-offset range
149    /// or forces a new `Cluster`, same as any large forward jump).
150    ///
151    /// Frames are buffered into the open `Cluster` and only reach `output`
152    /// when the `Cluster` closes ([`Self::flush`], the batch/range limit, or
153    /// the next `push_frame` that can't share the open `Cluster`) — call
154    /// [`Self::poll_bytes`] after to drain.
155    ///
156    /// # Errors
157    ///
158    /// [`MuxError::UnknownTrack`] if `track_number` was never registered.
159    pub fn push_frame(
160        &mut self,
161        track_number: u64,
162        timecode: i64,
163        is_keyframe: bool,
164        payload: &[u8],
165    ) -> Result<(), MuxError> {
166        if !self.tracks.iter().any(|t| t.track_number == track_number) {
167            return Err(MuxError::UnknownTrack(track_number));
168        }
169        let needs_new_cluster = match self.cluster_timecode {
170            None => true,
171            Some(base) => {
172                self.cluster_frames >= self.batch || !fits_relative_timecode(timecode - base)
173            }
174        };
175        if needs_new_cluster {
176            self.close_cluster();
177            self.cluster_timecode = Some(timecode);
178        }
179        // Base is always `Some` here — just set above, or the frame fit an
180        // already-open cluster.
181        let base = self.cluster_timecode.unwrap_or(timecode);
182        write_simple_block(
183            &mut self.cluster,
184            track_number,
185            (timecode - base) as i16,
186            is_keyframe,
187            payload,
188        );
189        self.cluster_frames += 1;
190        Ok(())
191    }
192
193    /// Force the open `Cluster` (if any) to close and become available via
194    /// [`Self::poll_bytes`]. Call before finishing the stream — an empty
195    /// open `Cluster` (no frames pushed) writes nothing.
196    pub fn flush(&mut self) {
197        self.close_cluster();
198    }
199
200    /// Append available output bytes into `out`, same drain contract as
201    /// `iso_bmff::mux::Muxer::poll_bytes`.
202    pub fn poll_bytes(&mut self, out: &mut Vec<u8>) -> usize {
203        let available = self.output.len().saturating_sub(self.output_consumed);
204        if available == 0 {
205            return 0;
206        }
207        out.extend_from_slice(&self.output[self.output_consumed..]);
208        self.output_consumed = self.output.len();
209        if self.output_consumed >= 64 * 1024 {
210            self.output.drain(..self.output_consumed);
211            self.output_consumed = 0;
212        }
213        available
214    }
215
216    fn close_cluster(&mut self) {
217        if self.cluster_timecode.is_none() || self.cluster.is_empty() {
218            self.cluster.clear();
219            self.cluster_timecode = None;
220            self.cluster_frames = 0;
221            return;
222        }
223        let base = self.cluster_timecode.unwrap_or(0);
224        let mut body = Vec::with_capacity(self.cluster.len() + 16);
225        write_uint_elem(&mut body, ids::TIMECODE, base.max(0) as u64);
226        body.extend_from_slice(&self.cluster);
227        write_id(&mut self.output, ids::CLUSTER);
228        vint::encode_size(body.len() as u64, &mut self.output);
229        self.output.extend_from_slice(&body);
230        self.cluster.clear();
231        self.cluster_timecode = None;
232        self.cluster_frames = 0;
233    }
234}
235
236fn write_id(out: &mut Vec<u8>, id: u32) {
237    vint::encode_id(id, out);
238}
239
240fn write_ebml_header(out: &mut Vec<u8>) {
241    let mut body = Vec::new();
242    write_uint_elem(&mut body, ids::EBML_VERSION, 1);
243    write_uint_elem(&mut body, ids::EBML_READ_VERSION, 1);
244    write_uint_elem(&mut body, ids::EBML_MAX_ID_LENGTH, 4);
245    write_uint_elem(&mut body, ids::EBML_MAX_SIZE_LENGTH, 8);
246    write_string_elem(&mut body, ids::DOC_TYPE, "webm");
247    write_uint_elem(&mut body, ids::DOC_TYPE_VERSION, 2);
248    write_uint_elem(&mut body, ids::DOC_TYPE_READ_VERSION, 2);
249    write_id(out, ids::EBML_HEADER);
250    vint::encode_size(body.len() as u64, out);
251    out.extend_from_slice(&body);
252}
253
254fn write_segment_info(out: &mut Vec<u8>, timecode_scale: u64) {
255    let mut body = Vec::new();
256    write_uint_elem(&mut body, ids::TIMECODE_SCALE, timecode_scale);
257    write_id(out, ids::SEGMENT_INFO);
258    vint::encode_size(body.len() as u64, out);
259    out.extend_from_slice(&body);
260}
261
262fn write_tracks(out: &mut Vec<u8>, tracks: &[TrackInfo]) {
263    if tracks.is_empty() {
264        return;
265    }
266    let mut body = Vec::new();
267    for t in tracks {
268        write_track_entry(&mut body, t);
269    }
270    write_id(out, ids::TRACKS);
271    vint::encode_size(body.len() as u64, out);
272    out.extend_from_slice(&body);
273}
274
275fn write_track_entry(out: &mut Vec<u8>, t: &TrackInfo) {
276    let mut body = Vec::new();
277    write_uint_elem(&mut body, ids::TRACK_NUMBER, t.track_number);
278    write_uint_elem(&mut body, ids::TRACK_TYPE, u64::from(t.track_type));
279    write_string_elem(&mut body, ids::CODEC_ID, &t.codec_id);
280    if let Some(cp) = &t.codec_private {
281        write_binary_elem(&mut body, ids::CODEC_PRIVATE, cp);
282    }
283    if t.is_video() {
284        let mut video = Vec::new();
285        write_uint_elem(&mut video, ids::PIXEL_WIDTH, u64::from(t.width));
286        write_uint_elem(&mut video, ids::PIXEL_HEIGHT, u64::from(t.height));
287        write_id(&mut body, ids::VIDEO);
288        vint::encode_size(video.len() as u64, &mut body);
289        body.extend_from_slice(&video);
290    } else {
291        let mut audio = Vec::new();
292        write_float_elem(&mut audio, ids::SAMPLING_FREQUENCY, t.sample_rate);
293        write_uint_elem(&mut audio, ids::CHANNELS, u64::from(t.channels));
294        write_id(&mut body, ids::AUDIO);
295        vint::encode_size(audio.len() as u64, &mut body);
296        body.extend_from_slice(&audio);
297    }
298    write_id(out, ids::TRACK_ENTRY);
299    vint::encode_size(body.len() as u64, out);
300    out.extend_from_slice(&body);
301}
302
303/// `SimpleBlock` body: track number as a size-style VINT (marker stripped —
304/// same convention [`crate::demux`]'s `parse_block_common` reads), 2-byte
305/// signed relative timecode, 1 flags byte (`0x80` keyframe, no lacing bits
306/// set — this muxer never lacs), then the raw payload.
307fn write_simple_block(
308    out: &mut Vec<u8>,
309    track_number: u64,
310    relative_timecode: i16,
311    is_keyframe: bool,
312    payload: &[u8],
313) {
314    let mut body = Vec::with_capacity(payload.len() + 4);
315    vint::encode_size(track_number, &mut body);
316    body.extend_from_slice(&relative_timecode.to_be_bytes());
317    body.push(if is_keyframe { 0x80 } else { 0x00 });
318    body.extend_from_slice(payload);
319    write_id(out, ids::SIMPLE_BLOCK);
320    vint::encode_size(body.len() as u64, out);
321    out.extend_from_slice(&body);
322}
323
324/// Write a `UInt` master element: `id`, size, then the minimal big-endian
325/// byte representation of `value` (leading zero bytes stripped; `0` itself
326/// still writes one `0x00` byte — an empty-content `UInt` is legal EBML but
327/// less common in the wild, and 1 byte costs nothing here).
328fn write_uint_elem(out: &mut Vec<u8>, id: u32, value: u64) {
329    let be = value.to_be_bytes();
330    let first_nonzero = be.iter().position(|&b| b != 0).unwrap_or(be.len() - 1);
331    write_id(out, id);
332    vint::encode_size((be.len() - first_nonzero) as u64, out);
333    out.extend_from_slice(&be[first_nonzero..]);
334}
335
336/// Write an EBML `Float` master element as 8 bytes (`f64`, big-endian) —
337/// the demux side ([`crate::demux`]) accepts both 4- and 8-byte floats, so
338/// writing the 8-byte form unconditionally is spec-valid and simpler.
339fn write_float_elem(out: &mut Vec<u8>, id: u32, value: f64) {
340    write_id(out, id);
341    vint::encode_size(8, out);
342    out.extend_from_slice(&value.to_be_bytes());
343}
344
345/// Write an ASCII `String` master element (`CodecID`, `DocType`) — content
346/// bytes as-is, no null terminator (EBML strings are length-prefixed, not
347/// C-style).
348fn write_binary_elem(out: &mut Vec<u8>, id: u32, value: &[u8]) {
349    write_id(out, id);
350    vint::encode_size(value.len() as u64, out);
351    out.extend_from_slice(value);
352}
353
354fn write_string_elem(out: &mut Vec<u8>, id: u32, value: &str) {
355    write_id(out, id);
356    vint::encode_size(value.len() as u64, out);
357    out.extend_from_slice(value.as_bytes());
358}
359
360#[cfg(test)]
361#[path = "mux_tests.rs"]
362mod tests;