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