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(¶ms.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(¶ms.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(¶ms.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(¶ms.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}