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