Skip to main content

ebml_webm/
demux.rs

1//! Sans-IO `WebM`/Matroska demuxer: `push_bytes` → `poll_frame`.
2//!
3//! Element subset and known gaps (indefinite-`Cluster` lookahead, mux): see
4//! `adr/0001-ebml-vint-webm-schema-v1.md` and
5//! `adr/0002-full-matroska-profile.md`.
6
7#![forbid(unsafe_code)]
8
9use crate::lacing::{self, Lacing};
10use crate::types::{Bytes, CuePoint, Frame, Rational, SeekEntry, TrackInfo};
11use crate::{Error, INLINE_INDEX, INLINE_STACK, INLINE_TRACKS, ids, vint};
12use smallvec::SmallVec;
13use std::collections::VecDeque;
14
15/// Matroska/`WebM` default `TimecodeScale` (ns per tick) when `Info` omits it.
16const DEFAULT_TIMECODE_SCALE: u64 = 1_000_000;
17/// `Audio\SamplingFrequency` spec default (Hz) when absent.
18const DEFAULT_SAMPLE_RATE_HZ: f64 = 8000.0;
19/// `Audio\Channels` spec default when absent.
20const DEFAULT_CHANNELS: u32 = 1;
21
22#[derive(Debug, Clone, Copy)]
23struct OpenElement {
24    id: u32,
25    /// `None` = indefinite size; stays open until the parent closes or EOF.
26    end: Option<usize>,
27}
28
29#[derive(Debug, Default)]
30struct TrackScratch {
31    track_number: Option<u64>,
32    track_type: Option<u8>,
33    codec_id: Option<String>,
34    codec_private: Option<Bytes>,
35    width: u32,
36    height: u32,
37    sample_rate: Option<f64>,
38    channels: Option<u32>,
39}
40
41impl TrackScratch {
42    /// A track without a number or codec ID is not usable — drop it rather
43    /// than emit a half-populated [`TrackInfo`].
44    fn finish(self) -> Option<TrackInfo> {
45        Some(TrackInfo {
46            track_number: self.track_number?,
47            track_type: self.track_type.unwrap_or(0),
48            codec_id: self.codec_id?,
49            codec_private: self.codec_private,
50            width: self.width,
51            height: self.height,
52            sample_rate: self.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE_HZ),
53            channels: self.channels.unwrap_or(DEFAULT_CHANNELS),
54        })
55    }
56}
57
58/// A parsed `SimpleBlock`/`Block` before lace-splitting into `Frame`s.
59///
60/// `payloads` are copied out of `buffer` at parse time (not stored as
61/// `(start, end)` offsets): a `Block` inside a `BlockGroup` is held here until
62/// the group's closing tag is seen, which may span several `push_bytes`
63/// calls — `compact()` can drop the underlying buffer prefix in the meantime,
64/// so raw offsets would go stale (or point at already-discarded bytes).
65#[derive(Debug)]
66struct ParsedBlock {
67    track_number: u64,
68    timecode: i64,
69    /// Raw flags byte — bit `0x80` is `SimpleBlock`'s keyframe flag (reserved,
70    /// unused, in a `BlockGroup`'s `Block`).
71    flags: u8,
72    payloads: SmallVec<[Bytes; 8]>,
73}
74
75#[derive(Debug, Default)]
76struct BlockGroupScratch {
77    block: Option<ParsedBlock>,
78    has_reference_block: bool,
79    duration_ticks: Option<u64>,
80}
81
82#[derive(Debug, Default)]
83struct CuePointScratch {
84    time_ticks: Option<u64>,
85    cluster_position: Option<u64>,
86}
87
88#[derive(Debug, Default)]
89struct SeekScratch {
90    id: Option<u32>,
91    position: Option<u64>,
92}
93
94/// Sans-IO demuxer: `push_bytes` → `poll_frame`.
95#[derive(Debug)]
96pub struct Demuxer {
97    buffer: Vec<u8>,
98    read_pos: usize,
99    stack: SmallVec<[OpenElement; INLINE_STACK]>,
100    tracks: SmallVec<[TrackInfo; INLINE_TRACKS]>,
101    building_track: Option<TrackScratch>,
102    building_block_group: Option<BlockGroupScratch>,
103    building_cue_point: Option<CuePointScratch>,
104    building_seek: Option<SeekScratch>,
105    cues: SmallVec<[CuePoint; INLINE_INDEX]>,
106    seek_head: SmallVec<[SeekEntry; INLINE_INDEX]>,
107    timecode_scale: u64,
108    cluster_timecode: i64,
109    frames: VecDeque<Frame>,
110    /// Set once parsing hits a structurally unrecoverable position (reserved
111    /// VINT, indefinite size on a non-descend element). No further bytes are
112    /// interpreted; already-extracted tracks/frames are kept.
113    halted: bool,
114}
115
116impl Default for Demuxer {
117    fn default() -> Self {
118        Self {
119            buffer: Vec::new(),
120            read_pos: 0,
121            stack: SmallVec::new(),
122            tracks: SmallVec::new(),
123            building_track: None,
124            building_block_group: None,
125            building_cue_point: None,
126            building_seek: None,
127            cues: SmallVec::new(),
128            seek_head: SmallVec::new(),
129            timecode_scale: DEFAULT_TIMECODE_SCALE,
130            cluster_timecode: 0,
131            frames: VecDeque::new(),
132            halted: false,
133        }
134    }
135}
136
137impl Demuxer {
138    /// Empty demuxer.
139    #[must_use]
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Feed container bytes (sans-io; caller owns I/O).
145    pub fn push_bytes(&mut self, chunk: &[u8]) {
146        self.buffer.extend_from_slice(chunk);
147        self.pump();
148        self.compact();
149    }
150
151    /// Tracks discovered so far (populated once `Tracks` has been parsed).
152    #[must_use]
153    pub fn streams(&self) -> &[TrackInfo] {
154        &self.tracks
155    }
156
157    /// Next demuxed frame, if any.
158    pub fn poll_frame(&mut self) -> Option<Frame> {
159        self.frames.pop_front()
160    }
161
162    /// `Segment\Cues` entries parsed so far — informational seek index; this
163    /// crate does not seek (sans-io: seeking is the I/O adapter's job).
164    #[must_use]
165    pub fn cues(&self) -> &[CuePoint] {
166        &self.cues
167    }
168
169    /// `Segment\SeekHead` entries parsed so far — informational.
170    #[must_use]
171    pub fn seek_head(&self) -> &[SeekEntry] {
172        &self.seek_head
173    }
174
175    /// Media timebase derived from `Segment\Info\TimecodeScale`
176    /// (`1_000_000` ns/tick default per the Matroska/`WebM` spec).
177    #[must_use]
178    pub const fn time_base(&self) -> Rational {
179        Rational::new(self.timecode_scale, 1_000_000_000)
180    }
181
182    fn pump(&mut self) {
183        if self.halted {
184            return;
185        }
186        loop {
187            self.close_finished_contexts();
188            if !self.step() {
189                break;
190            }
191        }
192    }
193
194    fn close_finished_contexts(&mut self) {
195        loop {
196            let Some(top) = self.stack.last() else {
197                return;
198            };
199            let done = matches!(top.end, Some(end) if self.read_pos >= end);
200            if !done {
201                return;
202            }
203            if let Some(closed) = self.stack.pop() {
204                self.on_close(closed.id);
205            }
206        }
207    }
208
209    /// Process one element header at `read_pos`. `false` means stop pumping
210    /// for now (incomplete buffer or halted).
211    fn step(&mut self) -> bool {
212        let remaining = &self.buffer[self.read_pos..];
213        let (id, id_len) = match vint::decode_id(remaining) {
214            Ok(v) => v,
215            Err(Error::Incomplete) => return false,
216            Err(Error::ReservedVint | Error::Unsupported(_)) => {
217                self.halted = true;
218                return false;
219            }
220        };
221        let (vs, size_len) = match vint::decode_size(&remaining[id_len..]) {
222            Ok(v) => v,
223            Err(Error::Incomplete) => return false,
224            Err(Error::ReservedVint | Error::Unsupported(_)) => {
225                self.halted = true;
226                return false;
227            }
228        };
229        let header_len = id_len + size_len;
230        let content_start = self.read_pos + header_len;
231        let content_end = if vs.unknown {
232            None
233        } else {
234            Some(content_start + vs.value as usize)
235        };
236
237        if ids::is_descend_master(id) {
238            self.on_open(id);
239            self.stack.push(OpenElement {
240                id,
241                end: content_end,
242            });
243            self.read_pos = content_start;
244            return true;
245        }
246
247        let Some(end) = content_end else {
248            // Indefinite size on an element we don't descend into: there is
249            // no way to know where it ends. Unrecoverable — see adr/0001.
250            self.halted = true;
251            return false;
252        };
253        if end > self.buffer.len() {
254            return false; // wait for more bytes
255        }
256        self.handle_leaf(id, content_start, end);
257        self.read_pos = end;
258        true
259    }
260
261    fn on_open(&mut self, id: u32) {
262        match id {
263            ids::TRACK_ENTRY => self.building_track = Some(TrackScratch::default()),
264            ids::CLUSTER => self.cluster_timecode = 0,
265            ids::BLOCK_GROUP => self.building_block_group = Some(BlockGroupScratch::default()),
266            ids::CUE_POINT => self.building_cue_point = Some(CuePointScratch::default()),
267            ids::SEEK => self.building_seek = Some(SeekScratch::default()),
268            _ => {}
269        }
270    }
271
272    fn on_close(&mut self, id: u32) {
273        match id {
274            ids::TRACK_ENTRY => {
275                if let Some(scratch) = self.building_track.take() {
276                    if let Some(track) = scratch.finish() {
277                        self.tracks.push(track);
278                    }
279                }
280            }
281            ids::BLOCK_GROUP => self.finish_block_group(),
282            ids::CUE_POINT => {
283                if let Some(scratch) = self.building_cue_point.take() {
284                    if let (Some(time_ticks), Some(cluster_position)) =
285                        (scratch.time_ticks, scratch.cluster_position)
286                    {
287                        self.cues.push(CuePoint {
288                            time_ticks,
289                            cluster_position,
290                        });
291                    }
292                }
293            }
294            ids::SEEK => {
295                if let Some(scratch) = self.building_seek.take() {
296                    if let (Some(seek_id), Some(position)) = (scratch.id, scratch.position) {
297                        self.seek_head.push(SeekEntry {
298                            id: seek_id,
299                            position,
300                        });
301                    }
302                }
303            }
304            _ => {}
305        }
306    }
307
308    fn finish_block_group(&mut self) {
309        let Some(scratch) = self.building_block_group.take() else {
310            return;
311        };
312        let Some(block) = scratch.block else {
313            return;
314        };
315        let is_keyframe = !scratch.has_reference_block;
316        for payload in block.payloads {
317            self.frames.push_back(Frame {
318                track_number: block.track_number,
319                timecode: block.timecode,
320                is_keyframe,
321                duration_ticks: scratch.duration_ticks,
322                payload,
323            });
324        }
325    }
326
327    fn top_is(&self, id: u32) -> bool {
328        matches!(self.stack.last(), Some(top) if top.id == id)
329    }
330
331    fn handle_leaf(&mut self, id: u32, start: usize, end: usize) {
332        match id {
333            ids::TIMECODE_SCALE => {
334                if let Some(v) = read_uint(&self.buffer[start..end]) {
335                    self.timecode_scale = v.max(1);
336                }
337            }
338            ids::TRACK_NUMBER => {
339                let v = read_uint(&self.buffer[start..end]);
340                if let (Some(v), Some(scratch)) = (v, self.building_track.as_mut()) {
341                    scratch.track_number = Some(v);
342                }
343            }
344            ids::TRACK_TYPE => {
345                let v = read_uint(&self.buffer[start..end]);
346                if let (Some(v), Some(scratch)) = (v, self.building_track.as_mut()) {
347                    scratch.track_type = Some(v as u8);
348                }
349            }
350            ids::CODEC_ID => {
351                // clone: CodecID bytes live in the shared parse buffer; TrackInfo needs an owned String.
352                let codec_id = String::from_utf8(self.buffer[start..end].to_vec()).ok();
353                if let Some(scratch) = self.building_track.as_mut() {
354                    scratch.codec_id = codec_id;
355                }
356            }
357            ids::CODEC_PRIVATE => {
358                // clone: bytes live in the shared parse buffer; TrackInfo needs an owned copy.
359                let cp = Bytes::copy_from_slice(&self.buffer[start..end]);
360                if let Some(scratch) = self.building_track.as_mut() {
361                    scratch.codec_private = Some(cp);
362                }
363            }
364            ids::PIXEL_WIDTH if self.top_is(ids::VIDEO) => {
365                let v = read_uint(&self.buffer[start..end]);
366                if let (Some(v), Some(scratch)) = (v, self.building_track.as_mut()) {
367                    scratch.width = v as u32;
368                }
369            }
370            ids::PIXEL_HEIGHT if self.top_is(ids::VIDEO) => {
371                let v = read_uint(&self.buffer[start..end]);
372                if let (Some(v), Some(scratch)) = (v, self.building_track.as_mut()) {
373                    scratch.height = v as u32;
374                }
375            }
376            ids::SAMPLING_FREQUENCY if self.top_is(ids::AUDIO) => {
377                let v = read_float(&self.buffer[start..end]);
378                if let (Some(v), Some(scratch)) = (v, self.building_track.as_mut()) {
379                    scratch.sample_rate = Some(v);
380                }
381            }
382            ids::CHANNELS if self.top_is(ids::AUDIO) => {
383                let v = read_uint(&self.buffer[start..end]);
384                if let (Some(v), Some(scratch)) = (v, self.building_track.as_mut()) {
385                    scratch.channels = Some(v as u32);
386                }
387            }
388            ids::TIMECODE => {
389                if let Some(v) = read_uint(&self.buffer[start..end]) {
390                    self.cluster_timecode = v as i64;
391                }
392            }
393            ids::SIMPLE_BLOCK => self.handle_simple_block(start, end),
394            ids::BLOCK if self.building_block_group.is_some() => self.handle_block(start, end),
395            ids::BLOCK_DURATION if self.building_block_group.is_some() => {
396                let v = read_uint(&self.buffer[start..end]);
397                if let Some(scratch) = self.building_block_group.as_mut() {
398                    scratch.duration_ticks = v;
399                }
400            }
401            ids::REFERENCE_BLOCK if self.building_block_group.is_some() => {
402                if let Some(scratch) = self.building_block_group.as_mut() {
403                    scratch.has_reference_block = true;
404                }
405            }
406            ids::CUE_TIME if self.top_is(ids::CUE_POINT) => {
407                let v = read_uint(&self.buffer[start..end]);
408                if let Some(scratch) = self.building_cue_point.as_mut() {
409                    scratch.time_ticks = v;
410                }
411            }
412            ids::CUE_CLUSTER_POSITION if self.top_is(ids::CUE_TRACK_POSITIONS) => {
413                let v = read_uint(&self.buffer[start..end]);
414                if let Some(scratch) = self.building_cue_point.as_mut() {
415                    scratch.cluster_position = v;
416                }
417            }
418            ids::SEEK_ID if self.top_is(ids::SEEK) => {
419                let v = read_uint(&self.buffer[start..end]);
420                if let (Some(v), Some(scratch)) = (v, self.building_seek.as_mut()) {
421                    scratch.id = Some(v as u32);
422                }
423            }
424            ids::SEEK_POSITION if self.top_is(ids::SEEK) => {
425                let v = read_uint(&self.buffer[start..end]);
426                if let Some(scratch) = self.building_seek.as_mut() {
427                    scratch.position = v;
428                }
429            }
430            _ => {}
431        }
432    }
433
434    /// Parse a block's common wire format (track number VINT + 2-byte signed
435    /// relative timecode + flags byte, optionally laced). Returns `None` on a
436    /// truncated/malformed block.
437    fn parse_block_common(&self, start: usize, end: usize) -> Option<ParsedBlock> {
438        let body = &self.buffer[start..end];
439        let (track_number, tn_len) = vint::decode_size(body).ok()?;
440        if body.len() < tn_len + 3 {
441            return None;
442        }
443        let rel_tc = i16::from_be_bytes([body[tn_len], body[tn_len + 1]]);
444        let flags = body[tn_len + 2];
445        let lacing = Lacing::from_flags(flags);
446        let ranges = lacing::split(body, tn_len + 3, lacing)?;
447        let timecode = self.cluster_timecode.saturating_add(i64::from(rel_tc));
448        // `ranges` are relative to `body` (i.e. `start`); copy payload bytes out
449        // now, while they're still guaranteed present in `buffer` — see
450        // `ParsedBlock` doc comment for why offsets alone aren't safe to defer.
451        let payloads = ranges
452            .into_iter()
453            .map(|(s, e)| Bytes::copy_from_slice(&self.buffer[start + s..start + e]))
454            .collect();
455        Some(ParsedBlock {
456            track_number: track_number.value,
457            timecode,
458            flags,
459            payloads,
460        })
461    }
462
463    fn handle_simple_block(&mut self, start: usize, end: usize) {
464        let Some(block) = self.parse_block_common(start, end) else {
465            return;
466        };
467        let is_keyframe = block.flags & 0x80 != 0;
468        for payload in block.payloads {
469            self.frames.push_back(Frame {
470                track_number: block.track_number,
471                timecode: block.timecode,
472                is_keyframe,
473                duration_ticks: None,
474                payload,
475            });
476        }
477    }
478
479    fn handle_block(&mut self, start: usize, end: usize) {
480        let Some(block) = self.parse_block_common(start, end) else {
481            return;
482        };
483        if let Some(scratch) = self.building_block_group.as_mut() {
484            scratch.block = Some(block);
485        }
486    }
487
488    fn compact(&mut self) {
489        let drained = self.read_pos;
490        if drained == 0 {
491            return;
492        }
493        if drained < 64 * 1024 && drained * 2 < self.buffer.len() {
494            return;
495        }
496        self.buffer.drain(..drained);
497        for open in &mut self.stack {
498            if let Some(end) = open.end.as_mut() {
499                *end -= drained;
500            }
501        }
502        self.read_pos = 0;
503    }
504}
505
506/// Matroska/`WebM` "Unsigned Integer" element: big-endian, up to 8 bytes.
507fn read_uint(body: &[u8]) -> Option<u64> {
508    if body.len() > 8 {
509        return None;
510    }
511    let mut v = 0u64;
512    for &b in body {
513        v = (v << 8) | u64::from(b);
514    }
515    Some(v)
516}
517
518/// Matroska/`WebM` "Float" element: 4-byte (f32) or 8-byte (f64) IEEE-754 big-endian.
519fn read_float(body: &[u8]) -> Option<f64> {
520    match body.len() {
521        4 => Some(f64::from(f32::from_be_bytes(body.try_into().ok()?))),
522        8 => Some(f64::from_be_bytes(body.try_into().ok()?)),
523        _ => None,
524    }
525}
526
527#[cfg(test)]
528#[path = "demux_tests.rs"]
529mod tests;