Skip to main content

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}