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