Skip to main content

oxideav_core/
stream.rs

1//! Stream metadata shared between containers and codecs.
2
3use crate::format::{ChannelLayout, MediaType, PixelFormat, SampleFormat};
4use crate::limits::DecoderLimits;
5use crate::options::CodecOptions;
6use crate::rational::Rational;
7use crate::time::TimeBase;
8
9/// A stable identifier for a codec. Codec crates register a `CodecId` so the
10/// codec registry can look them up by name.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct CodecId(pub String);
13
14impl CodecId {
15    pub fn new(s: impl Into<String>) -> Self {
16        Self(s.into())
17    }
18
19    pub fn as_str(&self) -> &str {
20        &self.0
21    }
22}
23
24impl From<&str> for CodecId {
25    fn from(s: &str) -> Self {
26        Self(s.to_owned())
27    }
28}
29
30impl std::fmt::Display for CodecId {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(f, "{}", self.0)
33    }
34}
35
36/// A codec identifier scoped to a container format — the thing a
37/// demuxer reads out of the file to name a codec. Resolved to a
38/// [`CodecId`] by the codec registry.
39///
40/// Centralising these in the registry (instead of each container
41/// hand-rolling its own FourCC → CodecId table) lets:
42///
43/// * a codec crate declare its own tag claims in `register()`, keeping
44///   ownership co-located with the decoder;
45/// * multiple codecs claim the same tag with priority ordering;
46/// * optional per-claim probes disambiguate the tag-collision cases
47///   that happen everywhere in the wild (DIV3 that's actually MPEG-4
48///   Part 2, XVID that's actually MS-MPEG4v3, audio wFormatTag=0x0055
49///   that could be MP3 or — very rarely — something else, etc.).
50#[derive(Clone, Debug, PartialEq, Eq, Hash)]
51pub enum CodecTag {
52    /// Four-character code used by AVI's `bmih.biCompression`, MP4 /
53    /// QuickTime sample-entry type, Matroska V_/A_ tags built around
54    /// FourCC, and many others. Always stored with alphabetic bytes
55    /// upper-cased so lookups are case-insensitive; non-alphabetic
56    /// bytes are preserved as-is.
57    Fourcc([u8; 4]),
58
59    /// AVI / WAV `WAVEFORMATEX::wFormatTag` (e.g. 0x0001 = PCM,
60    /// 0x0055 = MP3, 0x00FF = "raw" AAC, 0x1610 = AAC ADTS).
61    WaveFormat(u16),
62
63    /// MP4 ObjectTypeIndication (ISO/IEC 14496-1 Table 5 / the values
64    /// in an MP4 `esds` `DecoderConfigDescriptor`). e.g. 0x40 = MPEG-4
65    /// AAC, 0x20 = MPEG-4 Visual, 0x69 = MP3.
66    Mp4ObjectType(u8),
67
68    /// Matroska `CodecID` element (full string, e.g.
69    /// `"V_MPEG4/ISO/AVC"`, `"A_AAC"`, `"A_VORBIS"`).
70    Matroska(String),
71}
72
73impl CodecTag {
74    /// Build a FourCC tag, upper-casing alphabetic bytes.
75    pub fn fourcc(raw: &[u8; 4]) -> Self {
76        let mut out = [0u8; 4];
77        for i in 0..4 {
78            out[i] = raw[i].to_ascii_uppercase();
79        }
80        Self::Fourcc(out)
81    }
82
83    pub fn wave_format(tag: u16) -> Self {
84        Self::WaveFormat(tag)
85    }
86
87    pub fn mp4_object_type(oti: u8) -> Self {
88        Self::Mp4ObjectType(oti)
89    }
90
91    pub fn matroska(id: impl Into<String>) -> Self {
92        Self::Matroska(id.into())
93    }
94}
95
96impl std::fmt::Display for CodecTag {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            Self::Fourcc(fcc) => {
100                // Print as bytes when ASCII-printable, else as hex.
101                if fcc.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
102                    write!(f, "fourcc({})", std::str::from_utf8(fcc).unwrap_or("????"))
103                } else {
104                    write!(
105                        f,
106                        "fourcc(0x{:02X}{:02X}{:02X}{:02X})",
107                        fcc[0], fcc[1], fcc[2], fcc[3]
108                    )
109                }
110            }
111            Self::WaveFormat(t) => write!(f, "wFormatTag(0x{t:04X})"),
112            Self::Mp4ObjectType(o) => write!(f, "mp4_oti(0x{o:02X})"),
113            Self::Matroska(s) => write!(f, "matroska({s})"),
114        }
115    }
116}
117
118/// Context passed to a codec's probe function during tag resolution.
119///
120/// Built by the demuxer from whatever it has already parsed (stream
121/// format block, a peek at the first packet, numeric hints like
122/// `bits_per_sample`). Probes read fields directly; the struct is
123/// `#[non_exhaustive]` so additional hints can be added later without
124/// breaking codec crates that match on it.
125///
126/// The canonical construction pattern, for a demuxer:
127///
128/// ```
129/// # use oxideav_core::{CodecTag, ProbeContext};
130/// let tag = CodecTag::wave_format(0x0001);
131/// let ctx = ProbeContext::new(&tag)
132///     .bits(24)
133///     .channels(2)
134///     .sample_rate(48_000);
135/// # let _ = ctx;
136/// ```
137///
138/// Codec authors read fields like `ctx.bits_per_sample` / `ctx.tag`
139/// directly — `#[non_exhaustive]` forbids struct-literal construction
140/// from outside this crate but does not restrict field access.
141#[non_exhaustive]
142#[derive(Clone, Debug)]
143pub struct ProbeContext<'a> {
144    /// The tag being resolved — always set.
145    pub tag: &'a CodecTag,
146    /// Raw container-level stream-format blob if available
147    /// (e.g. WAVEFORMATEX, BITMAPINFOHEADER, MP4 sample-entry bytes,
148    /// Matroska `CodecPrivate`). Format is container-specific.
149    pub header: Option<&'a [u8]>,
150    /// First packet bytes if the demuxer has already read one.
151    /// Most demuxers resolve tags at stream-discovery time before any
152    /// packet exists; this is `None` in that case.
153    pub packet: Option<&'a [u8]>,
154    /// Audio: bits per sample (from WAVEFORMATEX, MP4 sample entry,
155    /// Matroska `BitDepth`, etc.).
156    pub bits_per_sample: Option<u16>,
157    pub channels: Option<u16>,
158    pub sample_rate: Option<u32>,
159    pub width: Option<u32>,
160    pub height: Option<u32>,
161}
162
163impl<'a> ProbeContext<'a> {
164    /// Start building a context for `tag` with every hint field empty.
165    pub fn new(tag: &'a CodecTag) -> Self {
166        Self {
167            tag,
168            header: None,
169            packet: None,
170            bits_per_sample: None,
171            channels: None,
172            sample_rate: None,
173            width: None,
174            height: None,
175        }
176    }
177
178    pub fn header(mut self, h: &'a [u8]) -> Self {
179        self.header = Some(h);
180        self
181    }
182
183    pub fn packet(mut self, p: &'a [u8]) -> Self {
184        self.packet = Some(p);
185        self
186    }
187
188    pub fn bits(mut self, n: u16) -> Self {
189        self.bits_per_sample = Some(n);
190        self
191    }
192
193    pub fn channels(mut self, n: u16) -> Self {
194        self.channels = Some(n);
195        self
196    }
197
198    pub fn sample_rate(mut self, n: u32) -> Self {
199        self.sample_rate = Some(n);
200        self
201    }
202
203    pub fn width(mut self, n: u32) -> Self {
204        self.width = Some(n);
205        self
206    }
207
208    pub fn height(mut self, n: u32) -> Self {
209        self.height = Some(n);
210        self
211    }
212}
213
214/// Confidence value returned by a probe. `1.0` means "certainly me",
215/// `0.0` means "not me", values in between mean "partial evidence — if
216/// no higher-confidence claim exists, this should win". The registry
217/// picks the claim with the highest returned confidence and skips any
218/// that return `0.0`.
219pub type Confidence = f32;
220
221/// A probe function a codec attaches to its registration to
222/// disambiguate tag collisions. Called once per candidate
223/// registration during `resolve_tag`.
224pub type ProbeFn = fn(&ProbeContext) -> Confidence;
225
226/// Resolve a [`CodecTag`] (FourCC / WAVEFORMATEX / Matroska id / …) to a
227/// [`CodecId`]. The [`oxideav-codec`](https://crates.io/crates/oxideav-codec)
228/// registry implements this, but defining the trait here lets
229/// containers consume tag resolution via `&dyn CodecResolver` without
230/// pulling in the codec crate as a direct dependency.
231///
232/// **Inverse direction** (codec_id → wire tag) is intentionally NOT a
233/// method on this trait. Wire tags are per-stream state: different
234/// `mpeg4video` streams correctly identify as `DIVX` / `XVID` /
235/// `MP4V` / `FMP4`, different `h264` streams as `H264` vs `AVC1`,
236/// and so on. The stream's [`CodecParameters::tag`] field is the
237/// canonical home for that data — set by the demuxer when reading
238/// existing media and by the encoder via its `output_params()` at
239/// configure-time. A registry-level "give me the canonical tag for
240/// this codec_id" lookup walks registration order and returns
241/// whichever tag was declared first, which is arbitrary and breaks
242/// round-trip preservation.
243pub trait CodecResolver: Sync {
244    /// Resolve the tag in `ctx.tag` to a codec id. Implementations walk
245    /// every registration whose tag set contains the tag, call each
246    /// probe (treating `None` as "always 1.0"), and return the id with
247    /// the highest resulting confidence. Ties are broken by
248    /// registration order.
249    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId>;
250}
251
252/// Null resolver that resolves nothing — useful as a default when a
253/// caller doesn't have a real registry handy (e.g. unit tests, or
254/// legacy callers of the tag-free `open()` APIs).
255#[derive(Default, Clone, Copy)]
256pub struct NullCodecResolver;
257
258impl CodecResolver for NullCodecResolver {
259    fn resolve_tag(&self, _ctx: &ProbeContext) -> Option<CodecId> {
260        None
261    }
262}
263
264/// Codec-level parameters shared between demuxer/muxer and en/decoder.
265///
266/// **Marked `#[non_exhaustive]`** — construction via struct-literal
267/// syntax is not supported. Use the [`audio`](Self::audio) /
268/// [`video`](Self::video) constructors (or functional-update
269/// `CodecParameters { ..base }` syntax) so new fields can be added
270/// without another semver break.
271#[derive(Clone, Debug)]
272#[non_exhaustive]
273pub struct CodecParameters {
274    pub codec_id: CodecId,
275    pub media_type: MediaType,
276
277    // Audio-specific
278    pub sample_rate: Option<u32>,
279    pub channels: Option<u16>,
280    pub sample_format: Option<SampleFormat>,
281    /// Speaker layout for the audio stream. **This is the canonical
282    /// answer to "what layout does this stream have?"** — layout is a
283    /// stream-level property and is intentionally *not* duplicated on
284    /// individual [`AudioFrame`](crate::AudioFrame)s.
285    ///
286    /// Optional and additive alongside [`channels`](Self::channels): a
287    /// codec/container that only knows the count can leave this `None`
288    /// and consumers will fall back to [`ChannelLayout::from_count`]
289    /// via [`Self::resolved_layout`]. When both are set, they must
290    /// agree on channel count.
291    pub channel_layout: Option<ChannelLayout>,
292
293    // Video-specific
294    pub width: Option<u32>,
295    pub height: Option<u32>,
296    pub pixel_format: Option<PixelFormat>,
297    pub frame_rate: Option<Rational>,
298
299    /// Per-codec setup bytes (e.g., SPS/PPS, OpusHead). Format defined by codec.
300    pub extradata: Vec<u8>,
301
302    pub bit_rate: Option<u64>,
303
304    /// Codec-specific tuning knobs (e.g. `{"interlace": "true"}` for PNG's
305    /// Adam7 encode, `{"crf": "23"}` for h264). Empty by default. The shape
306    /// is declared by each codec's options struct — see
307    /// [`crate::options`]. Parsed once at encoder/decoder construction;
308    /// the hot path never touches this.
309    pub options: CodecOptions,
310
311    /// DoS-protection caps threaded into every decoder constructed from
312    /// these parameters. See [`DecoderLimits`] for the semantics of each
313    /// field. Defaults are conservative-but-finite (32 k × 32 k pixels,
314    /// 1 GiB per arena, etc.) — every existing real-world stream
315    /// decodes unchanged. Tighten via [`Self::with_limits`] when the
316    /// caller wants to harden the pipeline against untrusted input.
317    pub limits: DecoderLimits,
318
319    /// Optional 0-based device selector for hardware-accelerated codecs.
320    /// `None` (the default) means "use the backend's default device";
321    /// `Some(n)` requests device `n` from the backend's
322    /// [`crate::engine::HwDeviceInfo`] enumeration order.
323    ///
324    /// Software codecs ignore this field. Hardware codecs read it as
325    /// `params.device_index.unwrap_or(0)` to pick which physical engine
326    /// to bind to. Indexing matches the order of devices reported by the
327    /// codec entry's `engine_probe` function.
328    pub device_index: Option<u32>,
329
330    /// On-wire tag for this stream — the FourCC / WAVEFORMATEX
331    /// `wFormatTag` / MP4 ObjectTypeIndication / Matroska `CodecID`
332    /// string carried by the container. Set by the **producer**:
333    ///
334    /// * **Demuxers** populate this from the stream's container
335    ///   header at read-time so muxers re-emitting the same stream
336    ///   round-trip the original tag byte-for-byte (`mpeg4video`
337    ///   demuxed as `DIVX` re-muxes as `DIVX`, not as the codec
338    ///   crate's first-declared `XVID`).
339    /// * **Encoders** populate this in [`crate::Encoder::output_params`]
340    ///   to tell muxers which wire tag to write — needed for
341    ///   multi-FourCC codecs whose configuration (pixel format / bit
342    ///   depth / alpha / chroma sampling) selects one of several
343    ///   valid FourCCs (e.g. MagicYUV's 17 native v7 codes).
344    ///
345    /// `None` is the default — sensible for in-memory streams that
346    /// haven't been bound to a container yet. Muxers that need a
347    /// wire tag and find `None` here will fall back to whatever
348    /// container-specific synthesis they support (e.g. AVI's PCM
349    /// `wFormatTag` synthesis from `sample_format`, or the
350    /// `extradata[0..4]` printable-FourCC hint for legacy callers)
351    /// and otherwise return `Error::Unsupported`.
352    pub tag: Option<CodecTag>,
353
354    /// BCP-47 / ISO 639 language tag (`"en"`, `"jpn"`, …) when the
355    /// container labels the stream's language. `None` means
356    /// "unspecified" — not "neutral".
357    ///
358    /// Demuxers populate this from the container's per-track language
359    /// element (MKV `Language` / `LanguageBCP47`, MP4 `mdhd` ISO 639-2
360    /// code, Ogg `LANGUAGE=` comment, …). Muxers re-emit it on the
361    /// matching container element so a round-trip preserves the
362    /// caller-visible tag byte-for-byte. No validation is performed
363    /// here — the value is whatever string the producer supplied.
364    pub language: Option<String>,
365}
366
367impl CodecParameters {
368    pub fn audio(codec_id: CodecId) -> Self {
369        Self {
370            codec_id,
371            media_type: MediaType::Audio,
372            sample_rate: None,
373            channels: None,
374            sample_format: None,
375            channel_layout: None,
376            width: None,
377            height: None,
378            pixel_format: None,
379            frame_rate: None,
380            extradata: Vec::new(),
381            bit_rate: None,
382            options: CodecOptions::default(),
383            limits: DecoderLimits::default(),
384            device_index: None,
385            tag: None,
386            language: None,
387        }
388    }
389
390    /// True when `self` and `other` have the same codec_id and core
391    /// format parameters (sample_rate/channels/sample_format for audio,
392    /// width/height/pixel_format for video). Extradata and bitrate
393    /// differences are tolerated — many containers rewrite extradata
394    /// losslessly during a copy operation. `channel_layout` is compared
395    /// only via the channel count (through [`Self::resolved_layout`]) so
396    /// a stream that surfaces an explicit layout still matches a
397    /// count-only stream of the same width.
398    pub fn matches_core(&self, other: &CodecParameters) -> bool {
399        self.codec_id == other.codec_id
400            && self.sample_rate == other.sample_rate
401            && self.channels == other.channels
402            && self.sample_format == other.sample_format
403            && self.width == other.width
404            && self.height == other.height
405            && self.pixel_format == other.pixel_format
406    }
407
408    pub fn video(codec_id: CodecId) -> Self {
409        Self {
410            codec_id,
411            media_type: MediaType::Video,
412            sample_rate: None,
413            channels: None,
414            sample_format: None,
415            channel_layout: None,
416            width: None,
417            height: None,
418            pixel_format: None,
419            frame_rate: None,
420            extradata: Vec::new(),
421            bit_rate: None,
422            options: CodecOptions::default(),
423            limits: DecoderLimits::default(),
424            device_index: None,
425            tag: None,
426            language: None,
427        }
428    }
429
430    /// Construct subtitle codec parameters. No format-specific fields
431    /// are populated — subtitle codecs typically only carry an opaque
432    /// `extradata` blob (the format's header / style block) and the
433    /// codec id.
434    pub fn subtitle(codec_id: CodecId) -> Self {
435        Self {
436            codec_id,
437            media_type: MediaType::Subtitle,
438            sample_rate: None,
439            channels: None,
440            sample_format: None,
441            channel_layout: None,
442            width: None,
443            height: None,
444            pixel_format: None,
445            frame_rate: None,
446            extradata: Vec::new(),
447            bit_rate: None,
448            options: CodecOptions::default(),
449            limits: DecoderLimits::default(),
450            device_index: None,
451            tag: None,
452            language: None,
453        }
454    }
455
456    /// Construct generic data-stream codec parameters (timed metadata,
457    /// chapters, etc.). Like [`Self::subtitle`], no format-specific
458    /// fields are populated.
459    pub fn data(codec_id: CodecId) -> Self {
460        Self {
461            codec_id,
462            media_type: MediaType::Data,
463            sample_rate: None,
464            channels: None,
465            sample_format: None,
466            channel_layout: None,
467            width: None,
468            height: None,
469            pixel_format: None,
470            frame_rate: None,
471            extradata: Vec::new(),
472            bit_rate: None,
473            options: CodecOptions::default(),
474            limits: DecoderLimits::default(),
475            device_index: None,
476            tag: None,
477            language: None,
478        }
479    }
480
481    /// Builder method: set the channel count.
482    ///
483    /// Pairs with [`Self::channel_layout`] for the layout. The two are
484    /// kept as independent fields so a codec that only knows one or the
485    /// other can populate just the field it has; [`Self::resolved_layout`]
486    /// derives a layout from whatever is set.
487    pub fn channels(mut self, n: u16) -> Self {
488        self.channels = Some(n);
489        self
490    }
491
492    /// Builder method: set the channel layout. Mirrors
493    /// [`Self::channels`]; setting one does not auto-fill the other —
494    /// use [`Self::resolved_layout`] / [`Self::resolved_channels`] at
495    /// read time to bridge the two.
496    pub fn channel_layout(mut self, layout: ChannelLayout) -> Self {
497        self.channel_layout = Some(layout);
498        self
499    }
500
501    /// Best-effort layout: prefers an explicit [`Self::channel_layout`]
502    /// when set, otherwise infers one from [`Self::channels`] via
503    /// [`ChannelLayout::from_count`]. Returns `None` only when neither
504    /// field is populated (e.g. video / data streams, or audio params
505    /// surfaced before the codec has been opened).
506    ///
507    /// This is the canonical call-site for resolving a stream's
508    /// channel layout — frames do *not* carry layout, so audio
509    /// consumers (downmix, device routing, channel-aware filters)
510    /// should read it from the stream's `CodecParameters` once and
511    /// pass it down with the frame.
512    pub fn resolved_layout(&self) -> Option<ChannelLayout> {
513        self.channel_layout
514            .or_else(|| self.channels.map(ChannelLayout::from_count))
515    }
516
517    /// Best-effort channel count: prefers an explicit
518    /// [`Self::channels`] when set, otherwise reads the count off
519    /// [`Self::channel_layout`]. Returns `None` only when neither
520    /// field is populated.
521    pub fn resolved_channels(&self) -> Option<u16> {
522        self.channels
523            .or_else(|| self.channel_layout.map(|l| l.channel_count()))
524    }
525
526    /// Read-only access to the DoS-protection caps for any decoder
527    /// constructed from these parameters. See [`DecoderLimits`].
528    pub fn limits(&self) -> &DecoderLimits {
529        &self.limits
530    }
531
532    /// Builder method: replace the [`DecoderLimits`] for these
533    /// parameters. Use to tighten caps before passing parameters into
534    /// `make_decoder` (e.g. when processing untrusted uploads on a
535    /// shared server).
536    ///
537    /// ```
538    /// # use oxideav_core::{CodecId, CodecParameters, DecoderLimits};
539    /// let limits = DecoderLimits::default()
540    ///     .with_max_pixels_per_frame(4096 * 4096)
541    ///     .with_max_arenas_in_flight(2);
542    /// let p = CodecParameters::video(CodecId::new("h263")).with_limits(limits);
543    /// assert_eq!(p.limits().max_pixels_per_frame, 4096 * 4096);
544    /// ```
545    pub fn with_limits(mut self, limits: DecoderLimits) -> Self {
546        self.limits = limits;
547        self
548    }
549
550    /// Bind subsequent decoder/encoder construction to a specific device.
551    /// `index` matches the position in the `engine_probe` device list.
552    ///
553    /// Software codecs ignore this field. Hardware codecs read it as
554    /// `params.device_index.unwrap_or(0)` to pick which physical engine
555    /// to bind to.
556    pub fn with_device_index(mut self, index: u32) -> Self {
557        self.device_index = Some(index);
558        self
559    }
560
561    /// Builder method: set the on-wire [`tag`](Self::tag).
562    ///
563    /// Demuxers call this from their stream-format parser so muxers
564    /// re-emitting the stream preserve the original FourCC / wFormatTag
565    /// byte-for-byte. Encoders call this in `output_params()` to
566    /// announce which wire tag they're producing.
567    ///
568    /// ```
569    /// # use oxideav_core::{CodecId, CodecParameters, CodecTag};
570    /// let p = CodecParameters::video(CodecId::new("magicyuv"))
571    ///     .with_tag(CodecTag::fourcc(b"M8RG"));
572    /// assert_eq!(p.tag, Some(CodecTag::fourcc(b"M8RG")));
573    /// ```
574    pub fn with_tag(mut self, tag: CodecTag) -> Self {
575        self.tag = Some(tag);
576        self
577    }
578
579    /// Builder method: set the per-stream [`language`](Self::language)
580    /// tag. Accepts any string — BCP-47 short codes (`"en"`), ISO
581    /// 639-2/T three-letter codes (`"jpn"`), or container-native
582    /// values are all passed through verbatim. No validation is
583    /// performed; the muxer writes whatever the caller hands in.
584    ///
585    /// ```
586    /// # use oxideav_core::{CodecId, CodecParameters};
587    /// let p = CodecParameters::audio(CodecId::new("aac")).with_language("jpn");
588    /// assert_eq!(p.language.as_deref(), Some("jpn"));
589    /// ```
590    pub fn with_language(mut self, language: impl Into<String>) -> Self {
591        self.language = Some(language.into());
592        self
593    }
594}
595
596/// Description of a single stream inside a container.
597#[derive(Clone, Debug)]
598pub struct StreamInfo {
599    pub index: u32,
600    pub time_base: TimeBase,
601    pub duration: Option<i64>,
602    pub start_time: Option<i64>,
603    pub params: CodecParameters,
604}
605
606#[cfg(test)]
607mod codec_tag_tests {
608    use super::*;
609
610    #[test]
611    fn fourcc_uppercases_on_construction() {
612        let t = CodecTag::fourcc(b"div3");
613        assert_eq!(t, CodecTag::Fourcc(*b"DIV3"));
614        // Non-alphabetic bytes preserved unchanged.
615        let t2 = CodecTag::fourcc(b"MP42");
616        assert_eq!(t2, CodecTag::Fourcc(*b"MP42"));
617        let t3 = CodecTag::fourcc(&[0xFF, b'a', 0x00, b'1']);
618        assert_eq!(t3, CodecTag::Fourcc([0xFF, b'A', 0x00, b'1']));
619    }
620
621    #[test]
622    fn fourcc_equality_case_insensitive_via_ctor() {
623        assert_eq!(CodecTag::fourcc(b"xvid"), CodecTag::fourcc(b"XVID"));
624        assert_eq!(CodecTag::fourcc(b"DiV3"), CodecTag::fourcc(b"div3"));
625    }
626
627    #[test]
628    fn display_printable_fourcc() {
629        assert_eq!(CodecTag::fourcc(b"XVID").to_string(), "fourcc(XVID)");
630    }
631
632    #[test]
633    fn display_non_printable_fourcc_as_hex() {
634        let t = CodecTag::Fourcc([0x00, 0x00, 0x00, 0x01]);
635        assert_eq!(t.to_string(), "fourcc(0x00000001)");
636    }
637
638    #[test]
639    fn display_wave_format() {
640        assert_eq!(
641            CodecTag::wave_format(0x0055).to_string(),
642            "wFormatTag(0x0055)"
643        );
644    }
645
646    #[test]
647    fn display_mp4_oti() {
648        assert_eq!(CodecTag::mp4_object_type(0x40).to_string(), "mp4_oti(0x40)");
649    }
650
651    #[test]
652    fn display_matroska() {
653        assert_eq!(
654            CodecTag::matroska("V_MPEG4/ISO/AVC").to_string(),
655            "matroska(V_MPEG4/ISO/AVC)",
656        );
657    }
658
659    #[test]
660    fn null_resolver_resolves_nothing() {
661        let r = NullCodecResolver;
662        let xvid = CodecTag::fourcc(b"XVID");
663        assert!(r.resolve_tag(&ProbeContext::new(&xvid)).is_none());
664        let wf = CodecTag::wave_format(0x0055);
665        assert!(r.resolve_tag(&ProbeContext::new(&wf)).is_none());
666    }
667
668    #[test]
669    fn probe_context_builder_fills_hints() {
670        let tag = CodecTag::wave_format(0x0001);
671        let ctx = ProbeContext::new(&tag)
672            .bits(24)
673            .channels(2)
674            .sample_rate(48_000)
675            .header(&[1, 2, 3])
676            .packet(&[4, 5]);
677        assert_eq!(ctx.bits_per_sample, Some(24));
678        assert_eq!(ctx.channels, Some(2));
679        assert_eq!(ctx.sample_rate, Some(48_000));
680        assert_eq!(ctx.header.unwrap(), &[1, 2, 3]);
681        assert_eq!(ctx.packet.unwrap(), &[4, 5]);
682    }
683}
684
685#[cfg(test)]
686mod channel_layout_plumbing_tests {
687    use super::*;
688
689    #[test]
690    fn audio_params_default_to_no_layout() {
691        let p = CodecParameters::audio(CodecId::new("pcm_s16le"));
692        assert!(p.channel_layout.is_none());
693        assert!(p.channels.is_none());
694        assert!(p.resolved_layout().is_none());
695        assert!(p.resolved_channels().is_none());
696    }
697
698    #[test]
699    fn channels_only_infers_layout_via_from_count() {
700        let p = CodecParameters::audio(CodecId::new("pcm_s16le")).channels(6);
701        assert_eq!(p.channels, Some(6));
702        assert!(p.channel_layout.is_none());
703        assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround51));
704        assert_eq!(p.resolved_channels(), Some(6));
705    }
706
707    #[test]
708    fn explicit_layout_wins_over_count() {
709        let p = CodecParameters::audio(CodecId::new("ac3"))
710            .channels(6)
711            .channel_layout(ChannelLayout::Surround60);
712        // 6ch by-count would default to Surround51, but the explicit
713        // layout overrides.
714        assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround60));
715        assert_eq!(p.resolved_channels(), Some(6));
716    }
717
718    #[test]
719    fn layout_only_yields_count_via_resolved_channels() {
720        let p =
721            CodecParameters::audio(CodecId::new("ac3")).channel_layout(ChannelLayout::Surround71);
722        assert!(p.channels.is_none());
723        assert_eq!(p.resolved_channels(), Some(8));
724        assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround71));
725    }
726}
727
728#[cfg(test)]
729mod codec_parameters_device_index_tests {
730    use super::*;
731
732    #[test]
733    fn codec_parameters_device_index_defaults_to_none() {
734        assert!(CodecParameters::audio(CodecId::new("pcm_s16le"))
735            .device_index
736            .is_none());
737        assert!(CodecParameters::video(CodecId::new("h264"))
738            .device_index
739            .is_none());
740        assert!(CodecParameters::subtitle(CodecId::new("srt"))
741            .device_index
742            .is_none());
743        assert!(CodecParameters::data(CodecId::new("bin"))
744            .device_index
745            .is_none());
746    }
747
748    #[test]
749    fn codec_parameters_with_device_index_sets_field() {
750        let p = CodecParameters::video(CodecId::new("h264")).with_device_index(2);
751        assert_eq!(p.device_index, Some(2));
752    }
753}
754
755#[cfg(test)]
756mod codec_parameters_tag_tests {
757    use super::*;
758
759    #[test]
760    fn tag_defaults_to_none_on_every_constructor() {
761        assert!(CodecParameters::audio(CodecId::new("aac")).tag.is_none());
762        assert!(CodecParameters::video(CodecId::new("h264")).tag.is_none());
763        assert!(CodecParameters::subtitle(CodecId::new("srt")).tag.is_none());
764        assert!(CodecParameters::data(CodecId::new("bin")).tag.is_none());
765    }
766
767    #[test]
768    fn with_tag_builder_sets_field() {
769        let p =
770            CodecParameters::video(CodecId::new("magicyuv")).with_tag(CodecTag::fourcc(b"M8RG"));
771        assert_eq!(p.tag, Some(CodecTag::fourcc(b"M8RG")));
772    }
773
774    #[test]
775    fn with_tag_round_trip_preserves_demuxed_fourcc() {
776        // The canonical use-case: a demuxer sees DIVX in the bitstream
777        // and tags the params accordingly. The mpeg4video codec also
778        // claims XVID / MP4V / FMP4, but the muxer must re-emit DIVX.
779        let demuxed =
780            CodecParameters::video(CodecId::new("mpeg4video")).with_tag(CodecTag::fourcc(b"DIVX"));
781        // Muxer reads `params.tag` directly — no registry round-trip.
782        assert_eq!(demuxed.tag, Some(CodecTag::fourcc(b"DIVX")));
783    }
784
785    #[test]
786    fn wave_format_tag_preserved() {
787        let p = CodecParameters::audio(CodecId::new("mp3")).with_tag(CodecTag::wave_format(0x0055));
788        assert_eq!(p.tag, Some(CodecTag::WaveFormat(0x0055)));
789    }
790}
791
792#[cfg(test)]
793mod codec_parameters_language_tests {
794    use super::*;
795
796    #[test]
797    fn language_defaults_to_none_on_every_constructor() {
798        assert!(CodecParameters::audio(CodecId::new("aac"))
799            .language
800            .is_none());
801        assert!(CodecParameters::video(CodecId::new("h264"))
802            .language
803            .is_none());
804        assert!(CodecParameters::subtitle(CodecId::new("srt"))
805            .language
806            .is_none());
807        assert!(CodecParameters::data(CodecId::new("bin"))
808            .language
809            .is_none());
810    }
811
812    #[test]
813    fn with_language_round_trips_value() {
814        let p = CodecParameters::audio(CodecId::new("aac")).with_language("jpn");
815        assert_eq!(p.language.as_deref(), Some("jpn"));
816    }
817
818    #[test]
819    fn with_language_accepts_bcp47_short_code() {
820        let p = CodecParameters::audio(CodecId::new("aac")).with_language("en");
821        assert_eq!(p.language.as_deref(), Some("en"));
822    }
823
824    #[test]
825    fn with_language_accepts_owned_string() {
826        let tag = String::from("fre");
827        let p = CodecParameters::audio(CodecId::new("aac")).with_language(tag);
828        assert_eq!(p.language.as_deref(), Some("fre"));
829    }
830}