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 t.is_video() {
281        let mut video = Vec::new();
282        write_uint_elem(&mut video, ids::PIXEL_WIDTH, u64::from(t.width));
283        write_uint_elem(&mut video, ids::PIXEL_HEIGHT, u64::from(t.height));
284        write_id(&mut body, ids::VIDEO);
285        vint::encode_size(video.len() as u64, &mut body);
286        body.extend_from_slice(&video);
287    } else {
288        let mut audio = Vec::new();
289        write_float_elem(&mut audio, ids::SAMPLING_FREQUENCY, t.sample_rate);
290        write_uint_elem(&mut audio, ids::CHANNELS, u64::from(t.channels));
291        write_id(&mut body, ids::AUDIO);
292        vint::encode_size(audio.len() as u64, &mut body);
293        body.extend_from_slice(&audio);
294    }
295    write_id(out, ids::TRACK_ENTRY);
296    vint::encode_size(body.len() as u64, out);
297    out.extend_from_slice(&body);
298}
299
300/// `SimpleBlock` body: track number as a size-style VINT (marker stripped —
301/// same convention [`crate::demux`]'s `parse_block_common` reads), 2-byte
302/// signed relative timecode, 1 flags byte (`0x80` keyframe, no lacing bits
303/// set — this muxer never lacs), then the raw payload.
304fn write_simple_block(
305    out: &mut Vec<u8>,
306    track_number: u64,
307    relative_timecode: i16,
308    is_keyframe: bool,
309    payload: &[u8],
310) {
311    let mut body = Vec::with_capacity(payload.len() + 4);
312    vint::encode_size(track_number, &mut body);
313    body.extend_from_slice(&relative_timecode.to_be_bytes());
314    body.push(if is_keyframe { 0x80 } else { 0x00 });
315    body.extend_from_slice(payload);
316    write_id(out, ids::SIMPLE_BLOCK);
317    vint::encode_size(body.len() as u64, out);
318    out.extend_from_slice(&body);
319}
320
321/// Write a `UInt` master element: `id`, size, then the minimal big-endian
322/// byte representation of `value` (leading zero bytes stripped; `0` itself
323/// still writes one `0x00` byte — an empty-content `UInt` is legal EBML but
324/// less common in the wild, and 1 byte costs nothing here).
325fn write_uint_elem(out: &mut Vec<u8>, id: u32, value: u64) {
326    let be = value.to_be_bytes();
327    let first_nonzero = be.iter().position(|&b| b != 0).unwrap_or(be.len() - 1);
328    write_id(out, id);
329    vint::encode_size((be.len() - first_nonzero) as u64, out);
330    out.extend_from_slice(&be[first_nonzero..]);
331}
332
333/// Write an EBML `Float` master element as 8 bytes (`f64`, big-endian) —
334/// the demux side ([`crate::demux`]) accepts both 4- and 8-byte floats, so
335/// writing the 8-byte form unconditionally is spec-valid and simpler.
336fn write_float_elem(out: &mut Vec<u8>, id: u32, value: f64) {
337    write_id(out, id);
338    vint::encode_size(8, out);
339    out.extend_from_slice(&value.to_be_bytes());
340}
341
342/// Write an ASCII `String` master element (`CodecID`, `DocType`) — content
343/// bytes as-is, no null terminator (EBML strings are length-prefixed, not
344/// C-style).
345fn write_string_elem(out: &mut Vec<u8>, id: u32, value: &str) {
346    write_id(out, id);
347    vint::encode_size(value.len() as u64, out);
348    out.extend_from_slice(value.as_bytes());
349}
350
351#[cfg(test)]
352#[path = "mux_tests.rs"]
353mod tests;