Skip to main content

oxideav_core/
stream.rs

1//! Stream metadata shared between containers and codecs.
2
3use crate::format::{MediaType, PixelFormat, SampleFormat};
4use crate::options::CodecOptions;
5use crate::rational::Rational;
6use crate::time::TimeBase;
7
8/// A stable identifier for a codec. Codec crates register a `CodecId` so the
9/// codec registry can look them up by name.
10#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11pub struct CodecId(pub String);
12
13impl CodecId {
14    pub fn new(s: impl Into<String>) -> Self {
15        Self(s.into())
16    }
17
18    pub fn as_str(&self) -> &str {
19        &self.0
20    }
21}
22
23impl From<&str> for CodecId {
24    fn from(s: &str) -> Self {
25        Self(s.to_owned())
26    }
27}
28
29impl std::fmt::Display for CodecId {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}", self.0)
32    }
33}
34
35/// A codec identifier scoped to a container format — the thing a
36/// demuxer reads out of the file to name a codec. Resolved to a
37/// [`CodecId`] by the codec registry.
38///
39/// Centralising these in the registry (instead of each container
40/// hand-rolling its own FourCC → CodecId table) lets:
41///
42/// * a codec crate declare its own tag claims in `register()`, keeping
43///   ownership co-located with the decoder;
44/// * multiple codecs claim the same tag with priority ordering;
45/// * optional per-claim probes disambiguate the tag-collision cases
46///   that happen everywhere in the wild (DIV3 that's actually MPEG-4
47///   Part 2, XVID that's actually MS-MPEG4v3, audio wFormatTag=0x0055
48///   that could be MP3 or — very rarely — something else, etc.).
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
50pub enum CodecTag {
51    /// Four-character code used by AVI's `bmih.biCompression`, MP4 /
52    /// QuickTime sample-entry type, Matroska V_/A_ tags built around
53    /// FourCC, and many others. Always stored with alphabetic bytes
54    /// upper-cased so lookups are case-insensitive; non-alphabetic
55    /// bytes are preserved as-is.
56    Fourcc([u8; 4]),
57
58    /// AVI / WAV `WAVEFORMATEX::wFormatTag` (e.g. 0x0001 = PCM,
59    /// 0x0055 = MP3, 0x00FF = "raw" AAC, 0x1610 = AAC ADTS).
60    WaveFormat(u16),
61
62    /// MP4 ObjectTypeIndication (ISO/IEC 14496-1 Table 5 / the values
63    /// in an MP4 `esds` `DecoderConfigDescriptor`). e.g. 0x40 = MPEG-4
64    /// AAC, 0x20 = MPEG-4 Visual, 0x69 = MP3.
65    Mp4ObjectType(u8),
66
67    /// Matroska `CodecID` element (full string, e.g.
68    /// `"V_MPEG4/ISO/AVC"`, `"A_AAC"`, `"A_VORBIS"`).
69    Matroska(String),
70}
71
72impl CodecTag {
73    /// Build a FourCC tag, upper-casing alphabetic bytes.
74    pub fn fourcc(raw: &[u8; 4]) -> Self {
75        let mut out = [0u8; 4];
76        for i in 0..4 {
77            out[i] = raw[i].to_ascii_uppercase();
78        }
79        Self::Fourcc(out)
80    }
81
82    pub fn wave_format(tag: u16) -> Self {
83        Self::WaveFormat(tag)
84    }
85
86    pub fn mp4_object_type(oti: u8) -> Self {
87        Self::Mp4ObjectType(oti)
88    }
89
90    pub fn matroska(id: impl Into<String>) -> Self {
91        Self::Matroska(id.into())
92    }
93}
94
95impl std::fmt::Display for CodecTag {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Self::Fourcc(fcc) => {
99                // Print as bytes when ASCII-printable, else as hex.
100                if fcc.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
101                    write!(f, "fourcc({})", std::str::from_utf8(fcc).unwrap_or("????"))
102                } else {
103                    write!(
104                        f,
105                        "fourcc(0x{:02X}{:02X}{:02X}{:02X})",
106                        fcc[0], fcc[1], fcc[2], fcc[3]
107                    )
108                }
109            }
110            Self::WaveFormat(t) => write!(f, "wFormatTag(0x{t:04X})"),
111            Self::Mp4ObjectType(o) => write!(f, "mp4_oti(0x{o:02X})"),
112            Self::Matroska(s) => write!(f, "matroska({s})"),
113        }
114    }
115}
116
117/// Context passed to a codec's probe function during tag resolution.
118///
119/// Built by the demuxer from whatever it has already parsed (stream
120/// format block, a peek at the first packet, numeric hints like
121/// `bits_per_sample`). Probes read fields directly; the struct is
122/// `#[non_exhaustive]` so additional hints can be added later without
123/// breaking codec crates that match on it.
124///
125/// The canonical construction pattern, for a demuxer:
126///
127/// ```
128/// # use oxideav_core::{CodecTag, ProbeContext};
129/// let tag = CodecTag::wave_format(0x0001);
130/// let ctx = ProbeContext::new(&tag)
131///     .bits(24)
132///     .channels(2)
133///     .sample_rate(48_000);
134/// # let _ = ctx;
135/// ```
136///
137/// Codec authors read fields like `ctx.bits_per_sample` / `ctx.tag`
138/// directly — `#[non_exhaustive]` forbids struct-literal construction
139/// from outside this crate but does not restrict field access.
140#[non_exhaustive]
141#[derive(Clone, Debug)]
142pub struct ProbeContext<'a> {
143    /// The tag being resolved — always set.
144    pub tag: &'a CodecTag,
145    /// Raw container-level stream-format blob if available
146    /// (e.g. WAVEFORMATEX, BITMAPINFOHEADER, MP4 sample-entry bytes,
147    /// Matroska `CodecPrivate`). Format is container-specific.
148    pub header: Option<&'a [u8]>,
149    /// First packet bytes if the demuxer has already read one.
150    /// Most demuxers resolve tags at stream-discovery time before any
151    /// packet exists; this is `None` in that case.
152    pub packet: Option<&'a [u8]>,
153    /// Audio: bits per sample (from WAVEFORMATEX, MP4 sample entry,
154    /// Matroska `BitDepth`, etc.).
155    pub bits_per_sample: Option<u16>,
156    pub channels: Option<u16>,
157    pub sample_rate: Option<u32>,
158    pub width: Option<u32>,
159    pub height: Option<u32>,
160}
161
162impl<'a> ProbeContext<'a> {
163    /// Start building a context for `tag` with every hint field empty.
164    pub fn new(tag: &'a CodecTag) -> Self {
165        Self {
166            tag,
167            header: None,
168            packet: None,
169            bits_per_sample: None,
170            channels: None,
171            sample_rate: None,
172            width: None,
173            height: None,
174        }
175    }
176
177    pub fn header(mut self, h: &'a [u8]) -> Self {
178        self.header = Some(h);
179        self
180    }
181
182    pub fn packet(mut self, p: &'a [u8]) -> Self {
183        self.packet = Some(p);
184        self
185    }
186
187    pub fn bits(mut self, n: u16) -> Self {
188        self.bits_per_sample = Some(n);
189        self
190    }
191
192    pub fn channels(mut self, n: u16) -> Self {
193        self.channels = Some(n);
194        self
195    }
196
197    pub fn sample_rate(mut self, n: u32) -> Self {
198        self.sample_rate = Some(n);
199        self
200    }
201
202    pub fn width(mut self, n: u32) -> Self {
203        self.width = Some(n);
204        self
205    }
206
207    pub fn height(mut self, n: u32) -> Self {
208        self.height = Some(n);
209        self
210    }
211}
212
213/// Confidence value returned by a probe. `1.0` means "certainly me",
214/// `0.0` means "not me", values in between mean "partial evidence — if
215/// no higher-confidence claim exists, this should win". The registry
216/// picks the claim with the highest returned confidence and skips any
217/// that return `0.0`.
218pub type Confidence = f32;
219
220/// A probe function a codec attaches to its registration to
221/// disambiguate tag collisions. Called once per candidate
222/// registration during `resolve_tag`.
223pub type ProbeFn = fn(&ProbeContext) -> Confidence;
224
225/// Resolve a [`CodecTag`] (FourCC / WAVEFORMATEX / Matroska id / …) to a
226/// [`CodecId`]. The [`oxideav-codec`](https://crates.io/crates/oxideav-codec)
227/// registry implements this, but defining the trait here lets
228/// containers consume tag resolution via `&dyn CodecResolver` without
229/// pulling in the codec crate as a direct dependency.
230pub trait CodecResolver: Sync {
231    /// Resolve the tag in `ctx.tag` to a codec id. Implementations walk
232    /// every registration whose tag set contains the tag, call each
233    /// probe (treating `None` as "always 1.0"), and return the id with
234    /// the highest resulting confidence. Ties are broken by
235    /// registration order.
236    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId>;
237}
238
239/// Null resolver that resolves nothing — useful as a default when a
240/// caller doesn't have a real registry handy (e.g. unit tests, or
241/// legacy callers of the tag-free `open()` APIs).
242#[derive(Default, Clone, Copy)]
243pub struct NullCodecResolver;
244
245impl CodecResolver for NullCodecResolver {
246    fn resolve_tag(&self, _ctx: &ProbeContext) -> Option<CodecId> {
247        None
248    }
249}
250
251/// Codec-level parameters shared between demuxer/muxer and en/decoder.
252///
253/// **Marked `#[non_exhaustive]`** — construction via struct-literal
254/// syntax is not supported. Use the [`audio`](Self::audio) /
255/// [`video`](Self::video) constructors (or functional-update
256/// `CodecParameters { ..base }` syntax) so new fields can be added
257/// without another semver break.
258#[derive(Clone, Debug)]
259#[non_exhaustive]
260pub struct CodecParameters {
261    pub codec_id: CodecId,
262    pub media_type: MediaType,
263
264    // Audio-specific
265    pub sample_rate: Option<u32>,
266    pub channels: Option<u16>,
267    pub sample_format: Option<SampleFormat>,
268
269    // Video-specific
270    pub width: Option<u32>,
271    pub height: Option<u32>,
272    pub pixel_format: Option<PixelFormat>,
273    pub frame_rate: Option<Rational>,
274
275    /// Per-codec setup bytes (e.g., SPS/PPS, OpusHead). Format defined by codec.
276    pub extradata: Vec<u8>,
277
278    pub bit_rate: Option<u64>,
279
280    /// Codec-specific tuning knobs (e.g. `{"interlace": "true"}` for PNG's
281    /// Adam7 encode, `{"crf": "23"}` for h264). Empty by default. The shape
282    /// is declared by each codec's options struct — see
283    /// [`crate::options`]. Parsed once at encoder/decoder construction;
284    /// the hot path never touches this.
285    pub options: CodecOptions,
286}
287
288impl CodecParameters {
289    pub fn audio(codec_id: CodecId) -> Self {
290        Self {
291            codec_id,
292            media_type: MediaType::Audio,
293            sample_rate: None,
294            channels: None,
295            sample_format: None,
296            width: None,
297            height: None,
298            pixel_format: None,
299            frame_rate: None,
300            extradata: Vec::new(),
301            bit_rate: None,
302            options: CodecOptions::default(),
303        }
304    }
305
306    /// True when `self` and `other` have the same codec_id and core
307    /// format parameters (sample_rate/channels/sample_format for audio,
308    /// width/height/pixel_format for video). Extradata and bitrate
309    /// differences are tolerated — many containers rewrite extradata
310    /// losslessly during a copy operation.
311    pub fn matches_core(&self, other: &CodecParameters) -> bool {
312        self.codec_id == other.codec_id
313            && self.sample_rate == other.sample_rate
314            && self.channels == other.channels
315            && self.sample_format == other.sample_format
316            && self.width == other.width
317            && self.height == other.height
318            && self.pixel_format == other.pixel_format
319    }
320
321    pub fn video(codec_id: CodecId) -> Self {
322        Self {
323            codec_id,
324            media_type: MediaType::Video,
325            sample_rate: None,
326            channels: None,
327            sample_format: None,
328            width: None,
329            height: None,
330            pixel_format: None,
331            frame_rate: None,
332            extradata: Vec::new(),
333            bit_rate: None,
334            options: CodecOptions::default(),
335        }
336    }
337
338    /// Construct subtitle codec parameters. No format-specific fields
339    /// are populated — subtitle codecs typically only carry an opaque
340    /// `extradata` blob (the format's header / style block) and the
341    /// codec id.
342    pub fn subtitle(codec_id: CodecId) -> Self {
343        Self {
344            codec_id,
345            media_type: MediaType::Subtitle,
346            sample_rate: None,
347            channels: None,
348            sample_format: None,
349            width: None,
350            height: None,
351            pixel_format: None,
352            frame_rate: None,
353            extradata: Vec::new(),
354            bit_rate: None,
355            options: CodecOptions::default(),
356        }
357    }
358
359    /// Construct generic data-stream codec parameters (timed metadata,
360    /// chapters, etc.). Like [`Self::subtitle`], no format-specific
361    /// fields are populated.
362    pub fn data(codec_id: CodecId) -> Self {
363        Self {
364            codec_id,
365            media_type: MediaType::Data,
366            sample_rate: None,
367            channels: None,
368            sample_format: None,
369            width: None,
370            height: None,
371            pixel_format: None,
372            frame_rate: None,
373            extradata: Vec::new(),
374            bit_rate: None,
375            options: CodecOptions::default(),
376        }
377    }
378}
379
380/// Description of a single stream inside a container.
381#[derive(Clone, Debug)]
382pub struct StreamInfo {
383    pub index: u32,
384    pub time_base: TimeBase,
385    pub duration: Option<i64>,
386    pub start_time: Option<i64>,
387    pub params: CodecParameters,
388}
389
390#[cfg(test)]
391mod codec_tag_tests {
392    use super::*;
393
394    #[test]
395    fn fourcc_uppercases_on_construction() {
396        let t = CodecTag::fourcc(b"div3");
397        assert_eq!(t, CodecTag::Fourcc(*b"DIV3"));
398        // Non-alphabetic bytes preserved unchanged.
399        let t2 = CodecTag::fourcc(b"MP42");
400        assert_eq!(t2, CodecTag::Fourcc(*b"MP42"));
401        let t3 = CodecTag::fourcc(&[0xFF, b'a', 0x00, b'1']);
402        assert_eq!(t3, CodecTag::Fourcc([0xFF, b'A', 0x00, b'1']));
403    }
404
405    #[test]
406    fn fourcc_equality_case_insensitive_via_ctor() {
407        assert_eq!(CodecTag::fourcc(b"xvid"), CodecTag::fourcc(b"XVID"));
408        assert_eq!(CodecTag::fourcc(b"DiV3"), CodecTag::fourcc(b"div3"));
409    }
410
411    #[test]
412    fn display_printable_fourcc() {
413        assert_eq!(CodecTag::fourcc(b"XVID").to_string(), "fourcc(XVID)");
414    }
415
416    #[test]
417    fn display_non_printable_fourcc_as_hex() {
418        let t = CodecTag::Fourcc([0x00, 0x00, 0x00, 0x01]);
419        assert_eq!(t.to_string(), "fourcc(0x00000001)");
420    }
421
422    #[test]
423    fn display_wave_format() {
424        assert_eq!(
425            CodecTag::wave_format(0x0055).to_string(),
426            "wFormatTag(0x0055)"
427        );
428    }
429
430    #[test]
431    fn display_mp4_oti() {
432        assert_eq!(CodecTag::mp4_object_type(0x40).to_string(), "mp4_oti(0x40)");
433    }
434
435    #[test]
436    fn display_matroska() {
437        assert_eq!(
438            CodecTag::matroska("V_MPEG4/ISO/AVC").to_string(),
439            "matroska(V_MPEG4/ISO/AVC)",
440        );
441    }
442
443    #[test]
444    fn null_resolver_resolves_nothing() {
445        let r = NullCodecResolver;
446        let xvid = CodecTag::fourcc(b"XVID");
447        assert!(r.resolve_tag(&ProbeContext::new(&xvid)).is_none());
448        let wf = CodecTag::wave_format(0x0055);
449        assert!(r.resolve_tag(&ProbeContext::new(&wf)).is_none());
450    }
451
452    #[test]
453    fn probe_context_builder_fills_hints() {
454        let tag = CodecTag::wave_format(0x0001);
455        let ctx = ProbeContext::new(&tag)
456            .bits(24)
457            .channels(2)
458            .sample_rate(48_000)
459            .header(&[1, 2, 3])
460            .packet(&[4, 5]);
461        assert_eq!(ctx.bits_per_sample, Some(24));
462        assert_eq!(ctx.channels, Some(2));
463        assert_eq!(ctx.sample_rate, Some(48_000));
464        assert_eq!(ctx.header.unwrap(), &[1, 2, 3]);
465        assert_eq!(ctx.packet.unwrap(), &[4, 5]);
466    }
467}