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