Skip to main content

rff_format/
lib.rs

1//! `rff-format` — the container (muxer/demuxer) abstraction layer (FFmpeg's
2//! `libavformat` core, minus the formats themselves).
3//!
4//! A container is the file wrapper (AVI, MP4, Matroska, ...) that interleaves
5//! one or more elementary streams. This crate defines:
6//! * [`Stream`] — the description of one elementary stream inside a container,
7//! * [`Demuxer`] — reads a container, exposing streams and [`Packet`]s,
8//! * [`Muxer`] — writes streams + packets back into a container,
9//! * [`FormatRegistry`] — what individual format crates register into.
10//!
11//! Concrete formats live in their own crates (`rff-format-avi`, ...).
12
13pub mod aac;
14pub mod av1;
15pub mod avc;
16pub mod hvc;
17
18use std::collections::HashMap;
19use std::io::{Read, Seek, Write};
20
21use rff_core::{
22    CodecId, ColorRange, Dictionary, Error, MediaType, Packet, PixelFormat, Rational, Result,
23    SampleFormat,
24};
25
26/// Description of one elementary stream within a container.
27#[derive(Debug, Clone)]
28pub struct Stream {
29    /// Position of this stream within its container.
30    pub index: usize,
31    pub codec_id: CodecId,
32    pub media_type: MediaType,
33    /// Unit of all timestamps on packets of this stream.
34    pub time_base: Rational,
35    // --- Video parameters (zero/ignored for non-video) ---
36    pub width: u32,
37    pub height: u32,
38    /// Raw pixel layout, when known (raw video). Self-describing codecs leave this `None`.
39    pub pixel_format: Option<PixelFormat>,
40    /// Luma/chroma value range. JPEG and PNG sources are always
41    /// [`ColorRange::Full`]; leaving it [`Unspecified`](ColorRange::Unspecified)
42    /// makes containers label the data limited-range, which silently corrupts it.
43    pub color_range: ColorRange,
44    // --- Audio parameters (zero/ignored for non-audio) ---
45    pub sample_rate: u32,
46    pub channels: u16,
47    /// Raw sample layout, when known (PCM). Self-describing codecs leave this `None`.
48    pub sample_format: Option<SampleFormat>,
49    /// Codec-private initialization data (e.g. H.264 SPS/PPS, OpusHead). Empty
50    /// for codecs whose bitstream is fully self-describing.
51    pub extradata: Vec<u8>,
52    /// Total stream duration in [`time_base`](Stream::time_base) units, when
53    /// the container declares it (MP4 `mdhd`, Matroska `Duration`). `None` when
54    /// unknown (live streams, TS).
55    pub duration: Option<i64>,
56    /// Total packet count (frames for video), when the container's index
57    /// declares it. `None` when unknown — never a guess.
58    pub nb_frames: Option<u64>,
59}
60
61impl Stream {
62    /// Create a stream with sane defaults for the given codec.
63    pub fn new(index: usize, codec_id: CodecId) -> Stream {
64        Stream {
65            index,
66            codec_id,
67            media_type: codec_id.media_type(),
68            time_base: Rational::new(1, 1000),
69            width: 0,
70            height: 0,
71            pixel_format: None,
72            color_range: ColorRange::Unspecified,
73            sample_rate: 0,
74            channels: 0,
75            sample_format: None,
76            extradata: Vec::new(),
77            duration: None,
78            nb_frames: None,
79        }
80    }
81}
82
83/// A byte source a demuxer reads from. (`Box<dyn Read + Send>` keeps the trait
84/// object-safe and lets inputs be files, network streams, or in-memory buffers.)
85pub type Input = Box<dyn Read + Send>;
86
87/// A byte source that can also seek — what a local file or an in-memory buffer
88/// offers and a pipe or socket does not. Blanket-implemented for every
89/// `Read + Seek` type.
90pub trait ReadSeek: Read + Seek {}
91impl<T: Read + Seek> ReadSeek for T {}
92
93/// A seekable [`Input`]. Formats whose index can sit anywhere in the file (MP4's
94/// `moov`) use it to read only that index and fetch each payload on demand,
95/// instead of buffering the whole file. It upcasts to [`Input`] for formats
96/// that only stream.
97pub type SeekInput = Box<dyn ReadSeek + Send>;
98/// A byte sink a muxer writes to.
99pub type Output = Box<dyn Write + Send>;
100
101/// A byte sink that can also seek -- a local file, an in-memory buffer; not a
102/// pipe or socket. Blanket-implemented for every `Write + Seek` type.
103pub trait WriteSeek: Write + Seek {}
104impl<T: Write + Seek> WriteSeek for T {}
105
106/// A seekable [`Output`]. Containers whose header records sizes or an index
107/// that is only known at the end (MP4 `mdat`/`moov`, Matroska Segment/Cues,
108/// AVI `movi`/`idx1`, WAV `RIFF`/`data`) use it to stream the payload as it
109/// arrives and patch the header afterwards, instead of holding the whole
110/// output in memory until the trailer (#28). It upcasts to [`Output`] for
111/// muxers that only stream.
112pub type SeekOutput = Box<dyn WriteSeek + Send>;
113
114/// Reads a container format: parses the header into [`Stream`]s, then yields
115/// [`Packet`]s until the input is exhausted.
116pub trait Demuxer: Send {
117    /// Parse the container header and return its streams.
118    fn read_header(&mut self) -> Result<Vec<Stream>>;
119
120    /// Read the next packet. Returns [`Error::Eof`] at end of input.
121    fn read_packet(&mut self) -> Result<Packet>;
122}
123
124/// Writes a container format: declare streams, write packets, finalize.
125pub trait Muxer: Send {
126    /// Hand the muxer output metadata (`-metadata title=...`). Called before
127    /// [`write_header`](Muxer::write_header); formats without a metadata slot
128    /// simply ignore it (the default).
129    fn set_metadata(&mut self, _metadata: &Dictionary) {}
130
131    /// Write the container header for the given set of streams.
132    fn write_header(&mut self, streams: &[Stream]) -> Result<()>;
133
134    /// Write one packet (its `stream_index` selects the target stream).
135    fn write_packet(&mut self, packet: &Packet) -> Result<()>;
136
137    /// Finalize the container (indexes, trailers, fixups).
138    fn write_trailer(&mut self) -> Result<()>;
139}
140
141/// Factory opening a demuxer over an input byte source.
142pub type DemuxerFactory = fn(Input) -> Box<dyn Demuxer>;
143/// Factory opening a demuxer over a [`SeekInput`] — registered alongside the
144/// format's [`DemuxerFactory`] by formats that read less when they can seek.
145pub type SeekableDemuxerFactory = fn(SeekInput) -> Box<dyn Demuxer>;
146/// Factory opening a muxer over an output byte sink.
147pub type MuxerFactory = fn(Output) -> Box<dyn Muxer>;
148/// Factory opening a muxer over a [`SeekOutput`] -- registered alongside the
149/// format's [`MuxerFactory`] by formats that can stream when they can seek.
150pub type SeekableMuxerFactory = fn(SeekOutput) -> Box<dyn Muxer>;
151/// Factory opening a muxer that owns its own file set (HLS/DASH write a
152/// playlist/manifest plus many segment files, so a single byte sink cannot
153/// model them). Receives the output path and the format options
154/// (`-hls_time 6` → `{"hls_time": "6"}`).
155pub type PathMuxerFactory = fn(&std::path::Path, &Dictionary) -> Result<Box<dyn Muxer>>;
156/// Content sniffer: scores how strongly a byte prefix looks like this format,
157/// `0` (no match) to `100` (certain) — mirrors FFmpeg's `AVInputFormat::read_probe`.
158pub type ProbeFn = fn(&[u8]) -> i32;
159
160/// What a container's **muxer** will accept: the machine-readable half of the
161/// rules each muxer already enforces when it writes a header.
162///
163/// Muxers validate at run time, and a few historically wrote a zero codec tag
164/// rather than refusing an unmappable codec. Declaring the same rules here lets
165/// a caller answer "what can this file become?" *before* opening an encoder —
166/// which is what `rff-targets` is built on.
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub struct MuxCaps {
169    /// Every codec this muxer can write. Empty for demux-only formats.
170    pub codecs: &'static [CodecId],
171    /// Most streams the container holds; `None` when there is no fixed limit.
172    pub max_streams: Option<usize>,
173    /// True when the container holds exactly ONE picture, so extra frames are
174    /// refused or dropped (png, jpeg, gif, webp, avif, jxl, av2f).
175    pub still_image: bool,
176}
177
178impl MuxCaps {
179    /// A format that cannot be written at all (demux-only).
180    pub const NONE: MuxCaps = MuxCaps {
181        codecs: &[],
182        max_streams: Some(0),
183        still_image: false,
184    };
185
186    /// A container interleaving any number of streams (mp4, matroska, mpegts).
187    pub const fn container(codecs: &'static [CodecId]) -> MuxCaps {
188        MuxCaps {
189            codecs,
190            max_streams: None,
191            still_image: false,
192        }
193    }
194
195    /// A file holding exactly one elementary stream (wav, mp3, ivf, ogg).
196    pub const fn single(codecs: &'static [CodecId]) -> MuxCaps {
197        MuxCaps {
198            codecs,
199            max_streams: Some(1),
200            still_image: false,
201        }
202    }
203
204    /// Narrow a [`single`](MuxCaps::single) file to one picture (png, jpeg).
205    pub const fn image(self) -> MuxCaps {
206        MuxCaps {
207            codecs: self.codecs,
208            max_streams: self.max_streams,
209            still_image: true,
210        }
211    }
212
213    /// Can this muxer write `id`?
214    pub fn accepts(&self, id: CodecId) -> bool {
215        self.codecs.contains(&id)
216    }
217
218    /// Can this muxer carry any stream of `media` at all?
219    pub fn accepts_media(&self, media: MediaType) -> bool {
220        self.codecs.iter().any(|c| c.media_type() == media)
221    }
222
223    /// The codecs this muxer accepts for one media category.
224    pub fn codecs_for(&self, media: MediaType) -> impl Iterator<Item = CodecId> + '_ {
225        self.codecs
226            .iter()
227            .copied()
228            .filter(move |c| c.media_type() == media)
229    }
230}
231
232/// Static description of a container format and its read/write support.
233pub struct Format {
234    /// Short name (`avi`, `mp4`, ...), as used by `ffmpeg -f <name>`.
235    pub name: &'static str,
236    pub long_name: &'static str,
237    /// File extensions that imply this format (without the dot).
238    pub extensions: &'static [&'static str],
239    /// Present if this format can be demuxed (read).
240    pub demuxer: Option<DemuxerFactory>,
241    /// Present if this format can be muxed (written).
242    pub muxer: Option<MuxerFactory>,
243    /// Present for formats that mux into a *path* (playlist + segment files)
244    /// rather than one byte sink — HLS, DASH. Mutually exclusive with `muxer`.
245    pub muxer_path: Option<PathMuxerFactory>,
246    /// Content sniffer for magic-byte detection (independent of the filename).
247    pub probe: Option<ProbeFn>,
248    /// What this format's muxer accepts. [`MuxCaps::NONE`] for demux-only
249    /// formats. Keep it in step with the muxer — `tests/mux_caps.rs` gates it.
250    pub mux_caps: MuxCaps,
251}
252
253impl Format {
254    pub fn can_demux(&self) -> bool {
255        self.demuxer.is_some()
256    }
257    pub fn can_mux(&self) -> bool {
258        self.muxer.is_some() || self.muxer_path.is_some()
259    }
260}
261
262/// Holds every container format known to a running engine.
263#[derive(Default)]
264pub struct FormatRegistry {
265    by_name: HashMap<&'static str, Format>,
266    /// Seek-aware demuxer factories, keyed by format name (see
267    /// [`register_seekable_demuxer`](Self::register_seekable_demuxer)).
268    seekable: HashMap<&'static str, SeekableDemuxerFactory>,
269    /// Seek-aware muxer factories, keyed by format name (see
270    /// [`register_seekable_muxer`](Self::register_seekable_muxer)).
271    seekable_muxers: HashMap<&'static str, SeekableMuxerFactory>,
272}
273
274impl FormatRegistry {
275    pub fn new() -> FormatRegistry {
276        FormatRegistry::default()
277    }
278
279    pub fn register(&mut self, format: Format) {
280        self.by_name.insert(format.name, format);
281    }
282
283    /// Give the format named `name` a demuxer that takes a [`SeekInput`]. Used
284    /// by [`open_demuxer_seekable`](Self::open_demuxer_seekable) when the caller
285    /// has a seekable source; the plain [`Format::demuxer`] still serves pipes
286    /// and sockets.
287    pub fn register_seekable_demuxer(
288        &mut self,
289        name: &'static str,
290        factory: SeekableDemuxerFactory,
291    ) {
292        self.seekable.insert(name, factory);
293    }
294
295    pub fn by_name(&self, name: &str) -> Option<&Format> {
296        self.by_name.get(name)
297    }
298
299    /// Find a format whose extension list contains `ext` (case-insensitive,
300    /// no leading dot). The cheap "guess by filename" path; [`probe`](Self::probe)
301    /// inspects content instead.
302    pub fn by_extension(&self, ext: &str) -> Option<&Format> {
303        let ext = ext.trim_start_matches('.').to_ascii_lowercase();
304        self.by_name
305            .values()
306            .find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)))
307    }
308
309    /// Identify a format by sniffing a byte prefix: the registered format whose
310    /// [`probe`](Format::probe) returns the highest positive score wins. Returns
311    /// `None` if nothing recognizes the data.
312    pub fn probe(&self, data: &[u8]) -> Option<&Format> {
313        self.by_name
314            .values()
315            .filter_map(|f| f.probe.map(|p| (p(data), f)))
316            .filter(|(score, _)| *score > 0)
317            .max_by_key(|(score, _)| *score)
318            .map(|(_, f)| f)
319    }
320
321    /// Open a demuxer for the format named `name`.
322    pub fn open_demuxer(&self, name: &str, input: Input) -> Result<Box<dyn Demuxer>> {
323        self.by_name
324            .get(name)
325            .and_then(|f| f.demuxer)
326            .map(|factory| factory(input))
327            .ok_or_else(|| Error::DemuxerNotFound(name.to_string()))
328    }
329
330    /// Open a demuxer over a seekable input: the format's seek-aware demuxer if
331    /// it registered one, otherwise its plain streaming demuxer (a seekable
332    /// source is still a byte stream). Callers that hold a file should prefer
333    /// this — it is what lets MP4 read only `moov` instead of the whole file.
334    pub fn open_demuxer_seekable(&self, name: &str, input: SeekInput) -> Result<Box<dyn Demuxer>> {
335        if !self.by_name.get(name).is_some_and(Format::can_demux) {
336            return Err(Error::DemuxerNotFound(name.to_string()));
337        }
338        match self.seekable.get(name) {
339            Some(factory) => Ok(factory(input)),
340            None => self.open_demuxer(name, input),
341        }
342    }
343
344    /// Give the format named `name` a muxer that takes a [`SeekOutput`]. Used
345    /// by [`open_muxer_seekable`](Self::open_muxer_seekable) when the caller
346    /// writes to something seekable; the plain [`Format::muxer`] still serves
347    /// pipes and sockets.
348    pub fn register_seekable_muxer(&mut self, name: &'static str, factory: SeekableMuxerFactory) {
349        self.seekable_muxers.insert(name, factory);
350    }
351
352    /// Open a muxer over a seekable output: the format's seek-aware muxer if it
353    /// registered one, otherwise its plain muxer (a seekable sink is still a
354    /// byte sink). Callers writing a file should prefer this -- it is what lets
355    /// MP4/Matroska/AVI/WAV stream instead of buffering the whole output.
356    pub fn open_muxer_seekable(&self, name: &str, output: SeekOutput) -> Result<Box<dyn Muxer>> {
357        if !self.by_name.get(name).is_some_and(|f| f.muxer.is_some()) {
358            return Err(Error::MuxerNotFound(name.to_string()));
359        }
360        match self.seekable_muxers.get(name) {
361            Some(factory) => Ok(factory(output)),
362            None => self.open_muxer(name, output),
363        }
364    }
365
366    /// Open a muxer for the format named `name`.
367    pub fn open_muxer(&self, name: &str, output: Output) -> Result<Box<dyn Muxer>> {
368        self.by_name
369            .get(name)
370            .and_then(|f| f.muxer)
371            .map(|factory| factory(output))
372            .ok_or_else(|| Error::MuxerNotFound(name.to_string()))
373    }
374
375    pub fn iter(&self) -> impl Iterator<Item = &Format> {
376        self.by_name.values()
377    }
378
379    pub fn len(&self) -> usize {
380        self.by_name.len()
381    }
382
383    pub fn is_empty(&self) -> bool {
384        self.by_name.is_empty()
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use std::io::Cursor;
392
393    /// Reports which factory built it via the stream count.
394    struct Tagged(usize);
395    impl Demuxer for Tagged {
396        fn read_header(&mut self) -> Result<Vec<Stream>> {
397            Ok((0..self.0).map(|i| Stream::new(i, CodecId::None)).collect())
398        }
399        fn read_packet(&mut self) -> Result<rff_core::Packet> {
400            Err(Error::Eof)
401        }
402    }
403
404    fn format(name: &'static str) -> Format {
405        Format {
406            name,
407            long_name: name,
408            extensions: &[],
409            demuxer: Some(|_| Box::new(Tagged(1))),
410            muxer: None,
411            muxer_path: None,
412            probe: None,
413            mux_caps: MuxCaps::NONE,
414        }
415    }
416
417    fn streams(mut d: Box<dyn Demuxer>) -> usize {
418        d.read_header().unwrap().len()
419    }
420
421    #[test]
422    fn seekable_open_prefers_the_seekable_factory_and_falls_back() {
423        let mut r = FormatRegistry::new();
424        r.register(format("plain"));
425        r.register(format("seeky"));
426        r.register_seekable_demuxer("seeky", |_| Box::new(Tagged(2)));
427        let input = || -> SeekInput { Box::new(Cursor::new(vec![0u8; 4])) };
428        assert_eq!(
429            streams(r.open_demuxer_seekable("seeky", input()).unwrap()),
430            2
431        );
432        // No seekable factory: the streaming one serves the seekable input.
433        assert_eq!(
434            streams(r.open_demuxer_seekable("plain", input()).unwrap()),
435            1
436        );
437        // A plain stream open never takes the seekable path.
438        assert_eq!(
439            streams(
440                r.open_demuxer("seeky", Box::new(Cursor::new(vec![])))
441                    .unwrap()
442            ),
443            1
444        );
445        assert!(matches!(
446            r.open_demuxer_seekable("missing", input()),
447            Err(Error::DemuxerNotFound(_))
448        ));
449    }
450
451    /// Reports which factory built it through the error `write_trailer` returns.
452    struct TaggedMux(&'static str);
453    impl Muxer for TaggedMux {
454        fn write_header(&mut self, _: &[Stream]) -> Result<()> {
455            Ok(())
456        }
457        fn write_packet(&mut self, _: &rff_core::Packet) -> Result<()> {
458            Ok(())
459        }
460        fn write_trailer(&mut self) -> Result<()> {
461            Err(Error::invalid(self.0))
462        }
463    }
464
465    fn tag(mut m: Box<dyn Muxer>) -> String {
466        m.write_trailer().unwrap_err().to_string()
467    }
468
469    #[test]
470    fn seekable_muxer_open_prefers_the_seekable_factory_and_falls_back() {
471        let mut r = FormatRegistry::new();
472        let with_muxer = |name| Format {
473            muxer: Some(|_| Box::new(TaggedMux("plain"))),
474            ..format(name)
475        };
476        r.register(with_muxer("plain"));
477        r.register(with_muxer("seeky"));
478        r.register(format("demux_only"));
479        r.register_seekable_muxer("seeky", |_| Box::new(TaggedMux("seekable")));
480        let output = || -> SeekOutput { Box::new(Cursor::new(Vec::new())) };
481        assert!(tag(r.open_muxer_seekable("seeky", output()).unwrap()).contains("seekable"));
482        // No seekable factory: the plain one serves the seekable output.
483        assert!(tag(r.open_muxer_seekable("plain", output()).unwrap()).contains("plain"));
484        // A plain open never takes the seekable path.
485        assert!(tag(r.open_muxer("seeky", Box::new(Vec::new())).unwrap()).contains("plain"));
486        assert!(matches!(
487            r.open_muxer_seekable("demux_only", output()),
488            Err(Error::MuxerNotFound(_))
489        ));
490        assert!(matches!(
491            r.open_muxer_seekable("missing", output()),
492            Err(Error::MuxerNotFound(_))
493        ));
494    }
495}