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