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;
16
17use std::collections::HashMap;
18use std::io::{Read, Write};
19
20use rff_core::{
21    CodecId, ColorRange, Dictionary, Error, MediaType, Packet, PixelFormat, Rational, Result,
22    SampleFormat,
23};
24
25/// Description of one elementary stream within a container.
26#[derive(Debug, Clone)]
27pub struct Stream {
28    /// Position of this stream within its container.
29    pub index: usize,
30    pub codec_id: CodecId,
31    pub media_type: MediaType,
32    /// Unit of all timestamps on packets of this stream.
33    pub time_base: Rational,
34    // --- Video parameters (zero/ignored for non-video) ---
35    pub width: u32,
36    pub height: u32,
37    /// Raw pixel layout, when known (raw video). Self-describing codecs leave this `None`.
38    pub pixel_format: Option<PixelFormat>,
39    /// Luma/chroma value range. JPEG and PNG sources are always
40    /// [`ColorRange::Full`]; leaving it [`Unspecified`](ColorRange::Unspecified)
41    /// makes containers label the data limited-range, which silently corrupts it.
42    pub color_range: ColorRange,
43    // --- Audio parameters (zero/ignored for non-audio) ---
44    pub sample_rate: u32,
45    pub channels: u16,
46    /// Raw sample layout, when known (PCM). Self-describing codecs leave this `None`.
47    pub sample_format: Option<SampleFormat>,
48    /// Codec-private initialization data (e.g. H.264 SPS/PPS, OpusHead). Empty
49    /// for codecs whose bitstream is fully self-describing.
50    pub extradata: Vec<u8>,
51    /// Total stream duration in [`time_base`](Stream::time_base) units, when
52    /// the container declares it (MP4 `mdhd`, Matroska `Duration`). `None` when
53    /// unknown (live streams, TS).
54    pub duration: Option<i64>,
55    /// Total packet count (frames for video), when the container's index
56    /// declares it. `None` when unknown — never a guess.
57    pub nb_frames: Option<u64>,
58}
59
60impl Stream {
61    /// Create a stream with sane defaults for the given codec.
62    pub fn new(index: usize, codec_id: CodecId) -> Stream {
63        Stream {
64            index,
65            codec_id,
66            media_type: codec_id.media_type(),
67            time_base: Rational::new(1, 1000),
68            width: 0,
69            height: 0,
70            pixel_format: None,
71            color_range: ColorRange::Unspecified,
72            sample_rate: 0,
73            channels: 0,
74            sample_format: None,
75            extradata: Vec::new(),
76            duration: None,
77            nb_frames: None,
78        }
79    }
80}
81
82/// A byte source a demuxer reads from. (`Box<dyn Read + Send>` keeps the trait
83/// object-safe and lets inputs be files, network streams, or in-memory buffers.)
84pub type Input = Box<dyn Read + Send>;
85/// A byte sink a muxer writes to.
86pub type Output = Box<dyn Write + Send>;
87
88/// Reads a container format: parses the header into [`Stream`]s, then yields
89/// [`Packet`]s until the input is exhausted.
90pub trait Demuxer: Send {
91    /// Parse the container header and return its streams.
92    fn read_header(&mut self) -> Result<Vec<Stream>>;
93
94    /// Read the next packet. Returns [`Error::Eof`] at end of input.
95    fn read_packet(&mut self) -> Result<Packet>;
96}
97
98/// Writes a container format: declare streams, write packets, finalize.
99pub trait Muxer: Send {
100    /// Hand the muxer output metadata (`-metadata title=...`). Called before
101    /// [`write_header`](Muxer::write_header); formats without a metadata slot
102    /// simply ignore it (the default).
103    fn set_metadata(&mut self, _metadata: &Dictionary) {}
104
105    /// Write the container header for the given set of streams.
106    fn write_header(&mut self, streams: &[Stream]) -> Result<()>;
107
108    /// Write one packet (its `stream_index` selects the target stream).
109    fn write_packet(&mut self, packet: &Packet) -> Result<()>;
110
111    /// Finalize the container (indexes, trailers, fixups).
112    fn write_trailer(&mut self) -> Result<()>;
113}
114
115/// Factory opening a demuxer over an input byte source.
116pub type DemuxerFactory = fn(Input) -> Box<dyn Demuxer>;
117/// Factory opening a muxer over an output byte sink.
118pub type MuxerFactory = fn(Output) -> Box<dyn Muxer>;
119/// Factory opening a muxer that owns its own file set (HLS/DASH write a
120/// playlist/manifest plus many segment files, so a single byte sink cannot
121/// model them). Receives the output path and the format options
122/// (`-hls_time 6` → `{"hls_time": "6"}`).
123pub type PathMuxerFactory =
124    fn(&std::path::Path, &Dictionary) -> Result<Box<dyn Muxer>>;
125/// Content sniffer: scores how strongly a byte prefix looks like this format,
126/// `0` (no match) to `100` (certain) — mirrors FFmpeg's `AVInputFormat::read_probe`.
127pub type ProbeFn = fn(&[u8]) -> i32;
128
129/// What a container's **muxer** will accept: the machine-readable half of the
130/// rules each muxer already enforces when it writes a header.
131///
132/// Muxers validate at run time, and a few historically wrote a zero codec tag
133/// rather than refusing an unmappable codec. Declaring the same rules here lets
134/// a caller answer "what can this file become?" *before* opening an encoder —
135/// which is what `rff-targets` is built on.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct MuxCaps {
138    /// Every codec this muxer can write. Empty for demux-only formats.
139    pub codecs: &'static [CodecId],
140    /// Most streams the container holds; `None` when there is no fixed limit.
141    pub max_streams: Option<usize>,
142    /// True when the container holds exactly ONE picture, so extra frames are
143    /// refused or dropped (png, jpeg, gif, webp, avif, jxl, av2f).
144    pub still_image: bool,
145}
146
147impl MuxCaps {
148    /// A format that cannot be written at all (demux-only).
149    pub const NONE: MuxCaps = MuxCaps {
150        codecs: &[],
151        max_streams: Some(0),
152        still_image: false,
153    };
154
155    /// A container interleaving any number of streams (mp4, matroska, mpegts).
156    pub const fn container(codecs: &'static [CodecId]) -> MuxCaps {
157        MuxCaps {
158            codecs,
159            max_streams: None,
160            still_image: false,
161        }
162    }
163
164    /// A file holding exactly one elementary stream (wav, mp3, ivf, ogg).
165    pub const fn single(codecs: &'static [CodecId]) -> MuxCaps {
166        MuxCaps {
167            codecs,
168            max_streams: Some(1),
169            still_image: false,
170        }
171    }
172
173    /// Narrow a [`single`](MuxCaps::single) file to one picture (png, jpeg).
174    pub const fn image(self) -> MuxCaps {
175        MuxCaps {
176            codecs: self.codecs,
177            max_streams: self.max_streams,
178            still_image: true,
179        }
180    }
181
182    /// Can this muxer write `id`?
183    pub fn accepts(&self, id: CodecId) -> bool {
184        self.codecs.contains(&id)
185    }
186
187    /// Can this muxer carry any stream of `media` at all?
188    pub fn accepts_media(&self, media: MediaType) -> bool {
189        self.codecs.iter().any(|c| c.media_type() == media)
190    }
191
192    /// The codecs this muxer accepts for one media category.
193    pub fn codecs_for(&self, media: MediaType) -> impl Iterator<Item = CodecId> + '_ {
194        self.codecs
195            .iter()
196            .copied()
197            .filter(move |c| c.media_type() == media)
198    }
199}
200
201/// Static description of a container format and its read/write support.
202pub struct Format {
203    /// Short name (`avi`, `mp4`, ...), as used by `ffmpeg -f <name>`.
204    pub name: &'static str,
205    pub long_name: &'static str,
206    /// File extensions that imply this format (without the dot).
207    pub extensions: &'static [&'static str],
208    /// Present if this format can be demuxed (read).
209    pub demuxer: Option<DemuxerFactory>,
210    /// Present if this format can be muxed (written).
211    pub muxer: Option<MuxerFactory>,
212    /// Present for formats that mux into a *path* (playlist + segment files)
213    /// rather than one byte sink — HLS, DASH. Mutually exclusive with `muxer`.
214    pub muxer_path: Option<PathMuxerFactory>,
215    /// Content sniffer for magic-byte detection (independent of the filename).
216    pub probe: Option<ProbeFn>,
217    /// What this format's muxer accepts. [`MuxCaps::NONE`] for demux-only
218    /// formats. Keep it in step with the muxer — `tests/mux_caps.rs` gates it.
219    pub mux_caps: MuxCaps,
220}
221
222impl Format {
223    pub fn can_demux(&self) -> bool {
224        self.demuxer.is_some()
225    }
226    pub fn can_mux(&self) -> bool {
227        self.muxer.is_some() || self.muxer_path.is_some()
228    }
229}
230
231/// Holds every container format known to a running engine.
232#[derive(Default)]
233pub struct FormatRegistry {
234    by_name: HashMap<&'static str, Format>,
235}
236
237impl FormatRegistry {
238    pub fn new() -> FormatRegistry {
239        FormatRegistry::default()
240    }
241
242    pub fn register(&mut self, format: Format) {
243        self.by_name.insert(format.name, format);
244    }
245
246    pub fn by_name(&self, name: &str) -> Option<&Format> {
247        self.by_name.get(name)
248    }
249
250    /// Find a format whose extension list contains `ext` (case-insensitive,
251    /// no leading dot). The cheap "guess by filename" path; [`probe`](Self::probe)
252    /// inspects content instead.
253    pub fn by_extension(&self, ext: &str) -> Option<&Format> {
254        let ext = ext.trim_start_matches('.').to_ascii_lowercase();
255        self.by_name
256            .values()
257            .find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)))
258    }
259
260    /// Identify a format by sniffing a byte prefix: the registered format whose
261    /// [`probe`](Format::probe) returns the highest positive score wins. Returns
262    /// `None` if nothing recognizes the data.
263    pub fn probe(&self, data: &[u8]) -> Option<&Format> {
264        self.by_name
265            .values()
266            .filter_map(|f| f.probe.map(|p| (p(data), f)))
267            .filter(|(score, _)| *score > 0)
268            .max_by_key(|(score, _)| *score)
269            .map(|(_, f)| f)
270    }
271
272    /// Open a demuxer for the format named `name`.
273    pub fn open_demuxer(&self, name: &str, input: Input) -> Result<Box<dyn Demuxer>> {
274        self.by_name
275            .get(name)
276            .and_then(|f| f.demuxer)
277            .map(|factory| factory(input))
278            .ok_or_else(|| Error::DemuxerNotFound(name.to_string()))
279    }
280
281    /// Open a muxer for the format named `name`.
282    pub fn open_muxer(&self, name: &str, output: Output) -> Result<Box<dyn Muxer>> {
283        self.by_name
284            .get(name)
285            .and_then(|f| f.muxer)
286            .map(|factory| factory(output))
287            .ok_or_else(|| Error::MuxerNotFound(name.to_string()))
288    }
289
290    pub fn iter(&self) -> impl Iterator<Item = &Format> {
291        self.by_name.values()
292    }
293
294    pub fn len(&self) -> usize {
295        self.by_name.len()
296    }
297
298    pub fn is_empty(&self) -> bool {
299        self.by_name.is_empty()
300    }
301}