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