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