oxideav_core/registry/codec.rs
1//! In-process codec registry.
2//!
3//! Every codec crate declares itself with one [`CodecInfo`] value —
4//! capabilities, factory functions, the container tags it claims, and
5//! (optionally) a probe function used to disambiguate genuine tag
6//! collisions. The registry stores those registrations and exposes
7//! three orthogonal lookups:
8//!
9//! - **id-keyed** — `make_decoder(params)` / `make_encoder(params)` walk
10//! the implementations registered under `params.codec_id`, filter by
11//! capability restrictions, and try them in priority order with init-
12//! time fallback.
13//! - **tag-keyed** — `resolve_tag(&ProbeContext)` walks every
14//! registration whose `tags` contains `ctx.tag`, calls each probe
15//! (treating `None` as "returns 1.0"), and returns the id with the
16//! highest resulting confidence. First-registered wins on ties.
17//! - **diagnostic** — `all_implementations`, `all_tag_registrations`.
18//!
19//! The tag path explicitly DOES NOT short-circuit on "first claim with
20//! no probe" — every claimant is asked, so a lower-priority probed
21//! claim can out-rank a higher-priority unprobed one when the content
22//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.).
23
24use std::collections::HashMap;
25
26use crate::{
27 CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecPreferences,
28 CodecResolver, CodecTag, Error, ExecutionContext, Frame, OptionField, Packet, ProbeContext,
29 ProbeFn, Result,
30};
31
32// ───────────────────────── codec traits ─────────────────────────
33
34/// A packet-to-frame decoder.
35pub trait Decoder: Send {
36 fn codec_id(&self) -> &CodecId;
37
38 /// Feed one compressed packet. May or may not produce a frame immediately —
39 /// call `receive_frame` in a loop afterwards.
40 fn send_packet(&mut self, packet: &Packet) -> Result<()>;
41
42 /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the
43 /// decoder needs another packet.
44 fn receive_frame(&mut self) -> Result<Frame>;
45
46 /// Signal end-of-stream. After this, `receive_frame` will drain buffered
47 /// frames and eventually return `Error::Eof`.
48 fn flush(&mut self) -> Result<()>;
49
50 /// Discard all carry-over state so the decoder can resume from a new
51 /// bitstream position without producing stale output. Called by the
52 /// player after a container seek.
53 ///
54 /// Unlike [`flush`](Self::flush) (which signals end-of-stream and
55 /// drains buffered frames), `reset` is expected to:
56 /// * drop every buffered input packet and pending output frame;
57 /// * zero any per-stream filter / predictor / overlap memory so the
58 /// next `send_packet` decodes as if it were the first;
59 /// * leave the codec id and stream parameters untouched.
60 ///
61 /// The default is a conservative "drain-then-forget": call
62 /// [`flush`](Self::flush) and ignore any remaining frames. Stateful
63 /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap,
64 /// reference pictures, …) should override this to wipe their
65 /// internal state explicitly — otherwise the first ~N output
66 /// samples after a seek will be glitchy until the state re-adapts.
67 fn reset(&mut self) -> Result<()> {
68 self.flush()?;
69 // Drain any remaining output frames so the next send_packet
70 // starts clean. NeedMore / Eof both mean "no more frames"; any
71 // other error is surfaced so the caller can see why.
72 loop {
73 match self.receive_frame() {
74 Ok(_) => {}
75 Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()),
76 Err(e) => return Err(e),
77 }
78 }
79 }
80
81 /// Advisory: announce the runtime environment (today: a thread budget
82 /// for codec-internal parallelism). Called at most once, before the
83 /// first `send_packet`. Default no-op; codecs that want to run
84 /// slice-/GOP-/tile-parallel override this to capture the budget.
85 /// Ignoring the hint is always safe — callers must still work with
86 /// a decoder that runs serial.
87 fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
88}
89
90/// A frame-to-packet encoder.
91pub trait Encoder: Send {
92 fn codec_id(&self) -> &CodecId;
93
94 /// Parameters describing this encoder's output stream (to feed into a muxer).
95 fn output_params(&self) -> &CodecParameters;
96
97 fn send_frame(&mut self, frame: &Frame) -> Result<()>;
98
99 fn receive_packet(&mut self) -> Result<Packet>;
100
101 fn flush(&mut self) -> Result<()>;
102
103 /// Advisory: announce the runtime environment. Same semantics as
104 /// [`Decoder::set_execution_context`].
105 fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
106}
107
108/// Factory that builds a decoder for a given codec parameter set.
109pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
110
111/// Factory that builds an encoder for a given codec parameter set.
112pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
113
114// ───────────────────────── CodecInfo ─────────────────────────
115
116/// A single registration: capabilities, decoder/encoder factories,
117/// optional probe, and the container tags this codec claims.
118///
119/// Codec crates build one of these per codec id inside their
120/// `register(reg)` function and hand it to
121/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
122/// additional fields can be added without breaking existing codec
123/// crates — construction is only possible through
124/// [`CodecInfo::new`] plus the builder methods below.
125#[non_exhaustive]
126pub struct CodecInfo {
127 pub id: CodecId,
128 pub capabilities: CodecCapabilities,
129 pub decoder_factory: Option<DecoderFactory>,
130 pub encoder_factory: Option<EncoderFactory>,
131 /// Probe function that returns a confidence in `0.0..=1.0` for a
132 /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
133 /// claimed tag" — the correct default for codecs whose tag claims
134 /// are unambiguous.
135 pub probe: Option<ProbeFn>,
136 /// Tags this codec is willing to be looked up under. One codec may
137 /// claim many tags (an AAC decoder covers several WaveFormat ids,
138 /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
139 pub tags: Vec<CodecTag>,
140 /// Schema of the encoder's recognised option keys
141 /// (`CodecParameters::options`). Attached with
142 /// [`Self::encoder_options`]. Used for validation / `oxideav list`
143 /// / pipeline JSON checks.
144 pub encoder_options_schema: Option<&'static [OptionField]>,
145 /// Schema of the decoder's recognised option keys.
146 pub decoder_options_schema: Option<&'static [OptionField]>,
147}
148
149impl CodecInfo {
150 /// Start a new registration for `id` with empty capabilities, no
151 /// factories, no probe, and no tags. Chain the builder methods
152 /// below to fill it in, then hand the result to
153 /// [`CodecRegistry::register`].
154 pub fn new(id: CodecId) -> Self {
155 Self {
156 capabilities: CodecCapabilities::audio(id.as_str()),
157 id,
158 decoder_factory: None,
159 encoder_factory: None,
160 probe: None,
161 tags: Vec::new(),
162 encoder_options_schema: None,
163 decoder_options_schema: None,
164 }
165 }
166
167 /// Replace the capability description. The default built by
168 /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
169 /// real registration should call this.
170 pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
171 self.capabilities = caps;
172 self
173 }
174
175 pub fn decoder(mut self, factory: DecoderFactory) -> Self {
176 self.decoder_factory = Some(factory);
177 self
178 }
179
180 pub fn encoder(mut self, factory: EncoderFactory) -> Self {
181 self.encoder_factory = Some(factory);
182 self
183 }
184
185 pub fn probe(mut self, probe: ProbeFn) -> Self {
186 self.probe = Some(probe);
187 self
188 }
189
190 /// Claim a single container tag for this codec. Equivalent to
191 /// `.tags([tag])` but avoids the array ceremony for single-tag
192 /// claims.
193 pub fn tag(mut self, tag: CodecTag) -> Self {
194 self.tags.push(tag);
195 self
196 }
197
198 /// Claim a set of container tags for this codec. Takes any
199 /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
200 /// codec with 3-6 tags reads as one clean block.
201 pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
202 self.tags.extend(tags);
203 self
204 }
205
206 /// Declare the options struct this codec's encoder factory expects.
207 /// Attaches `T::SCHEMA` so the registry can enumerate recognised
208 /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
209 /// The factory itself still has to call
210 /// [`crate::parse_options::<T>()`] against
211 /// `CodecParameters::options` at init time.
212 pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
213 self.encoder_options_schema = Some(T::SCHEMA);
214 self
215 }
216
217 /// Declare the options struct this codec's decoder factory expects.
218 /// See [`Self::encoder_options`] for the encoder counterpart.
219 pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
220 self.decoder_options_schema = Some(T::SCHEMA);
221 self
222 }
223}
224
225/// Internal per-impl record held inside the registry's id map. Kept
226/// distinct from [`CodecInfo`] so the id map stays cheap to walk
227/// during `make_decoder` / `make_encoder` lookups.
228#[derive(Clone)]
229pub struct CodecImplementation {
230 pub caps: CodecCapabilities,
231 pub make_decoder: Option<DecoderFactory>,
232 pub make_encoder: Option<EncoderFactory>,
233 /// Encoder options schema declared via
234 /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
235 /// no tuning knobs (any non-empty `CodecParameters::options` will
236 /// still be rejected by the factory if the encoder calls
237 /// `parse_options` — this is purely informational for discovery).
238 pub encoder_options_schema: Option<&'static [OptionField]>,
239 pub decoder_options_schema: Option<&'static [OptionField]>,
240}
241
242#[derive(Default)]
243pub struct CodecRegistry {
244 /// id → list of implementations. Each registered codec appends one
245 /// entry here. `make_decoder` / `make_encoder` walk this list in
246 /// preference order.
247 impls: HashMap<CodecId, Vec<CodecImplementation>>,
248 /// Append-only list of every registration — the `tag_index` stores
249 /// offsets into this vector.
250 registrations: Vec<RegistrationRecord>,
251 /// Tag → indices into `registrations`. Indices are stored in
252 /// registration order so tie-breaking in `resolve_tag` is
253 /// deterministic (first-registered wins).
254 tag_index: HashMap<CodecTag, Vec<usize>>,
255}
256
257/// Internal registry record. Mirrors the subset of [`CodecInfo`]
258/// needed at resolve time.
259struct RegistrationRecord {
260 id: CodecId,
261 probe: Option<ProbeFn>,
262}
263
264impl CodecRegistry {
265 pub fn new() -> Self {
266 Self::default()
267 }
268
269 /// Register one codec. Expands into:
270 /// * an entry in the id → implementations map (for
271 /// `make_decoder` / `make_encoder`);
272 /// * an entry in the tag index for every claimed tag (for
273 /// `resolve_tag`).
274 ///
275 /// Calling `register` multiple times with the same id is allowed
276 /// and how multi-implementation codecs (software-plus-hardware
277 /// FLAC, for example) are expressed.
278 pub fn register(&mut self, info: CodecInfo) {
279 let CodecInfo {
280 id,
281 capabilities,
282 decoder_factory,
283 encoder_factory,
284 probe,
285 tags,
286 encoder_options_schema,
287 decoder_options_schema,
288 } = info;
289
290 let caps = {
291 let mut c = capabilities;
292 if decoder_factory.is_some() {
293 c = c.with_decode();
294 }
295 if encoder_factory.is_some() {
296 c = c.with_encode();
297 }
298 c
299 };
300
301 // Only record an implementation entry when at least one factory
302 // is present. A "tag-only" CodecInfo — used to attach extra tag
303 // claims to a codec that was already registered with factories —
304 // shouldn't pollute the impl list.
305 if decoder_factory.is_some() || encoder_factory.is_some() {
306 self.impls
307 .entry(id.clone())
308 .or_default()
309 .push(CodecImplementation {
310 caps,
311 make_decoder: decoder_factory,
312 make_encoder: encoder_factory,
313 encoder_options_schema,
314 decoder_options_schema,
315 });
316 }
317
318 let record_idx = self.registrations.len();
319 self.registrations.push(RegistrationRecord {
320 id: id.clone(),
321 probe,
322 });
323 for tag in tags {
324 self.tag_index.entry(tag).or_default().push(record_idx);
325 }
326 }
327
328 pub fn has_decoder(&self, id: &CodecId) -> bool {
329 self.impls
330 .get(id)
331 .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
332 .unwrap_or(false)
333 }
334
335 pub fn has_encoder(&self, id: &CodecId) -> bool {
336 self.impls
337 .get(id)
338 .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
339 .unwrap_or(false)
340 }
341
342 /// Build a decoder for `params`. Walks all implementations matching the
343 /// codec id in increasing priority order, skipping any excluded by the
344 /// caller's preferences. Init-time fallback: if a higher-priority impl's
345 /// constructor returns an error, the next candidate is tried.
346 pub fn make_decoder_with(
347 &self,
348 params: &CodecParameters,
349 prefs: &CodecPreferences,
350 ) -> Result<Box<dyn Decoder>> {
351 let candidates = self
352 .impls
353 .get(¶ms.codec_id)
354 .ok_or_else(|| Error::CodecNotFound(params.codec_id.to_string()))?;
355 let mut ranked: Vec<&CodecImplementation> = candidates
356 .iter()
357 .filter(|i| i.make_decoder.is_some() && !prefs.excludes(&i.caps))
358 .filter(|i| caps_fit_params(&i.caps, params, false))
359 .collect();
360 ranked.sort_by_key(|i| prefs.effective_priority(&i.caps));
361 let mut last_err: Option<Error> = None;
362 for imp in ranked {
363 match (imp.make_decoder.unwrap())(params) {
364 Ok(d) => return Ok(d),
365 Err(e) => last_err = Some(e),
366 }
367 }
368 Err(last_err.unwrap_or_else(|| {
369 Error::CodecNotFound(format!(
370 "no decoder for {} accepts the requested parameters",
371 params.codec_id
372 ))
373 }))
374 }
375
376 /// Build an encoder, with the same priority + fallback semantics.
377 pub fn make_encoder_with(
378 &self,
379 params: &CodecParameters,
380 prefs: &CodecPreferences,
381 ) -> Result<Box<dyn Encoder>> {
382 let candidates = self
383 .impls
384 .get(¶ms.codec_id)
385 .ok_or_else(|| Error::CodecNotFound(params.codec_id.to_string()))?;
386 let mut ranked: Vec<&CodecImplementation> = candidates
387 .iter()
388 .filter(|i| i.make_encoder.is_some() && !prefs.excludes(&i.caps))
389 .filter(|i| caps_fit_params(&i.caps, params, true))
390 .collect();
391 ranked.sort_by_key(|i| prefs.effective_priority(&i.caps));
392 let mut last_err: Option<Error> = None;
393 for imp in ranked {
394 match (imp.make_encoder.unwrap())(params) {
395 Ok(e) => return Ok(e),
396 Err(e) => last_err = Some(e),
397 }
398 }
399 Err(last_err.unwrap_or_else(|| {
400 Error::CodecNotFound(format!(
401 "no encoder for {} accepts the requested parameters",
402 params.codec_id
403 ))
404 }))
405 }
406
407 /// Default-preference shorthand for `make_decoder_with`.
408 pub fn make_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
409 self.make_decoder_with(params, &CodecPreferences::default())
410 }
411
412 /// Default-preference shorthand for `make_encoder_with`.
413 pub fn make_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
414 self.make_encoder_with(params, &CodecPreferences::default())
415 }
416
417 /// Iterate codec ids that have at least one decoder implementation.
418 pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
419 self.impls
420 .iter()
421 .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
422 .map(|(id, _)| id)
423 }
424
425 pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
426 self.impls
427 .iter()
428 .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
429 .map(|(id, _)| id)
430 }
431
432 /// All registered implementations of a given codec id.
433 pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
434 self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
435 }
436
437 /// Lookup the encoder options schema for a registered codec. Walks
438 /// implementations in registration order and returns the first
439 /// schema found. `None` means either the codec isn't registered or
440 /// no implementation declared an encoder schema.
441 pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
442 self.impls
443 .get(id)?
444 .iter()
445 .find_map(|i| i.encoder_options_schema)
446 }
447
448 /// Lookup the decoder options schema — see
449 /// [`encoder_options_schema`](Self::encoder_options_schema).
450 pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
451 self.impls
452 .get(id)?
453 .iter()
454 .find_map(|i| i.decoder_options_schema)
455 }
456
457 /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
458 /// to show capability flags per implementation.
459 pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
460 self.impls
461 .iter()
462 .flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
463 }
464
465 /// Iterator over every `(tag, codec_id)` pair currently registered —
466 /// used by `oxideav tags` debug output and by tests that want to
467 /// walk the tag surface.
468 pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
469 self.tag_index.iter().flat_map(move |(tag, idxs)| {
470 idxs.iter().map(move |&i| (tag, &self.registrations[i].id))
471 })
472 }
473
474 /// Inherent form of tag resolution that returns a reference.
475 /// The owned-value form used by container code lives behind the
476 /// [`CodecResolver`] trait impl below.
477 ///
478 /// Walks every registration that claimed `ctx.tag`, calls its
479 /// probe with `ctx`, and returns the id of the registration that
480 /// scored highest. Probes that return `0.0` are discarded; ties
481 /// on confidence are broken by registration order (first wins).
482 /// Registrations with no probe are treated as returning `1.0`.
483 pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
484 let idxs = self.tag_index.get(ctx.tag)?;
485 let mut best: Option<(f32, usize)> = None;
486 for &i in idxs {
487 let rec = &self.registrations[i];
488 let conf = match rec.probe {
489 Some(f) => f(ctx),
490 None => 1.0,
491 };
492 if conf <= 0.0 {
493 continue;
494 }
495 best = match best {
496 None => Some((conf, i)),
497 Some((bc, _)) if conf > bc => Some((conf, i)),
498 other => other,
499 };
500 }
501 best.map(|(_, i)| &self.registrations[i].id)
502 }
503}
504
505/// Implement the shared [`CodecResolver`] interface so container
506/// demuxers can accept `&dyn CodecResolver` without depending on
507/// this crate directly — the trait lives in oxideav-core.
508impl CodecResolver for CodecRegistry {
509 fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
510 self.resolve_tag_ref(ctx).cloned()
511 }
512}
513
514/// Check whether an implementation's restrictions are compatible with the
515/// requested codec parameters. `for_encode` swaps the rare cases where a
516/// restriction only applies one way.
517fn caps_fit_params(caps: &CodecCapabilities, p: &CodecParameters, for_encode: bool) -> bool {
518 let _ = for_encode; // reserved for future use (e.g. encode-only bitrate caps)
519 if let (Some(max), Some(w)) = (caps.max_width, p.width) {
520 if w > max {
521 return false;
522 }
523 }
524 if let (Some(max), Some(h)) = (caps.max_height, p.height) {
525 if h > max {
526 return false;
527 }
528 }
529 if let (Some(max), Some(br)) = (caps.max_bitrate, p.bit_rate) {
530 if br > max {
531 return false;
532 }
533 }
534 if let (Some(max), Some(sr)) = (caps.max_sample_rate, p.sample_rate) {
535 if sr > max {
536 return false;
537 }
538 }
539 if let (Some(max), Some(ch)) = (caps.max_channels, p.channels) {
540 if ch > max {
541 return false;
542 }
543 }
544 true
545}
546
547#[cfg(test)]
548mod tag_tests {
549 use super::*;
550 use crate::CodecCapabilities;
551
552 /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
553 /// 0x000001 start code in the first few bytes).
554 fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
555 match ctx.packet {
556 Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
557 Some(_) => 0.0,
558 None => 0.5, // no data yet — weak evidence
559 }
560 }
561
562 /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
563 /// (starts with a 0x000001 start code in the first few bytes).
564 fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
565 match ctx.packet {
566 Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
567 Some(_) => 0.0,
568 None => 0.5,
569 }
570 }
571
572 fn info(id: &str) -> CodecInfo {
573 CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
574 }
575
576 #[test]
577 fn resolve_single_claim_no_probe() {
578 let mut reg = CodecRegistry::new();
579 reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
580 let t = CodecTag::fourcc(b"FLAC");
581 assert_eq!(
582 reg.resolve_tag_ref(&ProbeContext::new(&t))
583 .map(|c| c.as_str()),
584 Some("flac"),
585 );
586 }
587
588 #[test]
589 fn resolve_missing_tag_returns_none() {
590 let reg = CodecRegistry::new();
591 let t = CodecTag::fourcc(b"????");
592 assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
593 }
594
595 #[test]
596 fn unprobed_claims_tie_first_registered_wins() {
597 // Two unprobed claims on the same tag: deterministic order.
598 let mut reg = CodecRegistry::new();
599 reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
600 reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
601 let t = CodecTag::fourcc(b"TEST");
602 assert_eq!(
603 reg.resolve_tag_ref(&ProbeContext::new(&t))
604 .map(|c| c.as_str()),
605 Some("first"),
606 );
607 }
608
609 #[test]
610 fn probe_picks_matching_bitstream() {
611 // The core bug fix: every probe is asked and the highest
612 // confidence wins regardless of registration order.
613 let mut reg = CodecRegistry::new();
614 reg.register(
615 info("msmpeg4v3")
616 .probe(probe_msmpeg4)
617 .tag(CodecTag::fourcc(b"DIV3")),
618 );
619 reg.register(
620 info("mpeg4video")
621 .probe(probe_mpeg4_part2)
622 .tag(CodecTag::fourcc(b"DIV3")),
623 );
624
625 let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
626 let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
627 let tag = CodecTag::fourcc(b"DIV3");
628
629 let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
630 assert_eq!(
631 reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
632 Some("mpeg4video"),
633 );
634 let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
635 assert_eq!(
636 reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
637 Some("msmpeg4v3"),
638 );
639 }
640
641 #[test]
642 fn unprobed_claim_wins_against_low_confidence_probe() {
643 // One codec claims a tag without a probe (→ confidence 1.0)
644 // and another claims it with a probe returning 0.3. The
645 // unprobed one wins — a codec that knows it owns the tag
646 // outright should not lose to a speculative probe.
647 let mut reg = CodecRegistry::new();
648 reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
649 reg.register(
650 info("speculative")
651 .probe(|_| 0.3)
652 .tag(CodecTag::fourcc(b"OWN_")),
653 );
654 let t = CodecTag::fourcc(b"OWN_");
655 assert_eq!(
656 reg.resolve_tag_ref(&ProbeContext::new(&t))
657 .map(|c| c.as_str()),
658 Some("owner"),
659 );
660 }
661
662 #[test]
663 fn probe_returning_zero_is_skipped() {
664 let mut reg = CodecRegistry::new();
665 reg.register(
666 info("refuses")
667 .probe(|_| 0.0)
668 .tag(CodecTag::fourcc(b"MAYB")),
669 );
670 reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
671 let t = CodecTag::fourcc(b"MAYB");
672 let ctx = ProbeContext::new(&t).packet(b"hello");
673 assert_eq!(
674 reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
675 Some("fallback"),
676 );
677 }
678
679 #[test]
680 fn fourcc_case_insensitive_lookup() {
681 let mut reg = CodecRegistry::new();
682 reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
683 // Registered as "DIV3" (uppercase via ctor); lookup using
684 // lowercase / mixed case also hits.
685 let upper = CodecTag::fourcc(b"DIV3");
686 let lower = CodecTag::fourcc(b"div3");
687 let mixed = CodecTag::fourcc(b"DiV3");
688 assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
689 assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
690 assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
691 }
692
693 #[test]
694 fn wave_format_and_matroska_tags_work() {
695 let mut reg = CodecRegistry::new();
696 reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
697 reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
698 let wf = CodecTag::wave_format(0x0055);
699 let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
700 assert_eq!(
701 reg.resolve_tag_ref(&ProbeContext::new(&wf))
702 .map(|c| c.as_str()),
703 Some("mp3"),
704 );
705 assert_eq!(
706 reg.resolve_tag_ref(&ProbeContext::new(&mk))
707 .map(|c| c.as_str()),
708 Some("h264"),
709 );
710 }
711
712 #[test]
713 fn mp4_object_type_tag_works() {
714 let mut reg = CodecRegistry::new();
715 reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
716 let t = CodecTag::mp4_object_type(0x40);
717 assert_eq!(
718 reg.resolve_tag_ref(&ProbeContext::new(&t))
719 .map(|c| c.as_str()),
720 Some("aac"),
721 );
722 }
723
724 #[test]
725 fn multi_tag_claim_all_resolve() {
726 let mut reg = CodecRegistry::new();
727 reg.register(info("aac").tags([
728 CodecTag::fourcc(b"MP4A"),
729 CodecTag::wave_format(0x00FF),
730 CodecTag::mp4_object_type(0x40),
731 CodecTag::matroska("A_AAC"),
732 ]));
733 for t in [
734 CodecTag::fourcc(b"MP4A"),
735 CodecTag::wave_format(0x00FF),
736 CodecTag::mp4_object_type(0x40),
737 CodecTag::matroska("A_AAC"),
738 ] {
739 assert_eq!(
740 reg.resolve_tag_ref(&ProbeContext::new(&t))
741 .map(|c| c.as_str()),
742 Some("aac"),
743 "tag {t:?} did not resolve",
744 );
745 }
746 }
747}