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 /// Read-only access to the DoS-protection caps for any decoder
679 /// constructed from these parameters. See [`DecoderLimits`].
680 pub fn limits(&self) -> &DecoderLimits {
681 &self.limits
682 }
683
684 /// Builder method: replace the [`DecoderLimits`] for these
685 /// parameters. Use to tighten caps before passing parameters into
686 /// `make_decoder` (e.g. when processing untrusted uploads on a
687 /// shared server).
688 ///
689 /// ```
690 /// # use oxideav_core::{CodecId, CodecParameters, DecoderLimits};
691 /// let limits = DecoderLimits::default()
692 /// .with_max_pixels_per_frame(4096 * 4096)
693 /// .with_max_arenas_in_flight(2);
694 /// let p = CodecParameters::video(CodecId::new("h263")).with_limits(limits);
695 /// assert_eq!(p.limits().max_pixels_per_frame, 4096 * 4096);
696 /// ```
697 pub fn with_limits(mut self, limits: DecoderLimits) -> Self {
698 self.limits = limits;
699 self
700 }
701
702 /// Bind subsequent decoder/encoder construction to a specific device.
703 /// `index` matches the position in the `engine_probe` device list.
704 ///
705 /// Software codecs ignore this field. Hardware codecs read it as
706 /// `params.device_index.unwrap_or(0)` to pick which physical engine
707 /// to bind to.
708 pub fn with_device_index(mut self, index: u32) -> Self {
709 self.device_index = Some(index);
710 self
711 }
712
713 /// Builder method: set the on-wire [`tag`](Self::tag).
714 ///
715 /// Demuxers call this from their stream-format parser so muxers
716 /// re-emitting the stream preserve the original FourCC / wFormatTag
717 /// byte-for-byte. Encoders call this in `output_params()` to
718 /// announce which wire tag they're producing.
719 ///
720 /// ```
721 /// # use oxideav_core::{CodecId, CodecParameters, CodecTag};
722 /// let p = CodecParameters::video(CodecId::new("magicyuv"))
723 /// .with_tag(CodecTag::fourcc(b"M8RG"));
724 /// assert_eq!(p.tag, Some(CodecTag::fourcc(b"M8RG")));
725 /// ```
726 pub fn with_tag(mut self, tag: CodecTag) -> Self {
727 self.tag = Some(tag);
728 self
729 }
730
731 /// Builder method: set the per-stream [`language`](Self::language)
732 /// tag. Accepts any string — BCP-47 short codes (`"en"`), ISO
733 /// 639-2/T three-letter codes (`"jpn"`), or container-native
734 /// values are all passed through verbatim. No validation is
735 /// performed; the muxer writes whatever the caller hands in.
736 ///
737 /// ```
738 /// # use oxideav_core::{CodecId, CodecParameters};
739 /// let p = CodecParameters::audio(CodecId::new("aac")).with_language("jpn");
740 /// assert_eq!(p.language.as_deref(), Some("jpn"));
741 /// ```
742 pub fn with_language(mut self, language: impl Into<String>) -> Self {
743 self.language = Some(language.into());
744 self
745 }
746
747 /// Builder method: set the stream's
748 /// [`color_signal`](Self::color_signal).
749 ///
750 /// ```
751 /// # use oxideav_core::{CodecId, CodecParameters, ColorRange, ColorSignal, PixelFormat};
752 /// // A 10-bit full-range BT.2020 PQ stream, as a container's colour
753 /// // record describes it (primaries 9, transfer 16, matrix 9).
754 /// let mut p = CodecParameters::video(CodecId::new("h265"))
755 /// .with_color_signal(ColorSignal::from_code_points(9, 16, 9, true));
756 /// p.pixel_format = Some(PixelFormat::Yuv420P10Le);
757 /// assert_eq!(p.resolved_color_range(), ColorRange::Full);
758 /// ```
759 pub fn with_color_signal(mut self, signal: ColorSignal) -> Self {
760 self.color_signal = signal;
761 self
762 }
763
764 /// Builder method: set only the sample range of the stream's
765 /// [`color_signal`](Self::color_signal), leaving the H.273 triple
766 /// as it was. For producers that know the range (an alpha plane is
767 /// full range; a container flag said so) but nothing else.
768 pub fn with_color_range(mut self, range: ColorRange) -> Self {
769 self.color_signal.range = range;
770 self
771 }
772
773 /// The stream's effective sample range: the explicit
774 /// [`color_signal`](Self::color_signal) range when specified,
775 /// otherwise the range implied by the
776 /// [`pixel_format`](Self::pixel_format) label
777 /// ([`PixelFormat::implied_color_range`] — `Full` for the `YuvJ*`
778 /// formats), otherwise [`ColorRange::Unspecified`]. Converters
779 /// apply their own policy to `Unspecified` (H.273 suggests limited
780 /// for video imagery).
781 pub fn resolved_color_range(&self) -> ColorRange {
782 if !self.color_signal.range.is_unspecified() {
783 return self.color_signal.range;
784 }
785 self.pixel_format
786 .and_then(|f| f.implied_color_range())
787 .unwrap_or(ColorRange::Unspecified)
788 }
789
790 /// Builder method: replace the stream's [`layers`](Self::layers)
791 /// description.
792 ///
793 /// ```
794 /// # use oxideav_core::{CodecId, CodecParameters, LayerInfo};
795 /// // A stereo pair: base layer = left view, layer 1 = right view
796 /// // predicted from the base.
797 /// let p = CodecParameters::video(CodecId::new("h265")).with_layers(vec![
798 /// LayerInfo::new(0).with_view_id(0),
799 /// LayerInfo::new(1).with_view_id(1).with_depends_on([0u16]),
800 /// ]);
801 /// assert_eq!(p.layers.len(), 2);
802 /// assert_eq!(p.layer(1).and_then(|l| l.view_id), Some(1));
803 /// assert!(p.is_multi_layer());
804 /// ```
805 pub fn with_layers(mut self, layers: Vec<LayerInfo>) -> Self {
806 self.layers = layers;
807 self
808 }
809
810 /// The description of layer `layer_id`, when
811 /// [`layers`](Self::layers) lists it.
812 pub fn layer(&self, layer_id: u16) -> Option<&LayerInfo> {
813 self.layers.iter().find(|l| l.layer_id == layer_id)
814 }
815
816 /// `true` when [`layers`](Self::layers) describes more than one
817 /// layer. Single-layer streams (and streams whose structure is not
818 /// described) report `false`.
819 pub fn is_multi_layer(&self) -> bool {
820 self.layers.len() > 1
821 }
822
823 /// The stream's attached metadata [`blobs`](Self::blobs), in
824 /// producer order.
825 pub fn blobs(&self) -> &[MetadataBlob] {
826 &self.blobs
827 }
828
829 /// The first attached blob of `kind`, if any. For kinds that may
830 /// legitimately repeat (several `cover-art` pictures) iterate
831 /// [`blobs_of`](Self::blobs_of) instead.
832 ///
833 /// ```
834 /// # use oxideav_core::{BlobKind, CodecId, CodecParameters, MetadataBlob};
835 /// let p = CodecParameters::video(CodecId::new("png"))
836 /// .with_blob(MetadataBlob::new(BlobKind::ICC, vec![0u8; 128]));
837 /// assert_eq!(p.blob(&BlobKind::ICC).map(|b| b.data.len()), Some(128));
838 /// assert!(p.blob(&BlobKind::EXIF).is_none());
839 /// ```
840 pub fn blob(&self, kind: &BlobKind) -> Option<&MetadataBlob> {
841 self.blobs.iter().find(|b| b.is(kind))
842 }
843
844 /// Every attached blob of `kind`, in producer order. Takes the kind
845 /// by value so `p.blobs_of(BlobKind::COVER_ART)` borrows nothing
846 /// but `self` (the constants are allocation-free).
847 pub fn blobs_of(&self, kind: BlobKind) -> impl Iterator<Item = &MetadataBlob> + '_ {
848 self.blobs.iter().filter(move |b| b.is(&kind))
849 }
850
851 /// Append a metadata blob. Blobs of the same kind are kept in
852 /// insertion order; nothing is replaced.
853 pub fn push_blob(&mut self, blob: MetadataBlob) {
854 self.blobs.push(blob);
855 }
856
857 /// Builder method: append a metadata blob (see
858 /// [`push_blob`](Self::push_blob)).
859 pub fn with_blob(mut self, blob: MetadataBlob) -> Self {
860 self.blobs.push(blob);
861 self
862 }
863
864 /// Builder method: replace the whole [`blobs`](Self::blobs) list.
865 pub fn with_blobs(mut self, blobs: Vec<MetadataBlob>) -> Self {
866 self.blobs = blobs;
867 self
868 }
869
870 /// Detach and return every attached blob, leaving the list empty.
871 pub fn take_blobs(&mut self) -> Vec<MetadataBlob> {
872 std::mem::take(&mut self.blobs)
873 }
874}
875
876/// Description of a single stream inside a container.
877#[derive(Clone, Debug)]
878pub struct StreamInfo {
879 /// 0-based index of the stream within its container.
880 pub index: u32,
881 /// Time base in which this stream's packet timestamps (and
882 /// `duration` / `start_time` below) are expressed.
883 pub time_base: TimeBase,
884 /// Stream duration in `time_base` units, when the container
885 /// declares one.
886 pub duration: Option<i64>,
887 /// Presentation timestamp of the first packet, in `time_base`
888 /// units, when known.
889 pub start_time: Option<i64>,
890 /// Codec-level parameters (codec id, format, extradata, ...).
891 pub params: CodecParameters,
892}
893
894#[cfg(test)]
895mod codec_tag_tests {
896 use super::*;
897
898 #[test]
899 fn fourcc_uppercases_on_construction() {
900 let t = CodecTag::fourcc(b"div3");
901 assert_eq!(t, CodecTag::Fourcc(*b"DIV3"));
902 // Non-alphabetic bytes preserved unchanged.
903 let t2 = CodecTag::fourcc(b"MP42");
904 assert_eq!(t2, CodecTag::Fourcc(*b"MP42"));
905 let t3 = CodecTag::fourcc(&[0xFF, b'a', 0x00, b'1']);
906 assert_eq!(t3, CodecTag::Fourcc([0xFF, b'A', 0x00, b'1']));
907 }
908
909 #[test]
910 fn fourcc_equality_case_insensitive_via_ctor() {
911 assert_eq!(CodecTag::fourcc(b"xvid"), CodecTag::fourcc(b"XVID"));
912 assert_eq!(CodecTag::fourcc(b"DiV3"), CodecTag::fourcc(b"div3"));
913 }
914
915 #[test]
916 fn display_printable_fourcc() {
917 assert_eq!(CodecTag::fourcc(b"XVID").to_string(), "fourcc(XVID)");
918 }
919
920 #[test]
921 fn display_non_printable_fourcc_as_hex() {
922 let t = CodecTag::Fourcc([0x00, 0x00, 0x00, 0x01]);
923 assert_eq!(t.to_string(), "fourcc(0x00000001)");
924 }
925
926 #[test]
927 fn display_wave_format() {
928 assert_eq!(
929 CodecTag::wave_format(0x0055).to_string(),
930 "wFormatTag(0x0055)"
931 );
932 }
933
934 #[test]
935 fn display_mp4_oti() {
936 assert_eq!(CodecTag::mp4_object_type(0x40).to_string(), "mp4_oti(0x40)");
937 }
938
939 #[test]
940 fn display_matroska() {
941 assert_eq!(
942 CodecTag::matroska("V_MPEG4/ISO/AVC").to_string(),
943 "matroska(V_MPEG4/ISO/AVC)",
944 );
945 }
946
947 #[test]
948 fn null_resolver_resolves_nothing() {
949 let r = NullCodecResolver;
950 let xvid = CodecTag::fourcc(b"XVID");
951 assert!(r.resolve_tag(&ProbeContext::new(&xvid)).is_none());
952 let wf = CodecTag::wave_format(0x0055);
953 assert!(r.resolve_tag(&ProbeContext::new(&wf)).is_none());
954 }
955
956 #[test]
957 fn probe_context_builder_fills_hints() {
958 let tag = CodecTag::wave_format(0x0001);
959 let ctx = ProbeContext::new(&tag)
960 .bits(24)
961 .channels(2)
962 .sample_rate(48_000)
963 .header(&[1, 2, 3])
964 .packet(&[4, 5]);
965 assert_eq!(ctx.bits_per_sample, Some(24));
966 assert_eq!(ctx.channels, Some(2));
967 assert_eq!(ctx.sample_rate, Some(48_000));
968 assert_eq!(ctx.header.unwrap(), &[1, 2, 3]);
969 assert_eq!(ctx.packet.unwrap(), &[4, 5]);
970 }
971}
972
973#[cfg(test)]
974mod channel_layout_plumbing_tests {
975 use super::*;
976
977 #[test]
978 fn audio_params_default_to_no_layout() {
979 let p = CodecParameters::audio(CodecId::new("pcm_s16le"));
980 assert!(p.channel_layout.is_none());
981 assert!(p.channels.is_none());
982 assert!(p.resolved_layout().is_none());
983 assert!(p.resolved_channels().is_none());
984 }
985
986 #[test]
987 fn channels_only_infers_layout_via_from_count() {
988 let p = CodecParameters::audio(CodecId::new("pcm_s16le")).channels(6);
989 assert_eq!(p.channels, Some(6));
990 assert!(p.channel_layout.is_none());
991 assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround51));
992 assert_eq!(p.resolved_channels(), Some(6));
993 }
994
995 #[test]
996 fn explicit_layout_wins_over_count() {
997 let p = CodecParameters::audio(CodecId::new("ac3"))
998 .channels(6)
999 .channel_layout(ChannelLayout::Surround60);
1000 // 6ch by-count would default to Surround51, but the explicit
1001 // layout overrides.
1002 assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround60));
1003 assert_eq!(p.resolved_channels(), Some(6));
1004 }
1005
1006 #[test]
1007 fn layout_only_yields_count_via_resolved_channels() {
1008 let p =
1009 CodecParameters::audio(CodecId::new("ac3")).channel_layout(ChannelLayout::Surround71);
1010 assert!(p.channels.is_none());
1011 assert_eq!(p.resolved_channels(), Some(8));
1012 assert_eq!(p.resolved_layout(), Some(ChannelLayout::Surround71));
1013 }
1014}
1015
1016#[cfg(test)]
1017mod codec_parameters_device_index_tests {
1018 use super::*;
1019
1020 #[test]
1021 fn codec_parameters_device_index_defaults_to_none() {
1022 assert!(CodecParameters::audio(CodecId::new("pcm_s16le"))
1023 .device_index
1024 .is_none());
1025 assert!(CodecParameters::video(CodecId::new("h264"))
1026 .device_index
1027 .is_none());
1028 assert!(CodecParameters::subtitle(CodecId::new("srt"))
1029 .device_index
1030 .is_none());
1031 assert!(CodecParameters::data(CodecId::new("bin"))
1032 .device_index
1033 .is_none());
1034 }
1035
1036 #[test]
1037 fn codec_parameters_with_device_index_sets_field() {
1038 let p = CodecParameters::video(CodecId::new("h264")).with_device_index(2);
1039 assert_eq!(p.device_index, Some(2));
1040 }
1041}
1042
1043#[cfg(test)]
1044mod codec_parameters_tag_tests {
1045 use super::*;
1046
1047 #[test]
1048 fn tag_defaults_to_none_on_every_constructor() {
1049 assert!(CodecParameters::audio(CodecId::new("aac")).tag.is_none());
1050 assert!(CodecParameters::video(CodecId::new("h264")).tag.is_none());
1051 assert!(CodecParameters::subtitle(CodecId::new("srt")).tag.is_none());
1052 assert!(CodecParameters::data(CodecId::new("bin")).tag.is_none());
1053 }
1054
1055 #[test]
1056 fn with_tag_builder_sets_field() {
1057 let p =
1058 CodecParameters::video(CodecId::new("magicyuv")).with_tag(CodecTag::fourcc(b"M8RG"));
1059 assert_eq!(p.tag, Some(CodecTag::fourcc(b"M8RG")));
1060 }
1061
1062 #[test]
1063 fn with_tag_round_trip_preserves_demuxed_fourcc() {
1064 // The canonical use-case: a demuxer sees DIVX in the bitstream
1065 // and tags the params accordingly. The mpeg4video codec also
1066 // claims XVID / MP4V / FMP4, but the muxer must re-emit DIVX.
1067 let demuxed =
1068 CodecParameters::video(CodecId::new("mpeg4video")).with_tag(CodecTag::fourcc(b"DIVX"));
1069 // Muxer reads `params.tag` directly — no registry round-trip.
1070 assert_eq!(demuxed.tag, Some(CodecTag::fourcc(b"DIVX")));
1071 }
1072
1073 #[test]
1074 fn wave_format_tag_preserved() {
1075 let p = CodecParameters::audio(CodecId::new("mp3")).with_tag(CodecTag::wave_format(0x0055));
1076 assert_eq!(p.tag, Some(CodecTag::WaveFormat(0x0055)));
1077 }
1078}
1079
1080#[cfg(test)]
1081mod codec_parameters_language_tests {
1082 use super::*;
1083
1084 #[test]
1085 fn language_defaults_to_none_on_every_constructor() {
1086 assert!(CodecParameters::audio(CodecId::new("aac"))
1087 .language
1088 .is_none());
1089 assert!(CodecParameters::video(CodecId::new("h264"))
1090 .language
1091 .is_none());
1092 assert!(CodecParameters::subtitle(CodecId::new("srt"))
1093 .language
1094 .is_none());
1095 assert!(CodecParameters::data(CodecId::new("bin"))
1096 .language
1097 .is_none());
1098 }
1099
1100 #[test]
1101 fn with_language_round_trips_value() {
1102 let p = CodecParameters::audio(CodecId::new("aac")).with_language("jpn");
1103 assert_eq!(p.language.as_deref(), Some("jpn"));
1104 }
1105
1106 #[test]
1107 fn with_language_accepts_bcp47_short_code() {
1108 let p = CodecParameters::audio(CodecId::new("aac")).with_language("en");
1109 assert_eq!(p.language.as_deref(), Some("en"));
1110 }
1111
1112 #[test]
1113 fn with_language_accepts_owned_string() {
1114 let tag = String::from("fre");
1115 let p = CodecParameters::audio(CodecId::new("aac")).with_language(tag);
1116 assert_eq!(p.language.as_deref(), Some("fre"));
1117 }
1118}
1119
1120#[cfg(test)]
1121mod codec_parameters_color_signal_tests {
1122 use super::*;
1123 use crate::signal::{ColorPrimaries, MatrixCoefficients, TransferCharacteristics};
1124
1125 #[test]
1126 fn color_signal_defaults_to_unspecified_on_every_constructor() {
1127 for p in [
1128 CodecParameters::audio(CodecId::new("aac")),
1129 CodecParameters::video(CodecId::new("h264")),
1130 CodecParameters::subtitle(CodecId::new("srt")),
1131 CodecParameters::data(CodecId::new("bin")),
1132 ] {
1133 assert!(p.color_signal.is_unspecified());
1134 assert_eq!(p.color_signal, ColorSignal::default());
1135 assert_eq!(p.resolved_color_range(), ColorRange::Unspecified);
1136 assert!(p.layers.is_empty());
1137 assert!(!p.is_multi_layer());
1138 assert!(p.layer(0).is_none());
1139 }
1140 }
1141
1142 #[test]
1143 fn with_color_signal_sets_field_and_survives_clone() {
1144 let sig = ColorSignal::from_code_points(9, 16, 9, false);
1145 let p = CodecParameters::video(CodecId::new("h265")).with_color_signal(sig);
1146 assert_eq!(p.color_signal, sig);
1147 assert_eq!(p.resolved_color_range(), ColorRange::Limited);
1148 let c = p.clone();
1149 assert_eq!(c.color_signal, sig);
1150 }
1151
1152 #[test]
1153 fn with_color_range_touches_only_the_range() {
1154 let p = CodecParameters::video(CodecId::new("h265"))
1155 .with_color_signal(ColorSignal::bt709_limited())
1156 .with_color_range(ColorRange::Full);
1157 assert_eq!(p.color_signal.range, ColorRange::Full);
1158 assert_eq!(p.color_signal.primaries, ColorPrimaries::BT709);
1159 assert_eq!(p.color_signal.transfer, TransferCharacteristics::BT709);
1160 assert_eq!(p.color_signal.matrix, MatrixCoefficients::BT709);
1161 }
1162
1163 #[test]
1164 fn resolved_range_prefers_explicit_signal_over_pixel_format_label() {
1165 // Label only: YuvJ ⇒ Full.
1166 let mut p = CodecParameters::video(CodecId::new("mjpeg"));
1167 p.pixel_format = Some(PixelFormat::YuvJ420P);
1168 assert_eq!(p.resolved_color_range(), ColorRange::Full);
1169 // Limited label with no signal ⇒ unspecified (not a guess).
1170 p.pixel_format = Some(PixelFormat::Yuv420P);
1171 assert_eq!(p.resolved_color_range(), ColorRange::Unspecified);
1172 // 10-bit full-range signal on a non-J label ⇒ Full — the case
1173 // the labels alone cannot express.
1174 p.pixel_format = Some(PixelFormat::Yuv420P10Le);
1175 p.color_signal.range = ColorRange::Full;
1176 assert_eq!(p.resolved_color_range(), ColorRange::Full);
1177 // An explicit Limited signal wins even over a J label.
1178 p.pixel_format = Some(PixelFormat::YuvJ444P);
1179 p.color_signal.range = ColorRange::Limited;
1180 assert_eq!(p.resolved_color_range(), ColorRange::Limited);
1181 // Alpha as Gray8 with an explicit full range.
1182 p.pixel_format = Some(PixelFormat::Gray8);
1183 p.color_signal.range = ColorRange::Full;
1184 assert_eq!(p.resolved_color_range(), ColorRange::Full);
1185 }
1186
1187 #[test]
1188 fn matches_core_ignores_color_signal_and_layers() {
1189 let a = CodecParameters::video(CodecId::new("h265"));
1190 let b = a
1191 .clone()
1192 .with_color_signal(ColorSignal::srgb())
1193 .with_layers(vec![LayerInfo::new(0), LayerInfo::new(1)]);
1194 assert!(a.matches_core(&b));
1195 assert!(b.matches_core(&a));
1196 }
1197
1198 #[test]
1199 fn matches_core_ignores_blobs() {
1200 let a = CodecParameters::video(CodecId::new("png"));
1201 let b = a
1202 .clone()
1203 .with_blob(MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3]));
1204 assert!(a.matches_core(&b));
1205 assert!(b.matches_core(&a));
1206 assert!(a.blobs().is_empty());
1207 assert_eq!(b.blobs().len(), 1);
1208 }
1209
1210 #[test]
1211 fn blobs_accessors_push_lookup_iterate_take() {
1212 let mut p = CodecParameters::audio(CodecId::new("mp3"));
1213 assert!(p.blobs().is_empty());
1214 assert!(p.blob(&BlobKind::COVER_ART).is_none());
1215 assert_eq!(p.blobs_of(BlobKind::COVER_ART).count(), 0);
1216
1217 p.push_blob(
1218 MetadataBlob::new(BlobKind::COVER_ART, vec![0xFF, 0xD8]).with_mime("image/jpeg"),
1219 );
1220 p.push_blob(MetadataBlob::new(
1221 BlobKind::custom("id3-text"),
1222 b"TIT2".to_vec(),
1223 ));
1224 p.push_blob(
1225 MetadataBlob::new(BlobKind::COVER_ART, vec![0x89, b'P']).with_mime("image/png"),
1226 );
1227
1228 // First of kind, in insertion order; every one of kind.
1229 assert_eq!(
1230 p.blob(&BlobKind::COVER_ART).and_then(|b| b.mime.as_deref()),
1231 Some("image/jpeg")
1232 );
1233 let covers: Vec<&str> = p
1234 .blobs_of(BlobKind::COVER_ART)
1235 .filter_map(|b| b.mime.as_deref())
1236 .collect();
1237 assert_eq!(covers, ["image/jpeg", "image/png"]);
1238 assert_eq!(
1239 p.blob(&BlobKind::custom("id3-text"))
1240 .map(|b| b.data.as_slice()),
1241 Some(&b"TIT2"[..])
1242 );
1243 assert!(p.blob(&BlobKind::ICC).is_none());
1244 assert_eq!(p.blobs().len(), 3);
1245
1246 // Field and accessor agree; clone carries the list.
1247 assert_eq!(p.blobs, p.blobs().to_vec());
1248 assert_eq!(p.clone().blobs().len(), 3);
1249
1250 // take empties; with_blobs replaces wholesale.
1251 let taken = p.take_blobs();
1252 assert_eq!(taken.len(), 3);
1253 assert!(p.blobs().is_empty());
1254 let q = p.with_blobs(vec![MetadataBlob::new(BlobKind::XMP, b"<x/>".to_vec())]);
1255 assert_eq!(q.blobs().len(), 1);
1256 assert!(q.blob(&BlobKind::XMP).is_some());
1257 }
1258
1259 #[test]
1260 fn every_constructor_starts_without_blobs() {
1261 let id = CodecId::new("x");
1262 for p in [
1263 CodecParameters::audio(id.clone()),
1264 CodecParameters::video(id.clone()),
1265 CodecParameters::subtitle(id.clone()),
1266 CodecParameters::data(id),
1267 ] {
1268 assert!(p.blobs().is_empty());
1269 }
1270 }
1271
1272 /// The same API carries an ICC profile on an image stream and cover
1273 /// art on an audio stream: nothing in the mechanism is specific to
1274 /// either medium or to any container.
1275 #[test]
1276 fn blobs_are_media_agnostic() {
1277 let icc = vec![0u8; 16];
1278 let jpeg = vec![0xFF, 0xD8, 0xFF, 0xE0];
1279
1280 let mut image = CodecParameters::video(CodecId::new("png"));
1281 image.push_blob(MetadataBlob::new(BlobKind::ICC, icc.clone()));
1282 let mut audio = CodecParameters::audio(CodecId::new("flac"));
1283 audio.push_blob(
1284 MetadataBlob::new(BlobKind::COVER_ART, jpeg.clone()).with_mime("image/jpeg"),
1285 );
1286
1287 // One generic routine serves both streams.
1288 fn first_payload<'a>(p: &'a CodecParameters, kind: &BlobKind) -> Option<&'a [u8]> {
1289 p.blob(kind).map(|b| b.data.as_slice())
1290 }
1291 assert_eq!(first_payload(&image, &BlobKind::ICC), Some(icc.as_slice()));
1292 assert_eq!(first_payload(&image, &BlobKind::COVER_ART), None);
1293 assert_eq!(
1294 first_payload(&audio, &BlobKind::COVER_ART),
1295 Some(jpeg.as_slice())
1296 );
1297 assert_eq!(first_payload(&audio, &BlobKind::ICC), None);
1298
1299 // Moving a blob between streams of different media is a plain
1300 // value move — the blob knows nothing about its carrier.
1301 let moved = audio.take_blobs();
1302 let image = image.with_blobs(moved);
1303 assert_eq!(
1304 image
1305 .blob(&BlobKind::COVER_ART)
1306 .and_then(|b| b.mime.as_deref()),
1307 Some("image/jpeg")
1308 );
1309 assert!(image.blob(&BlobKind::ICC).is_none());
1310 }
1311
1312 #[test]
1313 fn layers_builder_lookup_and_clone() {
1314 let p = CodecParameters::video(CodecId::new("h265")).with_layers(vec![
1315 LayerInfo::new(0).with_view_id(0),
1316 LayerInfo::new(1).with_view_id(1).with_depends_on([0u16]),
1317 LayerInfo::new(2).with_depends_on(vec![0, 1]),
1318 ]);
1319 assert!(p.is_multi_layer());
1320 assert_eq!(p.layer(0).map(|l| l.view_id), Some(Some(0)));
1321 assert_eq!(
1322 p.layer(1).map(|l| l.depends_on.as_slice()),
1323 Some(&[0u16][..])
1324 );
1325 assert_eq!(p.layer(2).map(|l| l.depends_on.len()), Some(2));
1326 assert!(p.layer(3).is_none());
1327 let c = p.clone();
1328 assert_eq!(c.layers, p.layers);
1329 // A single described layer is not "multi-layer".
1330 let single =
1331 CodecParameters::video(CodecId::new("h265")).with_layers(vec![LayerInfo::new(0)]);
1332 assert!(!single.is_multi_layer());
1333 }
1334}