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