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