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/// Reads a container format: parses the header into [`Stream`]s, then yields
102/// [`Packet`]s until the input is exhausted.
103pub trait Demuxer: Send {
104 /// Parse the container header and return its streams.
105 fn read_header(&mut self) -> Result<Vec<Stream>>;
106
107 /// Read the next packet. Returns [`Error::Eof`] at end of input.
108 fn read_packet(&mut self) -> Result<Packet>;
109}
110
111/// Writes a container format: declare streams, write packets, finalize.
112pub trait Muxer: Send {
113 /// Hand the muxer output metadata (`-metadata title=...`). Called before
114 /// [`write_header`](Muxer::write_header); formats without a metadata slot
115 /// simply ignore it (the default).
116 fn set_metadata(&mut self, _metadata: &Dictionary) {}
117
118 /// Write the container header for the given set of streams.
119 fn write_header(&mut self, streams: &[Stream]) -> Result<()>;
120
121 /// Write one packet (its `stream_index` selects the target stream).
122 fn write_packet(&mut self, packet: &Packet) -> Result<()>;
123
124 /// Finalize the container (indexes, trailers, fixups).
125 fn write_trailer(&mut self) -> Result<()>;
126}
127
128/// Factory opening a demuxer over an input byte source.
129pub type DemuxerFactory = fn(Input) -> Box<dyn Demuxer>;
130/// Factory opening a demuxer over a [`SeekInput`] — registered alongside the
131/// format's [`DemuxerFactory`] by formats that read less when they can seek.
132pub type SeekableDemuxerFactory = fn(SeekInput) -> Box<dyn Demuxer>;
133/// Factory opening a muxer over an output byte sink.
134pub type MuxerFactory = fn(Output) -> Box<dyn Muxer>;
135/// Factory opening a muxer that owns its own file set (HLS/DASH write a
136/// playlist/manifest plus many segment files, so a single byte sink cannot
137/// model them). Receives the output path and the format options
138/// (`-hls_time 6` → `{"hls_time": "6"}`).
139pub type PathMuxerFactory = fn(&std::path::Path, &Dictionary) -> Result<Box<dyn Muxer>>;
140/// Content sniffer: scores how strongly a byte prefix looks like this format,
141/// `0` (no match) to `100` (certain) — mirrors FFmpeg's `AVInputFormat::read_probe`.
142pub type ProbeFn = fn(&[u8]) -> i32;
143
144/// What a container's **muxer** will accept: the machine-readable half of the
145/// rules each muxer already enforces when it writes a header.
146///
147/// Muxers validate at run time, and a few historically wrote a zero codec tag
148/// rather than refusing an unmappable codec. Declaring the same rules here lets
149/// a caller answer "what can this file become?" *before* opening an encoder —
150/// which is what `rff-targets` is built on.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct MuxCaps {
153 /// Every codec this muxer can write. Empty for demux-only formats.
154 pub codecs: &'static [CodecId],
155 /// Most streams the container holds; `None` when there is no fixed limit.
156 pub max_streams: Option<usize>,
157 /// True when the container holds exactly ONE picture, so extra frames are
158 /// refused or dropped (png, jpeg, gif, webp, avif, jxl, av2f).
159 pub still_image: bool,
160}
161
162impl MuxCaps {
163 /// A format that cannot be written at all (demux-only).
164 pub const NONE: MuxCaps = MuxCaps {
165 codecs: &[],
166 max_streams: Some(0),
167 still_image: false,
168 };
169
170 /// A container interleaving any number of streams (mp4, matroska, mpegts).
171 pub const fn container(codecs: &'static [CodecId]) -> MuxCaps {
172 MuxCaps {
173 codecs,
174 max_streams: None,
175 still_image: false,
176 }
177 }
178
179 /// A file holding exactly one elementary stream (wav, mp3, ivf, ogg).
180 pub const fn single(codecs: &'static [CodecId]) -> MuxCaps {
181 MuxCaps {
182 codecs,
183 max_streams: Some(1),
184 still_image: false,
185 }
186 }
187
188 /// Narrow a [`single`](MuxCaps::single) file to one picture (png, jpeg).
189 pub const fn image(self) -> MuxCaps {
190 MuxCaps {
191 codecs: self.codecs,
192 max_streams: self.max_streams,
193 still_image: true,
194 }
195 }
196
197 /// Can this muxer write `id`?
198 pub fn accepts(&self, id: CodecId) -> bool {
199 self.codecs.contains(&id)
200 }
201
202 /// Can this muxer carry any stream of `media` at all?
203 pub fn accepts_media(&self, media: MediaType) -> bool {
204 self.codecs.iter().any(|c| c.media_type() == media)
205 }
206
207 /// The codecs this muxer accepts for one media category.
208 pub fn codecs_for(&self, media: MediaType) -> impl Iterator<Item = CodecId> + '_ {
209 self.codecs
210 .iter()
211 .copied()
212 .filter(move |c| c.media_type() == media)
213 }
214}
215
216/// Static description of a container format and its read/write support.
217pub struct Format {
218 /// Short name (`avi`, `mp4`, ...), as used by `ffmpeg -f <name>`.
219 pub name: &'static str,
220 pub long_name: &'static str,
221 /// File extensions that imply this format (without the dot).
222 pub extensions: &'static [&'static str],
223 /// Present if this format can be demuxed (read).
224 pub demuxer: Option<DemuxerFactory>,
225 /// Present if this format can be muxed (written).
226 pub muxer: Option<MuxerFactory>,
227 /// Present for formats that mux into a *path* (playlist + segment files)
228 /// rather than one byte sink — HLS, DASH. Mutually exclusive with `muxer`.
229 pub muxer_path: Option<PathMuxerFactory>,
230 /// Content sniffer for magic-byte detection (independent of the filename).
231 pub probe: Option<ProbeFn>,
232 /// What this format's muxer accepts. [`MuxCaps::NONE`] for demux-only
233 /// formats. Keep it in step with the muxer — `tests/mux_caps.rs` gates it.
234 pub mux_caps: MuxCaps,
235}
236
237impl Format {
238 pub fn can_demux(&self) -> bool {
239 self.demuxer.is_some()
240 }
241 pub fn can_mux(&self) -> bool {
242 self.muxer.is_some() || self.muxer_path.is_some()
243 }
244}
245
246/// Holds every container format known to a running engine.
247#[derive(Default)]
248pub struct FormatRegistry {
249 by_name: HashMap<&'static str, Format>,
250 /// Seek-aware demuxer factories, keyed by format name (see
251 /// [`register_seekable_demuxer`](Self::register_seekable_demuxer)).
252 seekable: HashMap<&'static str, SeekableDemuxerFactory>,
253}
254
255impl FormatRegistry {
256 pub fn new() -> FormatRegistry {
257 FormatRegistry::default()
258 }
259
260 pub fn register(&mut self, format: Format) {
261 self.by_name.insert(format.name, format);
262 }
263
264 /// Give the format named `name` a demuxer that takes a [`SeekInput`]. Used
265 /// by [`open_demuxer_seekable`](Self::open_demuxer_seekable) when the caller
266 /// has a seekable source; the plain [`Format::demuxer`] still serves pipes
267 /// and sockets.
268 pub fn register_seekable_demuxer(
269 &mut self,
270 name: &'static str,
271 factory: SeekableDemuxerFactory,
272 ) {
273 self.seekable.insert(name, factory);
274 }
275
276 pub fn by_name(&self, name: &str) -> Option<&Format> {
277 self.by_name.get(name)
278 }
279
280 /// Find a format whose extension list contains `ext` (case-insensitive,
281 /// no leading dot). The cheap "guess by filename" path; [`probe`](Self::probe)
282 /// inspects content instead.
283 pub fn by_extension(&self, ext: &str) -> Option<&Format> {
284 let ext = ext.trim_start_matches('.').to_ascii_lowercase();
285 self.by_name
286 .values()
287 .find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)))
288 }
289
290 /// Identify a format by sniffing a byte prefix: the registered format whose
291 /// [`probe`](Format::probe) returns the highest positive score wins. Returns
292 /// `None` if nothing recognizes the data.
293 pub fn probe(&self, data: &[u8]) -> Option<&Format> {
294 self.by_name
295 .values()
296 .filter_map(|f| f.probe.map(|p| (p(data), f)))
297 .filter(|(score, _)| *score > 0)
298 .max_by_key(|(score, _)| *score)
299 .map(|(_, f)| f)
300 }
301
302 /// Open a demuxer for the format named `name`.
303 pub fn open_demuxer(&self, name: &str, input: Input) -> Result<Box<dyn Demuxer>> {
304 self.by_name
305 .get(name)
306 .and_then(|f| f.demuxer)
307 .map(|factory| factory(input))
308 .ok_or_else(|| Error::DemuxerNotFound(name.to_string()))
309 }
310
311 /// Open a demuxer over a seekable input: the format's seek-aware demuxer if
312 /// it registered one, otherwise its plain streaming demuxer (a seekable
313 /// source is still a byte stream). Callers that hold a file should prefer
314 /// this — it is what lets MP4 read only `moov` instead of the whole file.
315 pub fn open_demuxer_seekable(&self, name: &str, input: SeekInput) -> Result<Box<dyn Demuxer>> {
316 if !self.by_name.get(name).is_some_and(Format::can_demux) {
317 return Err(Error::DemuxerNotFound(name.to_string()));
318 }
319 match self.seekable.get(name) {
320 Some(factory) => Ok(factory(input)),
321 None => self.open_demuxer(name, input),
322 }
323 }
324
325 /// Open a muxer for the format named `name`.
326 pub fn open_muxer(&self, name: &str, output: Output) -> Result<Box<dyn Muxer>> {
327 self.by_name
328 .get(name)
329 .and_then(|f| f.muxer)
330 .map(|factory| factory(output))
331 .ok_or_else(|| Error::MuxerNotFound(name.to_string()))
332 }
333
334 pub fn iter(&self) -> impl Iterator<Item = &Format> {
335 self.by_name.values()
336 }
337
338 pub fn len(&self) -> usize {
339 self.by_name.len()
340 }
341
342 pub fn is_empty(&self) -> bool {
343 self.by_name.is_empty()
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use std::io::Cursor;
351
352 /// Reports which factory built it via the stream count.
353 struct Tagged(usize);
354 impl Demuxer for Tagged {
355 fn read_header(&mut self) -> Result<Vec<Stream>> {
356 Ok((0..self.0).map(|i| Stream::new(i, CodecId::None)).collect())
357 }
358 fn read_packet(&mut self) -> Result<rff_core::Packet> {
359 Err(Error::Eof)
360 }
361 }
362
363 fn format(name: &'static str) -> Format {
364 Format {
365 name,
366 long_name: name,
367 extensions: &[],
368 demuxer: Some(|_| Box::new(Tagged(1))),
369 muxer: None,
370 muxer_path: None,
371 probe: None,
372 mux_caps: MuxCaps::NONE,
373 }
374 }
375
376 fn streams(mut d: Box<dyn Demuxer>) -> usize {
377 d.read_header().unwrap().len()
378 }
379
380 #[test]
381 fn seekable_open_prefers_the_seekable_factory_and_falls_back() {
382 let mut r = FormatRegistry::new();
383 r.register(format("plain"));
384 r.register(format("seeky"));
385 r.register_seekable_demuxer("seeky", |_| Box::new(Tagged(2)));
386 let input = || -> SeekInput { Box::new(Cursor::new(vec![0u8; 4])) };
387 assert_eq!(
388 streams(r.open_demuxer_seekable("seeky", input()).unwrap()),
389 2
390 );
391 // No seekable factory: the streaming one serves the seekable input.
392 assert_eq!(
393 streams(r.open_demuxer_seekable("plain", input()).unwrap()),
394 1
395 );
396 // A plain stream open never takes the seekable path.
397 assert_eq!(
398 streams(
399 r.open_demuxer("seeky", Box::new(Cursor::new(vec![])))
400 .unwrap()
401 ),
402 1
403 );
404 assert!(matches!(
405 r.open_demuxer_seekable("missing", input()),
406 Err(Error::DemuxerNotFound(_))
407 ));
408 }
409}