Skip to main content

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, CodecResolver, CodecTag,
29    Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn,
30    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    /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`,
256    /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every
257    /// `CodecInfo` they register; SW codecs leave this `None`.
258    /// Consumers (e.g. the CLI's `info` command) use it to group
259    /// codec entries by backend and to dedupe probe calls — multiple
260    /// `CodecInfo` entries with the same `engine_id` typically share
261    /// an `engine_probe` function, and consumers should call the probe
262    /// at most once per `engine_id` per pass. Attached via
263    /// [`Self::with_engine_id`].
264    pub engine_id: Option<&'static str>,
265    /// Optional engine probe function. When `Some`, calling it returns
266    /// one [`crate::engine::HwDeviceInfo`] entry per device the backend
267    /// sees. Phase-2 HW siblings populate this on every `CodecInfo`
268    /// they register; Phase-3 consumers (CLI) call it on demand.
269    /// Attached via [`Self::with_engine_probe`].
270    pub engine_probe: Option<crate::engine::EngineProbeFn>,
271}
272
273impl CodecInfo {
274    /// Start a new registration for `id` with empty capabilities, no
275    /// factories, no probe, and no tags. Chain the builder methods
276    /// below to fill it in, then hand the result to
277    /// [`CodecRegistry::register`].
278    pub fn new(id: CodecId) -> Self {
279        Self {
280            capabilities: CodecCapabilities::audio(id.as_str()),
281            id,
282            decoder_factory: None,
283            encoder_factory: None,
284            probe: None,
285            tags: Vec::new(),
286            encoder_options_schema: None,
287            decoder_options_schema: None,
288            engine_id: None,
289            engine_probe: None,
290        }
291    }
292
293    /// Replace the capability description. The default built by
294    /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
295    /// real registration should call this.
296    pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
297        self.capabilities = caps;
298        self
299    }
300
301    pub fn decoder(mut self, factory: DecoderFactory) -> Self {
302        self.decoder_factory = Some(factory);
303        self
304    }
305
306    pub fn encoder(mut self, factory: EncoderFactory) -> Self {
307        self.encoder_factory = Some(factory);
308        self
309    }
310
311    pub fn probe(mut self, probe: ProbeFn) -> Self {
312        self.probe = Some(probe);
313        self
314    }
315
316    /// Claim a single container tag for this codec. Equivalent to
317    /// `.tags([tag])` but avoids the array ceremony for single-tag
318    /// claims.
319    pub fn tag(mut self, tag: CodecTag) -> Self {
320        self.tags.push(tag);
321        self
322    }
323
324    /// Claim a set of container tags for this codec. Takes any
325    /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
326    /// codec with 3-6 tags reads as one clean block.
327    pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
328        self.tags.extend(tags);
329        self
330    }
331
332    /// Declare the options struct this codec's encoder factory expects.
333    /// Attaches `T::SCHEMA` so the registry can enumerate recognised
334    /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
335    /// The factory itself still has to call
336    /// [`crate::parse_options::<T>()`] against
337    /// `CodecParameters::options` at init time.
338    pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
339        self.encoder_options_schema = Some(T::SCHEMA);
340        self
341    }
342
343    /// Declare the options struct this codec's decoder factory expects.
344    /// See [`Self::encoder_options`] for the encoder counterpart.
345    pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
346        self.decoder_options_schema = Some(T::SCHEMA);
347        self
348    }
349
350    /// Tag this codec as belonging to a HW backend identified by
351    /// `engine_id`. Should match the `engine_id` of every other
352    /// `CodecInfo` registered by the same backend, and the corresponding
353    /// `engine_id` field used by the CLI for grouping. SW codecs leave
354    /// this unset.
355    pub fn with_engine_id(mut self, engine_id: &'static str) -> Self {
356        self.engine_id = Some(engine_id);
357        self
358    }
359
360    /// Attach a probe function. Consumers call it to enumerate the
361    /// engines (devices) this backend can dispatch to. Probes are
362    /// expected to be idempotent and side-effect free; consumers may
363    /// call them more than once per process and should dedupe by
364    /// [`Self::engine_id`].
365    pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self {
366        self.engine_probe = Some(probe);
367        self
368    }
369}
370
371/// Internal per-impl record held inside the registry's id map. Kept
372/// distinct from [`CodecInfo`] so the id map stays cheap to walk
373/// during `make_decoder` / `make_encoder` lookups.
374#[derive(Clone)]
375pub struct CodecImplementation {
376    pub caps: CodecCapabilities,
377    pub make_decoder: Option<DecoderFactory>,
378    pub make_encoder: Option<EncoderFactory>,
379    /// Encoder options schema declared via
380    /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
381    /// no tuning knobs (any non-empty `CodecParameters::options` will
382    /// still be rejected by the factory if the encoder calls
383    /// `parse_options` — this is purely informational for discovery).
384    pub encoder_options_schema: Option<&'static [OptionField]>,
385    pub decoder_options_schema: Option<&'static [OptionField]>,
386    /// HW backend identifier copied verbatim from the originating
387    /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW
388    /// backends; `None` on SW codecs. Consumers (CLI `info` command,
389    /// pipeline dispatcher, bench loop) read this to group entries by
390    /// backend without grepping `caps.implementation`.
391    pub engine_id: Option<&'static str>,
392    /// Engine probe function copied verbatim from the originating
393    /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a
394    /// probe wired; `None` on SW codecs. Consumers call it on demand
395    /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]).
396    pub engine_probe: Option<crate::engine::EngineProbeFn>,
397}
398
399#[derive(Default)]
400pub struct CodecRegistry {
401    /// id → list of implementations. Each registered codec appends one
402    /// entry here. `make_decoder` / `make_encoder` walk this list in
403    /// preference order.
404    impls: HashMap<CodecId, Vec<CodecImplementation>>,
405    /// Append-only list of every registration — the `tag_index` stores
406    /// offsets into this vector.
407    registrations: Vec<RegistrationRecord>,
408    /// Tag → indices into `registrations`. Indices are stored in
409    /// registration order so tie-breaking in `resolve_tag` is
410    /// deterministic (first-registered wins).
411    tag_index: HashMap<CodecTag, Vec<usize>>,
412}
413
414/// Internal registry record. Mirrors the subset of [`CodecInfo`]
415/// needed at resolve time.
416struct RegistrationRecord {
417    id: CodecId,
418    probe: Option<ProbeFn>,
419}
420
421impl CodecRegistry {
422    pub fn new() -> Self {
423        Self::default()
424    }
425
426    /// Register one codec. Expands into:
427    ///   * an entry in the id → implementations map (for
428    ///     `make_decoder` / `make_encoder`);
429    ///   * an entry in the tag index for every claimed tag (for
430    ///     `resolve_tag`).
431    ///
432    /// Calling `register` multiple times with the same id is allowed
433    /// and how multi-implementation codecs (software-plus-hardware
434    /// FLAC, for example) are expressed.
435    pub fn register(&mut self, info: CodecInfo) {
436        let CodecInfo {
437            id,
438            capabilities,
439            decoder_factory,
440            encoder_factory,
441            probe,
442            tags,
443            encoder_options_schema,
444            decoder_options_schema,
445            // engine_id / engine_probe are metadata attached to a
446            // CodecInfo for backends that want consumers (CLI `info`,
447            // pipeline bench) to enumerate the underlying devices on
448            // demand. They're surfaced verbatim on the resulting
449            // CodecImplementation so consumers can read them without
450            // grepping `caps.implementation`. Tag-only CodecInfo entries
451            // (no factories) drop the values on the floor — there's no
452            // CodecImplementation built in that branch.
453            engine_id,
454            engine_probe,
455        } = info;
456
457        let caps = {
458            let mut c = capabilities;
459            if decoder_factory.is_some() {
460                c = c.with_decode();
461            }
462            if encoder_factory.is_some() {
463                c = c.with_encode();
464            }
465            c
466        };
467
468        // Only record an implementation entry when at least one factory
469        // is present. A "tag-only" CodecInfo — used to attach extra tag
470        // claims to a codec that was already registered with factories —
471        // shouldn't pollute the impl list.
472        if decoder_factory.is_some() || encoder_factory.is_some() {
473            self.impls
474                .entry(id.clone())
475                .or_default()
476                .push(CodecImplementation {
477                    caps,
478                    make_decoder: decoder_factory,
479                    make_encoder: encoder_factory,
480                    encoder_options_schema,
481                    decoder_options_schema,
482                    engine_id,
483                    engine_probe,
484                });
485        }
486
487        let record_idx = self.registrations.len();
488        self.registrations.push(RegistrationRecord {
489            id: id.clone(),
490            probe,
491        });
492        for tag in tags {
493            self.tag_index.entry(tag).or_default().push(record_idx);
494        }
495    }
496
497    pub fn has_decoder(&self, id: &CodecId) -> bool {
498        self.impls
499            .get(id)
500            .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
501            .unwrap_or(false)
502    }
503
504    pub fn has_encoder(&self, id: &CodecId) -> bool {
505        self.impls
506            .get(id)
507            .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
508            .unwrap_or(false)
509    }
510
511    /// First registered decoder factory for `params.codec_id`, invoked
512    /// with `params`. No priority walk, no preference filter, no
513    /// init-time fallback to a lower-priority impl. Errors if no
514    /// decoder is registered for the codec.
515    ///
516    /// Intended for single-impl scenarios — typically a codec crate's
517    /// own self-tests, where exactly one impl has been registered into
518    /// a freshly-constructed registry. Production callers selecting
519    /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
520    /// should use `oxideav_pipeline::make_decoder_with` instead, which
521    /// applies `CodecPreferences` and walks priorities.
522    pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
523        let imp = self
524            .implementations(&params.codec_id)
525            .iter()
526            .find(|i| i.make_decoder.is_some())
527            .ok_or_else(|| {
528                Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
529            })?;
530        (imp.make_decoder.expect("checked above"))(params)
531    }
532
533    /// First registered encoder factory — see [`first_decoder`].
534    ///
535    /// [`first_decoder`]: Self::first_decoder
536    pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
537        let imp = self
538            .implementations(&params.codec_id)
539            .iter()
540            .find(|i| i.make_encoder.is_some())
541            .ok_or_else(|| {
542                Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
543            })?;
544        (imp.make_encoder.expect("checked above"))(params)
545    }
546
547    /// Look up a decoder by exact implementation name
548    /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
549    /// isn't registered or if it has no decoder factory.
550    pub fn decoder_by_impl(
551        &self,
552        impl_name: &str,
553        params: &CodecParameters,
554    ) -> Result<Box<dyn Decoder>> {
555        let imp = self
556            .implementations(&params.codec_id)
557            .iter()
558            .find(|i| i.caps.implementation == impl_name)
559            .ok_or_else(|| {
560                Error::CodecNotFound(format!(
561                    "no implementation `{impl_name}` for codec {}",
562                    params.codec_id
563                ))
564            })?;
565        let factory = imp
566            .make_decoder
567            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
568        factory(params)
569    }
570
571    /// Look up an encoder by exact implementation name — see
572    /// [`decoder_by_impl`].
573    ///
574    /// [`decoder_by_impl`]: Self::decoder_by_impl
575    pub fn encoder_by_impl(
576        &self,
577        impl_name: &str,
578        params: &CodecParameters,
579    ) -> Result<Box<dyn Encoder>> {
580        let imp = self
581            .implementations(&params.codec_id)
582            .iter()
583            .find(|i| i.caps.implementation == impl_name)
584            .ok_or_else(|| {
585                Error::CodecNotFound(format!(
586                    "no implementation `{impl_name}` for codec {}",
587                    params.codec_id
588                ))
589            })?;
590        let factory = imp
591            .make_encoder
592            .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
593        factory(params)
594    }
595
596    /// Iterate codec ids that have at least one decoder implementation.
597    pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
598        self.impls
599            .iter()
600            .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
601            .map(|(id, _)| id)
602    }
603
604    pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
605        self.impls
606            .iter()
607            .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
608            .map(|(id, _)| id)
609    }
610
611    /// All registered implementations of a given codec id.
612    pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
613        self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
614    }
615
616    /// Lookup the encoder options schema for a registered codec. Walks
617    /// implementations in registration order and returns the first
618    /// schema found. `None` means either the codec isn't registered or
619    /// no implementation declared an encoder schema.
620    pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
621        self.impls
622            .get(id)?
623            .iter()
624            .find_map(|i| i.encoder_options_schema)
625    }
626
627    /// Lookup the decoder options schema — see
628    /// [`encoder_options_schema`](Self::encoder_options_schema).
629    pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
630        self.impls
631            .get(id)?
632            .iter()
633            .find_map(|i| i.decoder_options_schema)
634    }
635
636    /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
637    /// to show capability flags per implementation.
638    pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
639        self.impls
640            .iter()
641            .flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
642    }
643
644    /// Iterator over every `(tag, codec_id)` pair currently registered —
645    /// used by `oxideav tags` debug output and by tests that want to
646    /// walk the tag surface.
647    pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
648        self.tag_index.iter().flat_map(move |(tag, idxs)| {
649            idxs.iter().map(move |&i| (tag, &self.registrations[i].id))
650        })
651    }
652
653    /// Inherent form of tag resolution that returns a reference.
654    /// The owned-value form used by container code lives behind the
655    /// [`CodecResolver`] trait impl below.
656    ///
657    /// Walks every registration that claimed `ctx.tag`, calls its
658    /// probe with `ctx`, and returns the id of the registration that
659    /// scored highest. Probes that return `0.0` are discarded; ties
660    /// on confidence are broken by registration order (first wins).
661    /// Registrations with no probe are treated as returning `1.0`.
662    pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
663        let idxs = self.tag_index.get(ctx.tag)?;
664        let mut best: Option<(f32, usize)> = None;
665        for &i in idxs {
666            let rec = &self.registrations[i];
667            let conf = match rec.probe {
668                Some(f) => f(ctx),
669                None => 1.0,
670            };
671            if conf <= 0.0 {
672                continue;
673            }
674            best = match best {
675                None => Some((conf, i)),
676                Some((bc, _)) if conf > bc => Some((conf, i)),
677                other => other,
678            };
679        }
680        best.map(|(_, i)| &self.registrations[i].id)
681    }
682}
683
684/// Implement the shared [`CodecResolver`] interface so container
685/// demuxers can accept `&dyn CodecResolver` without depending on
686/// this crate directly — the trait lives in oxideav-core.
687impl CodecResolver for CodecRegistry {
688    fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
689        self.resolve_tag_ref(ctx).cloned()
690    }
691}
692
693#[cfg(test)]
694mod tag_tests {
695    use super::*;
696    use crate::CodecCapabilities;
697
698    /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
699    /// 0x000001 start code in the first few bytes).
700    fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
701        match ctx.packet {
702            Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
703            Some(_) => 0.0,
704            None => 0.5, // no data yet — weak evidence
705        }
706    }
707
708    /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
709    /// (starts with a 0x000001 start code in the first few bytes).
710    fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
711        match ctx.packet {
712            Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
713            Some(_) => 0.0,
714            None => 0.5,
715        }
716    }
717
718    fn info(id: &str) -> CodecInfo {
719        CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
720    }
721
722    #[test]
723    fn resolve_single_claim_no_probe() {
724        let mut reg = CodecRegistry::new();
725        reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
726        let t = CodecTag::fourcc(b"FLAC");
727        assert_eq!(
728            reg.resolve_tag_ref(&ProbeContext::new(&t))
729                .map(|c| c.as_str()),
730            Some("flac"),
731        );
732    }
733
734    #[test]
735    fn resolve_missing_tag_returns_none() {
736        let reg = CodecRegistry::new();
737        let t = CodecTag::fourcc(b"????");
738        assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
739    }
740
741    #[test]
742    fn unprobed_claims_tie_first_registered_wins() {
743        // Two unprobed claims on the same tag: deterministic order.
744        let mut reg = CodecRegistry::new();
745        reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
746        reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
747        let t = CodecTag::fourcc(b"TEST");
748        assert_eq!(
749            reg.resolve_tag_ref(&ProbeContext::new(&t))
750                .map(|c| c.as_str()),
751            Some("first"),
752        );
753    }
754
755    #[test]
756    fn probe_picks_matching_bitstream() {
757        // The core bug fix: every probe is asked and the highest
758        // confidence wins regardless of registration order.
759        let mut reg = CodecRegistry::new();
760        reg.register(
761            info("msmpeg4v3")
762                .probe(probe_msmpeg4)
763                .tag(CodecTag::fourcc(b"DIV3")),
764        );
765        reg.register(
766            info("mpeg4video")
767                .probe(probe_mpeg4_part2)
768                .tag(CodecTag::fourcc(b"DIV3")),
769        );
770
771        let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
772        let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
773        let tag = CodecTag::fourcc(b"DIV3");
774
775        let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
776        assert_eq!(
777            reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
778            Some("mpeg4video"),
779        );
780        let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
781        assert_eq!(
782            reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
783            Some("msmpeg4v3"),
784        );
785    }
786
787    #[test]
788    fn unprobed_claim_wins_against_low_confidence_probe() {
789        // One codec claims a tag without a probe (→ confidence 1.0)
790        // and another claims it with a probe returning 0.3. The
791        // unprobed one wins — a codec that knows it owns the tag
792        // outright should not lose to a speculative probe.
793        let mut reg = CodecRegistry::new();
794        reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
795        reg.register(
796            info("speculative")
797                .probe(|_| 0.3)
798                .tag(CodecTag::fourcc(b"OWN_")),
799        );
800        let t = CodecTag::fourcc(b"OWN_");
801        assert_eq!(
802            reg.resolve_tag_ref(&ProbeContext::new(&t))
803                .map(|c| c.as_str()),
804            Some("owner"),
805        );
806    }
807
808    #[test]
809    fn probe_returning_zero_is_skipped() {
810        let mut reg = CodecRegistry::new();
811        reg.register(
812            info("refuses")
813                .probe(|_| 0.0)
814                .tag(CodecTag::fourcc(b"MAYB")),
815        );
816        reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
817        let t = CodecTag::fourcc(b"MAYB");
818        let ctx = ProbeContext::new(&t).packet(b"hello");
819        assert_eq!(
820            reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
821            Some("fallback"),
822        );
823    }
824
825    #[test]
826    fn fourcc_case_insensitive_lookup() {
827        let mut reg = CodecRegistry::new();
828        reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
829        // Registered as "DIV3" (uppercase via ctor); lookup using
830        // lowercase / mixed case also hits.
831        let upper = CodecTag::fourcc(b"DIV3");
832        let lower = CodecTag::fourcc(b"div3");
833        let mixed = CodecTag::fourcc(b"DiV3");
834        assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
835        assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
836        assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
837    }
838
839    #[test]
840    fn wave_format_and_matroska_tags_work() {
841        let mut reg = CodecRegistry::new();
842        reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
843        reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
844        let wf = CodecTag::wave_format(0x0055);
845        let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
846        assert_eq!(
847            reg.resolve_tag_ref(&ProbeContext::new(&wf))
848                .map(|c| c.as_str()),
849            Some("mp3"),
850        );
851        assert_eq!(
852            reg.resolve_tag_ref(&ProbeContext::new(&mk))
853                .map(|c| c.as_str()),
854            Some("h264"),
855        );
856    }
857
858    #[test]
859    fn mp4_object_type_tag_works() {
860        let mut reg = CodecRegistry::new();
861        reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
862        let t = CodecTag::mp4_object_type(0x40);
863        assert_eq!(
864            reg.resolve_tag_ref(&ProbeContext::new(&t))
865                .map(|c| c.as_str()),
866            Some("aac"),
867        );
868    }
869
870    #[test]
871    fn multi_tag_claim_all_resolve() {
872        let mut reg = CodecRegistry::new();
873        reg.register(info("aac").tags([
874            CodecTag::fourcc(b"MP4A"),
875            CodecTag::wave_format(0x00FF),
876            CodecTag::mp4_object_type(0x40),
877            CodecTag::matroska("A_AAC"),
878        ]));
879        for t in [
880            CodecTag::fourcc(b"MP4A"),
881            CodecTag::wave_format(0x00FF),
882            CodecTag::mp4_object_type(0x40),
883            CodecTag::matroska("A_AAC"),
884        ] {
885            assert_eq!(
886                reg.resolve_tag_ref(&ProbeContext::new(&t))
887                    .map(|c| c.as_str()),
888                Some("aac"),
889                "tag {t:?} did not resolve",
890            );
891        }
892    }
893}
894
895#[cfg(test)]
896mod engine_tests {
897    use super::*;
898    use crate::engine::HwDeviceInfo;
899
900    #[test]
901    fn codec_info_engine_id_and_probe_default_to_none() {
902        let ci = CodecInfo::new(CodecId::new("h264"));
903        assert!(ci.engine_id.is_none());
904        assert!(ci.engine_probe.is_none());
905    }
906
907    #[test]
908    fn codec_info_engine_builder_methods_set_fields() {
909        fn dummy_probe() -> Vec<HwDeviceInfo> {
910            vec![]
911        }
912        let ci = CodecInfo::new(CodecId::new("h264"))
913            .with_engine_id("nvidia")
914            .with_engine_probe(dummy_probe);
915        assert_eq!(ci.engine_id, Some("nvidia"));
916        assert!(ci.engine_probe.is_some());
917        let probe = ci.engine_probe.unwrap();
918        let result = probe();
919        assert!(result.is_empty());
920    }
921
922    #[test]
923    fn registering_codec_with_engine_metadata_does_not_panic() {
924        // The new fields are passthrough metadata — register() should
925        // accept them without affecting existing id/tag bookkeeping.
926        fn dummy_probe() -> Vec<HwDeviceInfo> {
927            vec![]
928        }
929        let mut reg = CodecRegistry::new();
930        reg.register(
931            CodecInfo::new(CodecId::new("h264"))
932                .capabilities(CodecCapabilities::audio("h264_nvdec"))
933                .tag(CodecTag::fourcc(b"H264"))
934                .with_engine_id("nvidia")
935                .with_engine_probe(dummy_probe),
936        );
937        let t = CodecTag::fourcc(b"H264");
938        assert_eq!(
939            reg.resolve_tag_ref(&ProbeContext::new(&t))
940                .map(|c| c.as_str()),
941            Some("h264"),
942        );
943    }
944
945    /// No-op decoder factory so the registration produces a real
946    /// CodecImplementation (the registry skips tag-only entries —
947    /// without a factory there'd be nothing in `implementations()`
948    /// to assert against).
949    fn dummy_decoder_factory(
950        _params: &crate::CodecParameters,
951    ) -> crate::Result<Box<dyn super::Decoder>> {
952        Err(crate::Error::unsupported("dummy decoder"))
953    }
954
955    #[test]
956    fn engine_metadata_propagates_through_register() {
957        fn dummy_probe() -> Vec<HwDeviceInfo> {
958            vec![]
959        }
960        let mut reg = CodecRegistry::default();
961        reg.register(
962            CodecInfo::new(CodecId::new("h264"))
963                .capabilities(CodecCapabilities::video("h264_test"))
964                .decoder(dummy_decoder_factory)
965                .with_engine_id("test-backend")
966                .with_engine_probe(dummy_probe),
967        );
968        let impls = reg.implementations(&CodecId::new("h264"));
969        assert_eq!(impls.len(), 1);
970        assert_eq!(impls[0].engine_id, Some("test-backend"));
971        assert!(impls[0].engine_probe.is_some());
972    }
973
974    #[test]
975    fn engine_metadata_absent_for_sw_codecs() {
976        // SW codecs don't call the engine builders — both fields
977        // should land as None on the resulting CodecImplementation.
978        let mut reg = CodecRegistry::default();
979        reg.register(
980            CodecInfo::new(CodecId::new("flac"))
981                .capabilities(CodecCapabilities::audio("flac_sw"))
982                .decoder(dummy_decoder_factory),
983        );
984        let impls = reg.implementations(&CodecId::new("flac"));
985        assert_eq!(impls.len(), 1);
986        assert!(impls[0].engine_id.is_none());
987        assert!(impls[0].engine_probe.is_none());
988    }
989}