Skip to main content

oxideav_container/
lib.rs

1//! Container traits (demuxer + muxer) and a registry.
2//!
3//! This crate is deliberately dependency-light: it defines the abstract
4//! [`Demuxer`] / [`Muxer`] traits that every container implementation
5//! (oxideav-mp4, oxideav-mkv, oxideav-flac, oxideav-ogg, …) fulfils,
6//! plus a [`registry::ContainerRegistry`] that the consumers of the
7//! framework use to pick a demuxer by probe bytes or filename hint.
8
9pub mod registry;
10
11use oxideav_core::{CodecResolver, Packet, Result, StreamInfo};
12use std::io::{Read, Seek, Write};
13
14/// Reads a container and emits packets per stream.
15pub trait Demuxer: Send {
16    /// Name of the container format (e.g., `"wav"`).
17    fn format_name(&self) -> &str;
18
19    /// Streams in this container. Stable across the lifetime of the demuxer.
20    fn streams(&self) -> &[StreamInfo];
21
22    /// Read the next packet from any stream. Returns `Error::Eof` at end.
23    fn next_packet(&mut self) -> Result<Packet>;
24
25    /// Hint that only the listed stream indices will be consumed by the
26    /// pipeline. Demuxers that can efficiently skip inactive streams at
27    /// the container level (e.g., MKV cluster-aware, MP4 trak-aware)
28    /// should override this. The default is a no-op — the pipeline
29    /// drops unwanted packets on the floor.
30    fn set_active_streams(&mut self, _indices: &[u32]) {}
31
32    /// Seek to the nearest keyframe at or before `pts` (in the given
33    /// stream's time base). Returns the actual timestamp seeked to, or
34    /// `Error::Unsupported` if this demuxer can't seek.
35    fn seek_to(&mut self, _stream_index: u32, _pts: i64) -> Result<i64> {
36        Err(oxideav_core::Error::unsupported(
37            "this demuxer does not support seeking",
38        ))
39    }
40
41    /// Container-level metadata as ordered (key, value) pairs.
42    /// Keys follow a loose convention borrowed from Vorbis comments:
43    /// `title`, `artist`, `album`, `comment`, `date`, `sample_name:<n>`,
44    /// `channels`, `n_patterns`, etc. Demuxers that carry no metadata
45    /// return an empty slice (the default).
46    fn metadata(&self) -> &[(String, String)] {
47        &[]
48    }
49    /// Container-level duration, if known. Default is `None` — callers
50    /// may fall back to the longest per-stream duration. Expressed as
51    /// microseconds for portability; convert to seconds at the edge.
52    fn duration_micros(&self) -> Option<i64> {
53        None
54    }
55
56    /// Attached pictures (cover art, artist photos, ...) embedded in
57    /// the container. Returns an empty slice (the default) when the
58    /// container carries none or doesn't support them. Containers that
59    /// do — ID3v2 on MP3, `METADATA_BLOCK_PICTURE` on FLAC, `covr`
60    /// atoms on MP4, etc. — override this to expose the images.
61    fn attached_pictures(&self) -> &[oxideav_core::AttachedPicture] {
62        &[]
63    }
64}
65
66/// Writes packets into a container.
67pub trait Muxer: Send {
68    fn format_name(&self) -> &str;
69
70    /// Write the container header. Must be called after stream configuration
71    /// and before the first `write_packet`.
72    fn write_header(&mut self) -> Result<()>;
73
74    fn write_packet(&mut self, packet: &Packet) -> Result<()>;
75
76    /// Finalize the file (write index, patch in total sizes, etc.).
77    fn write_trailer(&mut self) -> Result<()>;
78}
79
80/// Factory that tries to open a stream as a particular container format.
81///
82/// Implementations should read the minimum needed to confirm the format and
83/// return `Error::InvalidData` if the stream is not in this format.
84///
85/// The `codecs` parameter carries a resolver that converts container-
86/// level codec tags (FourCCs, WAVEFORMATEX wFormatTag, Matroska
87/// CodecIDs, …) into [`CodecId`](oxideav_core::CodecId) values. Demuxers
88/// that previously maintained hand-written tag-to-codec-id tables
89/// (AVI's codec_map, MP4's sample-entry map, MKV's codec-id map)
90/// should call `codecs.resolve_tag(tag, first_packet_bytes)` instead,
91/// letting the codec crates own their own tag claims with priority +
92/// optional probes. Pass
93/// [`NullCodecResolver`](oxideav_core::NullCodecResolver) when you
94/// don't have a real registry on hand (tests, stub callers).
95pub type OpenDemuxerFn =
96    fn(input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>>;
97
98/// Factory that creates a muxer for a set of streams.
99pub type OpenMuxerFn =
100    fn(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>>;
101
102/// Information passed to a content-based [`ProbeFn`].
103///
104/// `buf` holds the first few KB of the input — enough to recognise the
105/// magic bytes of any container we know about. `ext` carries the file
106/// extension as a hint (lowercase, no leading dot); some containers
107/// (raw MP3 with no ID3v2, headerless tracker formats) need it to break
108/// ties with otherwise weak signatures.
109pub struct ProbeData<'a> {
110    pub buf: &'a [u8],
111    pub ext: Option<&'a str>,
112}
113
114/// Confidence score returned by a [`ProbeFn`]. `0` means no match.
115/// Higher means more certain. Conventional values:
116///
117/// * `100` – unambiguous magic bytes at a known offset
118/// * `75`  – signature match corroborated by file extension
119/// * `50`  – signature match without extension corroboration
120/// * `25`  – extension match only (no content signature available)
121pub type ProbeScore = u8;
122
123/// Maximum probe score (alias for `100`).
124pub const MAX_PROBE_SCORE: ProbeScore = 100;
125/// Default score returned when only the file extension matches.
126pub const PROBE_SCORE_EXTENSION: ProbeScore = 25;
127
128/// Content-based format detection function.
129///
130/// Returns a [`ProbeScore`] in `0..=100`. Implementations should be
131/// pure (no I/O, no allocation beyond the stack) and fast — they may
132/// be invoked once per registered demuxer on every input file.
133pub type ProbeFn = fn(probe: &ProbeData) -> ProbeScore;
134
135/// Convenience trait bundle for seekable readers.
136pub trait ReadSeek: Read + Seek + Send {}
137impl<T: Read + Seek + Send> ReadSeek for T {}
138
139/// Convenience trait bundle for seekable writers.
140pub trait WriteSeek: Write + Seek + Send {}
141impl<T: Write + Seek + Send> WriteSeek for T {}
142
143pub use registry::ContainerRegistry;
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use oxideav_core::Error;
149
150    struct DummyDemuxer;
151
152    impl Demuxer for DummyDemuxer {
153        fn format_name(&self) -> &str {
154            "dummy"
155        }
156        fn streams(&self) -> &[StreamInfo] {
157            &[]
158        }
159        fn next_packet(&mut self) -> Result<Packet> {
160            Err(Error::Eof)
161        }
162    }
163
164    #[test]
165    fn default_seek_to_is_unsupported() {
166        let mut d = DummyDemuxer;
167        match d.seek_to(0, 0) {
168            Err(Error::Unsupported(_)) => {}
169            other => panic!(
170                "expected default seek_to to return Unsupported, got {:?}",
171                other
172            ),
173        }
174    }
175}