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}