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. Ties go to the lower
17//! [`CodecInfo::resolution_priority`], then to the earlier
18//! registration; `resolve_tag_candidates` exposes the ranked list.
19//! - **payload-magic-keyed** — `resolve_payload_magic(first_bytes)`
20//! prefix-matches the claimed payload magic prefixes against a
21//! stream's leading bytes; longest matching magic wins, then the
22//! lower resolution priority, then registration order
23//! (`resolve_payload_magic_candidates` exposes the ranked list). For
24//! containers that identify a codec by the payload itself rather
25//! than a tag (e.g. an Ogg logical stream's first packet, or raw
26//! elementary streams).
27//! - **diagnostic** — `all_implementations`, `all_tag_registrations`,
28//! `all_payload_magic_registrations`.
29//!
30//! The tag path explicitly DOES NOT short-circuit on "first claim with
31//! no probe" — every claimant is asked, so a lower-priority probed
32//! claim can out-rank a higher-priority unprobed one when the content
33//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.).
34
35use std::collections::HashMap;
36
37use crate::arena;
38use crate::{
39 CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecResolver, CodecTag,
40 Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn,
41 Result,
42};
43
44// ───────────────────────── codec traits ─────────────────────────
45
46/// A packet-to-frame decoder.
47pub trait Decoder: Send {
48 /// Identifier of the codec this decoder handles.
49 fn codec_id(&self) -> &CodecId;
50
51 /// Feed one compressed packet. May or may not produce a frame immediately —
52 /// call `receive_frame` in a loop afterwards.
53 fn send_packet(&mut self, packet: &Packet) -> Result<()>;
54
55 /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the
56 /// decoder needs another packet.
57 fn receive_frame(&mut self) -> Result<Frame>;
58
59 /// Pull the next decoded frame as an arena-backed [`arena::sync::Frame`].
60 ///
61 /// Decoders that build their output through an
62 /// [`arena::sync::ArenaPool`] override this to return the pooled
63 /// [`arena::sync::Frame`] **directly**, with no per-plane memcpy
64 /// out — the caller gets true zero-copy plane access via
65 /// [`arena::sync::FrameInner::plane`].
66 ///
67 /// The default implementation delegates to [`Self::receive_frame`]
68 /// and copies the video planes into a freshly-leased one-shot
69 /// `arena::sync::ArenaPool`. This makes the method an additive
70 /// change for every existing [`Decoder`] impl: callers using the
71 /// new API still work, but pay one memcpy per plane.
72 ///
73 /// **Audio / subtitle frames:** the [`arena::sync::Frame`] body is
74 /// video-only (planes + [`arena::sync::FrameHeader`] with
75 /// width/height/pixel format). The default implementation returns
76 /// [`Error::Unsupported`] for non-video frames; an audio decoder
77 /// that wants to expose `receive_arena_frame()` must override it
78 /// with its own arena-backed audio-frame type once the framework
79 /// gains one. Until then, audio decoders should keep using
80 /// [`Self::receive_frame`].
81 fn receive_arena_frame(&mut self) -> Result<arena::sync::Frame> {
82 let frame = self.receive_frame()?;
83 match frame {
84 Frame::Video(v) => video_frame_to_arena_sync_frame(&v),
85 Frame::Audio(_) => Err(Error::unsupported(
86 "receive_arena_frame: audio frames not yet supported by default impl",
87 )),
88 Frame::Subtitle(_) => Err(Error::unsupported(
89 "receive_arena_frame: subtitle frames have no arena-backed representation",
90 )),
91 Frame::Vector(_) => Err(Error::unsupported(
92 "receive_arena_frame: vector frames have no arena-backed representation",
93 )),
94 }
95 }
96
97 /// Signal end-of-stream. After this, `receive_frame` will drain buffered
98 /// frames and eventually return `Error::Eof`.
99 fn flush(&mut self) -> Result<()>;
100
101 /// Discard all carry-over state so the decoder can resume from a new
102 /// bitstream position without producing stale output. Called by the
103 /// player after a container seek.
104 ///
105 /// Unlike [`flush`](Self::flush) (which signals end-of-stream and
106 /// drains buffered frames), `reset` is expected to:
107 /// * drop every buffered input packet and pending output frame;
108 /// * zero any per-stream filter / predictor / overlap memory so the
109 /// next `send_packet` decodes as if it were the first;
110 /// * leave the codec id and stream parameters untouched.
111 ///
112 /// The default is a conservative "drain-then-forget": call
113 /// [`flush`](Self::flush) and ignore any remaining frames. Stateful
114 /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap,
115 /// reference pictures, …) should override this to wipe their
116 /// internal state explicitly — otherwise the first ~N output
117 /// samples after a seek will be glitchy until the state re-adapts.
118 fn reset(&mut self) -> Result<()> {
119 self.flush()?;
120 // Drain any remaining output frames so the next send_packet
121 // starts clean. NeedMore / Eof both mean "no more frames"; any
122 // other error is surfaced so the caller can see why.
123 loop {
124 match self.receive_frame() {
125 Ok(_) => {}
126 Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()),
127 Err(e) => return Err(e),
128 }
129 }
130 }
131
132 /// Advisory: announce the runtime environment (today: a thread budget
133 /// for codec-internal parallelism). Called at most once, before the
134 /// first `send_packet`. Default no-op; codecs that want to run
135 /// slice-/GOP-/tile-parallel override this to capture the budget.
136 /// Ignoring the hint is always safe — callers must still work with
137 /// a decoder that runs serial.
138 fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
139
140 /// Pixel layout of the video frames this decoder emits, stated
141 /// before the first frame — from the codec's configuration record
142 /// in `extradata`, from a negotiated hardware session, or from the
143 /// decoder's fixed output policy. `Some` is a **fact about the
144 /// frames to come** and takes precedence over the container's
145 /// [`CodecParameters::pixel_format`](crate::CodecParameters::pixel_format)
146 /// (a stream label is a claim the container made; the decoder is
147 /// the one producing the planes). `None` — the default — means
148 /// "not known yet: trust the stream parameters", and a decoder
149 /// whose output depends on the first packet (an untagged file that
150 /// decides its layout per picture) answers `None` until it has
151 /// seen one and the real layout afterwards.
152 ///
153 /// Pipelines and gateways read it to label frames from codec-only
154 /// registrations and to pick the conversion into an encoder's
155 /// [`accepted_pixel_formats`](crate::CodecCapabilities::accepted_pixel_formats);
156 /// a frame whose plane shape disagrees with the stated layout is
157 /// the decoder's defect, not the consumer's. The answer may change
158 /// between frames only for a decoder that documents it.
159 fn output_pixel_format(&self) -> Option<crate::PixelFormat> {
160 None
161 }
162}
163
164/// A frame-to-packet encoder.
165pub trait Encoder: Send {
166 /// Identifier of the codec this encoder produces.
167 fn codec_id(&self) -> &CodecId;
168
169 /// Parameters describing this encoder's output stream (to feed into a muxer).
170 fn output_params(&self) -> &CodecParameters;
171
172 /// Feed one uncompressed frame. May or may not produce a packet
173 /// immediately — call `receive_packet` in a loop afterwards.
174 fn send_frame(&mut self, frame: &Frame) -> Result<()>;
175
176 /// Pull the next encoded packet, if any. Returns `Error::NeedMore`
177 /// when the encoder needs another frame (or a `flush`).
178 fn receive_packet(&mut self) -> Result<Packet>;
179
180 /// Signal end of input: drain internal lookahead so the remaining
181 /// packets become available via `receive_packet`.
182 fn flush(&mut self) -> Result<()>;
183
184 /// Advisory: announce the runtime environment. Same semantics as
185 /// [`Decoder::set_execution_context`].
186 fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
187}
188
189/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a
190/// heap-backed [`crate::VideoFrame`] into a freshly-leased
191/// [`arena::sync::Frame`].
192///
193/// Allocates a single-slot, single-arena `arena::sync::ArenaPool`
194/// sized to fit the planes verbatim. The pool is dropped at the end of
195/// this call; the returned `Frame` keeps its leased buffer alive via
196/// `Arc<FrameInner>` (the `Arena`'s `Weak` handle to the dropped pool
197/// just stops upgrading — the buffer drops normally when the last
198/// `Frame` clone goes away).
199///
200/// Width / height / pixel-format on the returned `FrameHeader` are
201/// derived from the plane shape: `width = plane[0].stride`,
202/// `height = plane[0].data.len() / stride`. Pixel format is left as
203/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first
204/// per-plane sensible default — this is a best-effort label for the
205/// generic conversion path; decoders that override
206/// `receive_arena_frame` themselves should set the correct pixel
207/// format.
208fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result<arena::sync::Frame> {
209 if v.planes.is_empty() {
210 return Err(Error::invalid(
211 "receive_arena_frame: video frame has no planes",
212 ));
213 }
214 let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum();
215 if total_bytes == 0 {
216 return Err(Error::invalid(
217 "receive_arena_frame: video frame planes are empty",
218 ));
219 }
220 // One-shot pool sized exactly to the frame. The pool drops at end
221 // of scope; the leased Arena lives on inside the returned Frame
222 // (its Weak<ArenaPool> handle just won't upgrade in Drop, so the
223 // Box<[u8]> falls through to a normal heap free).
224 let pool = arena::sync::ArenaPool::with_alloc_count_cap(
225 1,
226 total_bytes,
227 // One alloc per plane, plus a generous safety margin.
228 (v.planes.len() as u32).saturating_add(4),
229 );
230 let arena = pool.lease()?;
231 let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len());
232 let mut cursor = 0usize;
233 for plane in &v.planes {
234 let dst = arena.alloc::<u8>(plane.data.len())?;
235 dst.copy_from_slice(&plane.data);
236 plane_offsets.push((cursor, plane.data.len()));
237 cursor += plane.data.len();
238 }
239 // Best-effort header: width = stride of plane 0, height inferred
240 // from plane 0's data length. Pixel format defaults to Yuv420P for
241 // the common 3-plane case, Gray8 for single-plane, otherwise
242 // Yuv444P. Decoders that care about exact pixel-format / width /
243 // height should override `receive_arena_frame` themselves so they
244 // can emit a correct `FrameHeader` straight from their arena
245 // build path.
246 let stride0 = v.planes[0].stride.max(1);
247 let width = stride0 as u32;
248 let height = (v.planes[0].data.len() / stride0) as u32;
249 // Count only image planes for the format guess — side-channel
250 // entries (palette, significant bits; copied verbatim above like
251 // any other plane) must not bump e.g. a single-plane palette frame
252 // out of the Gray8 label.
253 let pixel_format = match v.image_plane_count() {
254 1 => PixelFormat::Gray8,
255 3 => PixelFormat::Yuv420P,
256 _ => PixelFormat::Yuv444P,
257 };
258 let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts);
259 arena::sync::FrameInner::new(arena, &plane_offsets, header)
260}
261
262/// Factory that builds a decoder for a given codec parameter set.
263pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
264
265/// Factory that builds an encoder for a given codec parameter set.
266pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
267
268// ───────────────────────── CodecInfo ─────────────────────────
269
270/// A single registration: capabilities, decoder/encoder factories,
271/// optional probe, and the container tags this codec claims.
272///
273/// Codec crates build one of these per codec id inside their
274/// `register(reg)` function and hand it to
275/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
276/// additional fields can be added without breaking existing codec
277/// crates — construction is only possible through
278/// [`CodecInfo::new`] plus the builder methods below.
279#[non_exhaustive]
280pub struct CodecInfo {
281 /// Canonical codec identifier this entry registers.
282 pub id: CodecId,
283 /// Capability description (media kind, feature flags, priority).
284 pub capabilities: CodecCapabilities,
285 /// Factory producing a fresh decoder instance, if decode is supported.
286 pub decoder_factory: Option<DecoderFactory>,
287 /// Factory producing a fresh encoder instance, if encode is supported.
288 pub encoder_factory: Option<EncoderFactory>,
289 /// Probe function that returns a confidence in `0.0..=1.0` for a
290 /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
291 /// claimed tag" — the correct default for codecs whose tag claims
292 /// are unambiguous.
293 pub probe: Option<ProbeFn>,
294 /// Tags this codec is willing to be looked up under. One codec may
295 /// claim many tags (an AAC decoder covers several WaveFormat ids,
296 /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
297 pub tags: Vec<CodecTag>,
298 /// Payload magic prefixes this codec answers to (`\x01vorbis`,
299 /// `OpusHead`, …). Some carriage formats have no codec tag — the
300 /// codec is announced by a magic byte prefix on the payload itself
301 /// (an Ogg logical stream's first packet is the canonical case;
302 /// raw elementary streams are another). Such claims are
303 /// prefix-matched by
304 /// [`CodecRegistry::resolve_payload_magic_ref`] instead of living in
305 /// the exact-match [`CodecTag`] index. Attached with
306 /// [`Self::payload_magic`] / [`Self::payload_magics`]. Empty prefixes are
307 /// ignored at registration time (a zero-length prefix would match
308 /// every stream while carrying no evidence).
309 pub payload_magics: Vec<Vec<u8>>,
310 /// Schema of the encoder's recognised option keys
311 /// (`CodecParameters::options`). Attached with
312 /// [`Self::encoder_options`]. Used for validation / `oxideav list`
313 /// / pipeline JSON checks.
314 pub encoder_options_schema: Option<&'static [OptionField]>,
315 /// Schema of the decoder's recognised option keys.
316 pub decoder_options_schema: Option<&'static [OptionField]>,
317 /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`,
318 /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every
319 /// `CodecInfo` they register; SW codecs leave this `None`.
320 /// Consumers (e.g. the CLI's `info` command) use it to group
321 /// codec entries by backend and to dedupe probe calls — multiple
322 /// `CodecInfo` entries with the same `engine_id` typically share
323 /// an `engine_probe` function, and consumers should call the probe
324 /// at most once per `engine_id` per pass. Attached via
325 /// [`Self::with_engine_id`].
326 pub engine_id: Option<&'static str>,
327 /// Optional engine probe function. When `Some`, calling it returns
328 /// one [`crate::engine::HwDeviceInfo`] entry per device the backend
329 /// sees. Phase-2 HW siblings populate this on every `CodecInfo`
330 /// they register; Phase-3 consumers (CLI) call it on demand.
331 /// Attached via [`Self::with_engine_probe`].
332 pub engine_probe: Option<crate::engine::EngineProbeFn>,
333 /// Tie-break rank for this registration's **claims** (tags and
334 /// payload magics) when another codec id claims the same tag /
335 /// magic with equal evidence. Lower is preferred; defaults to
336 /// [`DEFAULT_PRIORITY`](crate::DEFAULT_PRIORITY). Deliberately
337 /// separate from [`CodecCapabilities::priority`]: that field ranks
338 /// *implementations of one codec id* against each other (HW before
339 /// SW), and a backend's implementation preference must not silently
340 /// out-rank a different codec's identity claim on an ambiguous tag.
341 /// Attached via [`Self::with_resolution_priority`].
342 pub resolution_priority: i32,
343}
344
345impl CodecInfo {
346 /// Start a new registration for `id` with empty capabilities, no
347 /// factories, no probe, and no tags. Chain the builder methods
348 /// below to fill it in, then hand the result to
349 /// [`CodecRegistry::register`].
350 pub fn new(id: CodecId) -> Self {
351 Self {
352 capabilities: CodecCapabilities::audio(id.as_str()),
353 id,
354 decoder_factory: None,
355 encoder_factory: None,
356 probe: None,
357 tags: Vec::new(),
358 payload_magics: Vec::new(),
359 encoder_options_schema: None,
360 decoder_options_schema: None,
361 engine_id: None,
362 engine_probe: None,
363 resolution_priority: crate::DEFAULT_PRIORITY,
364 }
365 }
366
367 /// Set the claim tie-break priority (see
368 /// [`Self::resolution_priority`]). Lower is preferred. Only consulted
369 /// when another codec id claims the same tag with equal probe
370 /// confidence, or the same-length payload magic; evidence always
371 /// out-ranks priority, and equal priorities fall through to
372 /// registration order (earlier wins).
373 pub fn with_resolution_priority(mut self, priority: i32) -> Self {
374 self.resolution_priority = priority;
375 self
376 }
377
378 /// Replace the capability description. The default built by
379 /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
380 /// real registration should call this.
381 pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
382 self.capabilities = caps;
383 self
384 }
385
386 /// Builder: attach the decoder factory.
387 pub fn decoder(mut self, factory: DecoderFactory) -> Self {
388 self.decoder_factory = Some(factory);
389 self
390 }
391
392 /// Builder: attach the encoder factory.
393 pub fn encoder(mut self, factory: EncoderFactory) -> Self {
394 self.encoder_factory = Some(factory);
395 self
396 }
397
398 /// Builder: attach a confidence probe (see [`CodecInfo::probe`]).
399 pub fn probe(mut self, probe: ProbeFn) -> Self {
400 self.probe = Some(probe);
401 self
402 }
403
404 /// Claim a single container tag for this codec. Equivalent to
405 /// `.tags([tag])` but avoids the array ceremony for single-tag
406 /// claims.
407 pub fn tag(mut self, tag: CodecTag) -> Self {
408 self.tags.push(tag);
409 self
410 }
411
412 /// Claim a set of container tags for this codec. Takes any
413 /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
414 /// codec with 3-6 tags reads as one clean block.
415 pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
416 self.tags.extend(tags);
417 self
418 }
419
420 /// Claim one payload magic prefix for this codec (see
421 /// [`Self::payload_magics`]). Chain repeatedly for codecs that answer
422 /// to more than one magic:
423 ///
424 /// ```
425 /// # use oxideav_core::registry::CodecInfo;
426 /// # use oxideav_core::CodecId;
427 /// let info = CodecInfo::new(CodecId::new("vorbis")).payload_magic(b"\x01vorbis");
428 /// # let _ = info;
429 /// ```
430 pub fn payload_magic(mut self, magic: impl Into<Vec<u8>>) -> Self {
431 self.payload_magics.push(magic.into());
432 self
433 }
434
435 /// Claim a set of payload magic prefixes for this codec — the
436 /// iterable companion to [`Self::payload_magic`], mirroring the
437 /// [`Self::tag`] / [`Self::tags`] pair.
438 pub fn payload_magics<I>(mut self, magics: I) -> Self
439 where
440 I: IntoIterator,
441 I::Item: Into<Vec<u8>>,
442 {
443 self.payload_magics
444 .extend(magics.into_iter().map(Into::into));
445 self
446 }
447
448 /// Declare the options struct this codec's encoder factory expects.
449 /// Attaches `T::SCHEMA` so the registry can enumerate recognised
450 /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
451 /// The factory itself still has to call
452 /// [`crate::parse_options::<T>()`] against
453 /// `CodecParameters::options` at init time.
454 pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
455 self.encoder_options_schema = Some(T::SCHEMA);
456 self
457 }
458
459 /// Declare the options struct this codec's decoder factory expects.
460 /// See [`Self::encoder_options`] for the encoder counterpart.
461 pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
462 self.decoder_options_schema = Some(T::SCHEMA);
463 self
464 }
465
466 /// Tag this codec as belonging to a HW backend identified by
467 /// `engine_id`. Should match the `engine_id` of every other
468 /// `CodecInfo` registered by the same backend, and the corresponding
469 /// `engine_id` field used by the CLI for grouping. SW codecs leave
470 /// this unset.
471 pub fn with_engine_id(mut self, engine_id: &'static str) -> Self {
472 self.engine_id = Some(engine_id);
473 self
474 }
475
476 /// Attach a probe function. Consumers call it to enumerate the
477 /// engines (devices) this backend can dispatch to. Probes are
478 /// expected to be idempotent and side-effect free; consumers may
479 /// call them more than once per process and should dedupe by
480 /// [`Self::engine_id`].
481 pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self {
482 self.engine_probe = Some(probe);
483 self
484 }
485}
486
487/// Internal per-impl record held inside the registry's id map. Kept
488/// distinct from [`CodecInfo`] so the id map stays cheap to walk
489/// during `make_decoder` / `make_encoder` lookups.
490#[derive(Clone)]
491pub struct CodecImplementation {
492 /// Capability description copied from the originating [`CodecInfo`].
493 pub caps: CodecCapabilities,
494 /// Decoder factory, if this implementation can decode.
495 pub make_decoder: Option<DecoderFactory>,
496 /// Encoder factory, if this implementation can encode.
497 pub make_encoder: Option<EncoderFactory>,
498 /// Encoder options schema declared via
499 /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
500 /// no tuning knobs (any non-empty `CodecParameters::options` will
501 /// still be rejected by the factory if the encoder calls
502 /// `parse_options` — this is purely informational for discovery).
503 pub encoder_options_schema: Option<&'static [OptionField]>,
504 /// Decoder options schema declared via
505 /// [`CodecInfo::decoder_options`]; same semantics as the encoder
506 /// schema above.
507 pub decoder_options_schema: Option<&'static [OptionField]>,
508 /// HW backend identifier copied verbatim from the originating
509 /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW
510 /// backends; `None` on SW codecs. Consumers (CLI `info` command,
511 /// pipeline dispatcher, bench loop) read this to group entries by
512 /// backend without grepping `caps.implementation`.
513 pub engine_id: Option<&'static str>,
514 /// Engine probe function copied verbatim from the originating
515 /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a
516 /// probe wired; `None` on SW codecs. Consumers call it on demand
517 /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]).
518 pub engine_probe: Option<crate::engine::EngineProbeFn>,
519}
520
521/// Registry mapping codec ids and container tags to their registered
522/// implementations; the lookup point behind `make_decoder` /
523/// `make_encoder` / `resolve_tag`.
524#[derive(Default)]
525pub struct CodecRegistry {
526 /// id → list of implementations. Each registered codec appends one
527 /// entry here. `make_decoder` / `make_encoder` walk this list in
528 /// preference order.
529 impls: HashMap<CodecId, Vec<CodecImplementation>>,
530 /// Codec ids in the order their first implementation was
531 /// registered — drives every id-level enumeration so listings are
532 /// stable across processes.
533 impl_order: Vec<CodecId>,
534 /// Append-only list of every registration — the `tag_index` stores
535 /// offsets into this vector.
536 registrations: Vec<RegistrationRecord>,
537 /// Tag → indices into `registrations`. Indices are stored in
538 /// registration order so tie-breaking in `resolve_tag` is
539 /// deterministic (first-registered wins).
540 tag_index: HashMap<CodecTag, Vec<usize>>,
541 /// Payload magic-prefix claims: `(magic, registration index)` in
542 /// registration order. Kept as a flat list rather than a map
543 /// because resolution is prefix matching (see
544 /// [`Self::resolve_payload_magic_ref`]), not exact-key lookup.
545 magic_index: Vec<(Vec<u8>, usize)>,
546}
547
548/// Internal registry record. Mirrors the subset of [`CodecInfo`]
549/// needed at resolve time.
550struct RegistrationRecord {
551 id: CodecId,
552 probe: Option<ProbeFn>,
553 /// Claim tie-break rank copied from [`CodecInfo::resolution_priority`].
554 priority: i32,
555 /// Tags claimed by this registration, in declaration order — the
556 /// source of [`CodecRegistry::all_tag_registrations`]'s ordering.
557 tags: Vec<CodecTag>,
558}
559
560/// One ranked entry from [`CodecRegistry::resolve_tag_candidates`].
561///
562/// Ordered by the registry's resolution rule — `confidence`
563/// descending, then `priority` ascending (lower is preferred), then
564/// `order` ascending (earlier registration wins) — so the first
565/// element is exactly what [`CodecRegistry::resolve_tag_ref`] returns.
566/// `#[non_exhaustive]`: read by field, never constructed by callers.
567#[non_exhaustive]
568#[derive(Clone, Copy, Debug, PartialEq)]
569pub struct TagCandidate<'a> {
570 /// Codec id of the claiming registration.
571 pub id: &'a CodecId,
572 /// Probe confidence for this context (`1.0` for unprobed claims;
573 /// never `<= 0.0` — such claims are not candidates).
574 pub confidence: crate::Confidence,
575 /// Claim tie-break priority (see [`CodecInfo::resolution_priority`]).
576 pub priority: i32,
577 /// 0-based registration sequence number (one per
578 /// [`CodecRegistry::register`] call) — the final tie-break. The
579 /// same id can appear more than once when it registered the claim
580 /// twice; the registry does not fold duplicates.
581 pub order: usize,
582}
583
584/// One ranked entry from [`CodecRegistry::resolve_payload_magic_candidates`].
585///
586/// Ordered by the registry's resolution rule — magic length
587/// descending (most specific prefix), then `priority` ascending, then
588/// `order` ascending — so the first element is exactly what
589/// [`CodecRegistry::resolve_payload_magic_ref`] returns.
590#[non_exhaustive]
591#[derive(Clone, Copy, Debug, PartialEq, Eq)]
592pub struct PayloadMagicCandidate<'a> {
593 /// Codec id of the claiming registration.
594 pub id: &'a CodecId,
595 /// The magic prefix that matched.
596 pub magic: &'a [u8],
597 /// Claim tie-break priority (see [`CodecInfo::resolution_priority`]).
598 pub priority: i32,
599 /// 0-based registration sequence number — the final tie-break.
600 pub order: usize,
601}
602
603impl CodecRegistry {
604 /// An empty registry (same as `Default`).
605 pub fn new() -> Self {
606 Self::default()
607 }
608
609 /// Register one codec. Expands into:
610 /// * an entry in the id → implementations map (for
611 /// `make_decoder` / `make_encoder`);
612 /// * an entry in the tag index for every claimed tag (for
613 /// `resolve_tag`).
614 ///
615 /// Calling `register` multiple times with the same id is allowed
616 /// and how multi-implementation codecs (software-plus-hardware
617 /// FLAC, for example) are expressed.
618 pub fn register(&mut self, info: CodecInfo) {
619 let CodecInfo {
620 id,
621 capabilities,
622 decoder_factory,
623 encoder_factory,
624 probe,
625 tags,
626 payload_magics,
627 encoder_options_schema,
628 decoder_options_schema,
629 // engine_id / engine_probe are metadata attached to a
630 // CodecInfo for backends that want consumers (CLI `info`,
631 // pipeline bench) to enumerate the underlying devices on
632 // demand. They're surfaced verbatim on the resulting
633 // CodecImplementation so consumers can read them without
634 // grepping `caps.implementation`. Tag-only CodecInfo entries
635 // (no factories) drop the values on the floor — there's no
636 // CodecImplementation built in that branch.
637 engine_id,
638 engine_probe,
639 resolution_priority,
640 } = info;
641
642 let caps = {
643 let mut c = capabilities;
644 if decoder_factory.is_some() {
645 c = c.with_decode();
646 }
647 if encoder_factory.is_some() {
648 c = c.with_encode();
649 }
650 c
651 };
652
653 // Only record an implementation entry when at least one factory
654 // is present. A "tag-only" CodecInfo — used to attach extra tag
655 // claims to a codec that was already registered with factories —
656 // shouldn't pollute the impl list.
657 if decoder_factory.is_some() || encoder_factory.is_some() {
658 let slot = self.impls.entry(id.clone()).or_default();
659 if slot.is_empty() {
660 self.impl_order.push(id.clone());
661 }
662 slot.push(CodecImplementation {
663 caps,
664 make_decoder: decoder_factory,
665 make_encoder: encoder_factory,
666 encoder_options_schema,
667 decoder_options_schema,
668 engine_id,
669 engine_probe,
670 });
671 }
672
673 let record_idx = self.registrations.len();
674 for tag in &tags {
675 self.tag_index
676 .entry(tag.clone())
677 .or_default()
678 .push(record_idx);
679 }
680 self.registrations.push(RegistrationRecord {
681 id: id.clone(),
682 probe,
683 priority: resolution_priority,
684 tags,
685 });
686 for magic in payload_magics {
687 // A zero-length prefix would match every stream while
688 // carrying no evidence — drop it here so resolution never
689 // has to special-case it.
690 if !magic.is_empty() {
691 self.magic_index.push((magic, record_idx));
692 }
693 }
694 }
695
696 /// Whether at least one registered implementation of `id` can decode.
697 pub fn has_decoder(&self, id: &CodecId) -> bool {
698 self.impls
699 .get(id)
700 .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
701 .unwrap_or(false)
702 }
703
704 /// Whether at least one registered implementation of `id` can encode.
705 pub fn has_encoder(&self, id: &CodecId) -> bool {
706 self.impls
707 .get(id)
708 .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
709 .unwrap_or(false)
710 }
711
712 /// First registered decoder factory for `params.codec_id`, invoked
713 /// with `params`. No priority walk, no preference filter, no
714 /// init-time fallback to a lower-priority impl. Errors if no
715 /// decoder is registered for the codec.
716 ///
717 /// Intended for single-impl scenarios — typically a codec crate's
718 /// own self-tests, where exactly one impl has been registered into
719 /// a freshly-constructed registry. Production callers selecting
720 /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
721 /// should use `oxideav_pipeline::make_decoder_with` instead, which
722 /// applies `CodecPreferences` and walks priorities.
723 pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
724 let imp = self
725 .implementations(¶ms.codec_id)
726 .iter()
727 .find(|i| i.make_decoder.is_some())
728 .ok_or_else(|| {
729 Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
730 })?;
731 (imp.make_decoder.expect("checked above"))(params)
732 }
733
734 /// First registered encoder factory — see [`first_decoder`].
735 ///
736 /// [`first_decoder`]: Self::first_decoder
737 pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
738 let imp = self
739 .implementations(¶ms.codec_id)
740 .iter()
741 .find(|i| i.make_encoder.is_some())
742 .ok_or_else(|| {
743 Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
744 })?;
745 (imp.make_encoder.expect("checked above"))(params)
746 }
747
748 /// Look up a decoder by exact implementation name
749 /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
750 /// isn't registered or if it has no decoder factory.
751 pub fn decoder_by_impl(
752 &self,
753 impl_name: &str,
754 params: &CodecParameters,
755 ) -> Result<Box<dyn Decoder>> {
756 let imp = self
757 .implementations(¶ms.codec_id)
758 .iter()
759 .find(|i| i.caps.implementation == impl_name)
760 .ok_or_else(|| {
761 Error::CodecNotFound(format!(
762 "no implementation `{impl_name}` for codec {}",
763 params.codec_id
764 ))
765 })?;
766 let factory = imp
767 .make_decoder
768 .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
769 factory(params)
770 }
771
772 /// Look up an encoder by exact implementation name — see
773 /// [`decoder_by_impl`].
774 ///
775 /// [`decoder_by_impl`]: Self::decoder_by_impl
776 pub fn encoder_by_impl(
777 &self,
778 impl_name: &str,
779 params: &CodecParameters,
780 ) -> Result<Box<dyn Encoder>> {
781 let imp = self
782 .implementations(¶ms.codec_id)
783 .iter()
784 .find(|i| i.caps.implementation == impl_name)
785 .ok_or_else(|| {
786 Error::CodecNotFound(format!(
787 "no implementation `{impl_name}` for codec {}",
788 params.codec_id
789 ))
790 })?;
791 let factory = imp
792 .make_encoder
793 .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
794 factory(params)
795 }
796
797 /// Iterate codec ids that have at least one decoder implementation,
798 /// in the order each id's first implementation was registered.
799 pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
800 self.impl_order.iter().filter(|id| self.has_decoder(id))
801 }
802
803 /// Iterate codec ids that have at least one encoder implementation,
804 /// in the order each id's first implementation was registered.
805 pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
806 self.impl_order.iter().filter(|id| self.has_encoder(id))
807 }
808
809 /// All registered implementations of a given codec id.
810 pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
811 self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
812 }
813
814 /// Lookup the encoder options schema for a registered codec. Walks
815 /// implementations in registration order and returns the first
816 /// schema found. `None` means either the codec isn't registered or
817 /// no implementation declared an encoder schema.
818 pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
819 self.impls
820 .get(id)?
821 .iter()
822 .find_map(|i| i.encoder_options_schema)
823 }
824
825 /// Lookup the decoder options schema — see
826 /// [`encoder_options_schema`](Self::encoder_options_schema).
827 pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
828 self.impls
829 .get(id)?
830 .iter()
831 .find_map(|i| i.decoder_options_schema)
832 }
833
834 /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
835 /// to show capability flags per implementation. Ids come in the
836 /// order their first implementation was registered; implementations
837 /// of one id in their own registration order.
838 pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
839 self.impl_order
840 .iter()
841 .flat_map(move |id| self.implementations(id).iter().map(move |i| (id, i)))
842 }
843
844 /// Iterator over every `(tag, codec_id)` pair currently registered —
845 /// used by `oxideav tags` debug output and by tests that want to
846 /// walk the tag surface. Registration order, then each
847 /// registration's tag declaration order.
848 pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
849 self.registrations
850 .iter()
851 .flat_map(|rec| rec.tags.iter().map(move |tag| (tag, &rec.id)))
852 }
853
854 /// Inherent form of tag resolution that returns a reference.
855 /// The owned-value form used by container code lives behind the
856 /// [`CodecResolver`] trait impl below.
857 ///
858 /// Walks every registration that claimed `ctx.tag`, calls its
859 /// probe with `ctx`, and returns the id of the registration that
860 /// scored highest. Probes that return `0.0` (or anything not
861 /// strictly positive, `NaN` included) are discarded; ties on
862 /// confidence are broken by [`CodecInfo::resolution_priority`]
863 /// (lower wins), then by registration order (first wins).
864 /// Registrations with no probe are treated as returning `1.0`.
865 /// [`resolve_tag_candidates`](Self::resolve_tag_candidates) returns
866 /// the whole ranked list this picks the head of.
867 pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
868 self.resolve_tag_candidates(ctx).first().map(|c| c.id)
869 }
870
871 /// Every registration that claimed `ctx.tag` and whose probe
872 /// accepts the context, ranked by the resolution rule: confidence
873 /// descending, then priority ascending, then registration order
874 /// ascending. Empty when the tag is unclaimed or every claimant's
875 /// probe refused. The observation hook for collision audits: a
876 /// list with two heads at equal confidence *and* equal priority is
877 /// a tie the registry broke by registration order alone.
878 pub fn resolve_tag_candidates(&self, ctx: &ProbeContext) -> Vec<TagCandidate<'_>> {
879 let Some(idxs) = self.tag_index.get(ctx.tag) else {
880 return Vec::new();
881 };
882 let mut out: Vec<TagCandidate<'_>> = idxs
883 .iter()
884 .filter_map(|&i| {
885 let rec = &self.registrations[i];
886 let confidence = match rec.probe {
887 Some(f) => f(ctx),
888 None => 1.0,
889 };
890 // `> 0.0` is false for NaN, so a broken probe is
891 // dropped rather than ranked unpredictably.
892 (confidence > 0.0).then_some(TagCandidate {
893 id: &rec.id,
894 confidence,
895 priority: rec.priority,
896 order: i,
897 })
898 })
899 .collect();
900 // `idxs` is already in registration order and `order` is
901 // unique, so the sort key is total and the result is fixed by
902 // the registrations alone.
903 out.sort_by(|a, b| {
904 b.confidence
905 .partial_cmp(&a.confidence)
906 .unwrap_or(std::cmp::Ordering::Equal)
907 .then(a.priority.cmp(&b.priority))
908 .then(a.order.cmp(&b.order))
909 });
910 out
911 }
912
913 /// Inherent form of payload-magic resolution that returns a
914 /// reference. The owned-value form used by container code lives
915 /// behind the [`CodecResolver`] trait impl below.
916 ///
917 /// Walks every registered payload magic prefix (declared via
918 /// [`CodecInfo::payload_magic`] / [`CodecInfo::payload_magics`]) and
919 /// returns the codec whose magic is a prefix of `first_bytes` —
920 /// however much of the stream's leading payload the caller has
921 /// (an Ogg demuxer passes the first packet of a logical stream; a
922 /// raw-stream prober passes the file head). The **longest**
923 /// matching magic wins (most specific claim); remaining ties are
924 /// broken by [`CodecInfo::resolution_priority`] (lower wins), then
925 /// by registration order (first wins). Unlike the tag path there
926 /// is no probe step: a payload magic is itself the bitstream
927 /// evidence a probe would look for, and specificity is expressed
928 /// by prefix length instead of a confidence value.
929 /// [`resolve_payload_magic_candidates`](Self::resolve_payload_magic_candidates)
930 /// returns the whole ranked list this picks the head of.
931 pub fn resolve_payload_magic_ref(&self, first_bytes: &[u8]) -> Option<&CodecId> {
932 self.resolve_payload_magic_candidates(first_bytes)
933 .first()
934 .map(|c| c.id)
935 }
936
937 /// Every registered payload magic that is a prefix of
938 /// `first_bytes`, ranked by the resolution rule: magic length
939 /// descending, then priority ascending, then registration order
940 /// ascending. The payload-magic companion to
941 /// [`resolve_tag_candidates`](Self::resolve_tag_candidates).
942 pub fn resolve_payload_magic_candidates(
943 &self,
944 first_bytes: &[u8],
945 ) -> Vec<PayloadMagicCandidate<'_>> {
946 let mut out: Vec<PayloadMagicCandidate<'_>> = self
947 .magic_index
948 .iter()
949 .filter(|(magic, _)| first_bytes.starts_with(magic))
950 .map(|(magic, i)| {
951 let rec = &self.registrations[*i];
952 PayloadMagicCandidate {
953 id: &rec.id,
954 magic: magic.as_slice(),
955 priority: rec.priority,
956 order: *i,
957 }
958 })
959 .collect();
960 // One registration may claim several magics that all match
961 // (`order` then repeats); the stable sort keeps those in their
962 // declaration order, which is the only remaining distinction
963 // and is itself fixed by the registration.
964 out.sort_by(|a, b| {
965 b.magic
966 .len()
967 .cmp(&a.magic.len())
968 .then(a.priority.cmp(&b.priority))
969 .then(a.order.cmp(&b.order))
970 });
971 out
972 }
973
974 /// Iterator over every `(payload magic, codec_id)` pair currently
975 /// registered, in registration order — the payload-magic companion
976 /// to [`all_tag_registrations`](Self::all_tag_registrations).
977 pub fn all_payload_magic_registrations(&self) -> impl Iterator<Item = (&[u8], &CodecId)> {
978 self.magic_index
979 .iter()
980 .map(move |(magic, i)| (magic.as_slice(), &self.registrations[*i].id))
981 }
982}
983
984/// Implement the shared [`CodecResolver`] interface so container
985/// demuxers can accept `&dyn CodecResolver` without depending on
986/// this crate directly — the trait lives in oxideav-core.
987impl CodecResolver for CodecRegistry {
988 fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
989 self.resolve_tag_ref(ctx).cloned()
990 }
991
992 fn resolve_payload_magic(&self, first_packet: &[u8]) -> Option<CodecId> {
993 self.resolve_payload_magic_ref(first_packet).cloned()
994 }
995}
996
997#[cfg(test)]
998mod tag_tests {
999 use super::*;
1000 use crate::CodecCapabilities;
1001
1002 /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
1003 /// 0x000001 start code in the first few bytes).
1004 fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
1005 match ctx.packet {
1006 Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
1007 Some(_) => 0.0,
1008 None => 0.5, // no data yet — weak evidence
1009 }
1010 }
1011
1012 /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
1013 /// (starts with a 0x000001 start code in the first few bytes).
1014 fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
1015 match ctx.packet {
1016 Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
1017 Some(_) => 0.0,
1018 None => 0.5,
1019 }
1020 }
1021
1022 fn info(id: &str) -> CodecInfo {
1023 CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1024 }
1025
1026 #[test]
1027 fn resolve_single_claim_no_probe() {
1028 let mut reg = CodecRegistry::new();
1029 reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
1030 let t = CodecTag::fourcc(b"FLAC");
1031 assert_eq!(
1032 reg.resolve_tag_ref(&ProbeContext::new(&t))
1033 .map(|c| c.as_str()),
1034 Some("flac"),
1035 );
1036 }
1037
1038 #[test]
1039 fn resolve_missing_tag_returns_none() {
1040 let reg = CodecRegistry::new();
1041 let t = CodecTag::fourcc(b"????");
1042 assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
1043 }
1044
1045 #[test]
1046 fn unprobed_claims_tie_first_registered_wins() {
1047 // Two unprobed claims on the same tag: deterministic order.
1048 let mut reg = CodecRegistry::new();
1049 reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
1050 reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
1051 let t = CodecTag::fourcc(b"TEST");
1052 assert_eq!(
1053 reg.resolve_tag_ref(&ProbeContext::new(&t))
1054 .map(|c| c.as_str()),
1055 Some("first"),
1056 );
1057 }
1058
1059 #[test]
1060 fn probe_picks_matching_bitstream() {
1061 // The core bug fix: every probe is asked and the highest
1062 // confidence wins regardless of registration order.
1063 let mut reg = CodecRegistry::new();
1064 reg.register(
1065 info("msmpeg4v3")
1066 .probe(probe_msmpeg4)
1067 .tag(CodecTag::fourcc(b"DIV3")),
1068 );
1069 reg.register(
1070 info("mpeg4video")
1071 .probe(probe_mpeg4_part2)
1072 .tag(CodecTag::fourcc(b"DIV3")),
1073 );
1074
1075 let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
1076 let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
1077 let tag = CodecTag::fourcc(b"DIV3");
1078
1079 let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
1080 assert_eq!(
1081 reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
1082 Some("mpeg4video"),
1083 );
1084 let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
1085 assert_eq!(
1086 reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
1087 Some("msmpeg4v3"),
1088 );
1089 }
1090
1091 #[test]
1092 fn unprobed_claim_wins_against_low_confidence_probe() {
1093 // One codec claims a tag without a probe (→ confidence 1.0)
1094 // and another claims it with a probe returning 0.3. The
1095 // unprobed one wins — a codec that knows it owns the tag
1096 // outright should not lose to a speculative probe.
1097 let mut reg = CodecRegistry::new();
1098 reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
1099 reg.register(
1100 info("speculative")
1101 .probe(|_| 0.3)
1102 .tag(CodecTag::fourcc(b"OWN_")),
1103 );
1104 let t = CodecTag::fourcc(b"OWN_");
1105 assert_eq!(
1106 reg.resolve_tag_ref(&ProbeContext::new(&t))
1107 .map(|c| c.as_str()),
1108 Some("owner"),
1109 );
1110 }
1111
1112 #[test]
1113 fn probe_returning_zero_is_skipped() {
1114 let mut reg = CodecRegistry::new();
1115 reg.register(
1116 info("refuses")
1117 .probe(|_| 0.0)
1118 .tag(CodecTag::fourcc(b"MAYB")),
1119 );
1120 reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
1121 let t = CodecTag::fourcc(b"MAYB");
1122 let ctx = ProbeContext::new(&t).packet(b"hello");
1123 assert_eq!(
1124 reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
1125 Some("fallback"),
1126 );
1127 }
1128
1129 #[test]
1130 fn fourcc_case_insensitive_lookup() {
1131 let mut reg = CodecRegistry::new();
1132 reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
1133 // Registered as "DIV3" (uppercase via ctor); lookup using
1134 // lowercase / mixed case also hits.
1135 let upper = CodecTag::fourcc(b"DIV3");
1136 let lower = CodecTag::fourcc(b"div3");
1137 let mixed = CodecTag::fourcc(b"DiV3");
1138 assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
1139 assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
1140 assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
1141 }
1142
1143 #[test]
1144 fn wave_format_and_matroska_tags_work() {
1145 let mut reg = CodecRegistry::new();
1146 reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
1147 reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
1148 let wf = CodecTag::wave_format(0x0055);
1149 let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
1150 assert_eq!(
1151 reg.resolve_tag_ref(&ProbeContext::new(&wf))
1152 .map(|c| c.as_str()),
1153 Some("mp3"),
1154 );
1155 assert_eq!(
1156 reg.resolve_tag_ref(&ProbeContext::new(&mk))
1157 .map(|c| c.as_str()),
1158 Some("h264"),
1159 );
1160 }
1161
1162 #[test]
1163 fn mp4_object_type_tag_works() {
1164 let mut reg = CodecRegistry::new();
1165 reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
1166 let t = CodecTag::mp4_object_type(0x40);
1167 assert_eq!(
1168 reg.resolve_tag_ref(&ProbeContext::new(&t))
1169 .map(|c| c.as_str()),
1170 Some("aac"),
1171 );
1172 }
1173
1174 #[test]
1175 fn multi_tag_claim_all_resolve() {
1176 let mut reg = CodecRegistry::new();
1177 reg.register(info("aac").tags([
1178 CodecTag::fourcc(b"MP4A"),
1179 CodecTag::wave_format(0x00FF),
1180 CodecTag::mp4_object_type(0x40),
1181 CodecTag::matroska("A_AAC"),
1182 ]));
1183 for t in [
1184 CodecTag::fourcc(b"MP4A"),
1185 CodecTag::wave_format(0x00FF),
1186 CodecTag::mp4_object_type(0x40),
1187 CodecTag::matroska("A_AAC"),
1188 ] {
1189 assert_eq!(
1190 reg.resolve_tag_ref(&ProbeContext::new(&t))
1191 .map(|c| c.as_str()),
1192 Some("aac"),
1193 "tag {t:?} did not resolve",
1194 );
1195 }
1196 }
1197}
1198
1199#[cfg(test)]
1200mod payload_magic_tests {
1201 use super::*;
1202 use crate::CodecCapabilities;
1203
1204 fn info(id: &str) -> CodecInfo {
1205 CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1206 }
1207
1208 /// Registry with the classic Ogg family registered, each under its
1209 /// real BOS magic.
1210 fn ogg_family_registry() -> CodecRegistry {
1211 let mut reg = CodecRegistry::new();
1212 reg.register(info("vorbis").payload_magic(b"\x01vorbis"));
1213 reg.register(info("opus").payload_magic(b"OpusHead"));
1214 reg.register(info("theora").payload_magic(b"\x80theora"));
1215 reg.register(info("flac").payload_magic(b"\x7fFLAC"));
1216 reg
1217 }
1218
1219 #[test]
1220 fn resolve_payload_magic_matches_first_packet_prefix() {
1221 let reg = ogg_family_registry();
1222 // A Vorbis identification header: magic + version + channels +
1223 // rate + ... — the resolver only needs the prefix to match.
1224 let vorbis_id_header = b"\x01vorbis\x00\x00\x00\x00\x02\x44\xac\x00\x00";
1225 assert_eq!(
1226 reg.resolve_payload_magic_ref(vorbis_id_header)
1227 .map(|c| c.as_str()),
1228 Some("vorbis"),
1229 );
1230 assert_eq!(
1231 reg.resolve_payload_magic_ref(b"OpusHead\x01\x02\x38\x01")
1232 .map(|c| c.as_str()),
1233 Some("opus"),
1234 );
1235 assert_eq!(
1236 reg.resolve_payload_magic_ref(b"\x80theora\x03\x02\x01")
1237 .map(|c| c.as_str()),
1238 Some("theora"),
1239 );
1240 assert_eq!(
1241 reg.resolve_payload_magic_ref(b"\x7fFLAC\x01\x00")
1242 .map(|c| c.as_str()),
1243 Some("flac"),
1244 );
1245 }
1246
1247 #[test]
1248 fn resolve_payload_magic_exact_length_packet_matches() {
1249 // A packet that is exactly the magic (nothing after it) still
1250 // resolves — starts_with is inclusive of equality.
1251 let reg = ogg_family_registry();
1252 assert_eq!(
1253 reg.resolve_payload_magic_ref(b"OpusHead")
1254 .map(|c| c.as_str()),
1255 Some("opus"),
1256 );
1257 }
1258
1259 #[test]
1260 fn resolve_payload_magic_unknown_or_short_packet_is_none() {
1261 let reg = ogg_family_registry();
1262 // Unknown magic.
1263 assert!(reg.resolve_payload_magic_ref(b"Speex 1.2.0").is_none());
1264 // Packet shorter than every registered magic.
1265 assert!(reg.resolve_payload_magic_ref(b"Opus").is_none());
1266 // Empty packet.
1267 assert!(reg.resolve_payload_magic_ref(b"").is_none());
1268 }
1269
1270 #[test]
1271 fn resolve_payload_magic_longest_prefix_wins_regardless_of_order() {
1272 // A shorter magic that is itself a prefix of a longer one must
1273 // lose to the more specific claim, whichever registered first.
1274 let mut reg = CodecRegistry::new();
1275 reg.register(info("generic").payload_magic(b"Opus"));
1276 reg.register(info("opus").payload_magic(b"OpusHead"));
1277 assert_eq!(
1278 reg.resolve_payload_magic_ref(b"OpusHead\x01")
1279 .map(|c| c.as_str()),
1280 Some("opus"),
1281 );
1282 // ...but the shorter claim still wins packets only it matches.
1283 assert_eq!(
1284 reg.resolve_payload_magic_ref(b"OpusTags")
1285 .map(|c| c.as_str()),
1286 Some("generic"),
1287 );
1288
1289 // Same result with the registration order flipped.
1290 let mut reg = CodecRegistry::new();
1291 reg.register(info("opus").payload_magic(b"OpusHead"));
1292 reg.register(info("generic").payload_magic(b"Opus"));
1293 assert_eq!(
1294 reg.resolve_payload_magic_ref(b"OpusHead\x01")
1295 .map(|c| c.as_str()),
1296 Some("opus"),
1297 );
1298 }
1299
1300 #[test]
1301 fn resolve_payload_magic_equal_length_tie_first_registered_wins() {
1302 let mut reg = CodecRegistry::new();
1303 reg.register(info("first").payload_magic(b"SameMagic"));
1304 reg.register(info("second").payload_magic(b"SameMagic"));
1305 assert_eq!(
1306 reg.resolve_payload_magic_ref(b"SameMagic\x00")
1307 .map(|c| c.as_str()),
1308 Some("first"),
1309 );
1310 }
1311
1312 #[test]
1313 fn empty_payload_magic_is_ignored_at_registration() {
1314 let mut reg = CodecRegistry::new();
1315 reg.register(info("greedy").payload_magic(b""));
1316 assert!(reg.resolve_payload_magic_ref(b"anything at all").is_none());
1317 assert_eq!(reg.all_payload_magic_registrations().count(), 0);
1318 }
1319
1320 #[test]
1321 fn payload_magics_plural_builder_and_diagnostics() {
1322 // One codec answering to several magics via the iterable
1323 // builder; the diagnostic iterator surfaces each claim in
1324 // registration order.
1325 let mut reg = CodecRegistry::new();
1326 reg.register(info("speex").payload_magics([b"Speex ".to_vec(), b"speex-alt".to_vec()]));
1327 assert_eq!(
1328 reg.resolve_payload_magic_ref(b"Speex 1.2")
1329 .map(|c| c.as_str()),
1330 Some("speex"),
1331 );
1332 assert_eq!(
1333 reg.resolve_payload_magic_ref(b"speex-alt\x00")
1334 .map(|c| c.as_str()),
1335 Some("speex"),
1336 );
1337 let all: Vec<(&[u8], &str)> = reg
1338 .all_payload_magic_registrations()
1339 .map(|(m, id)| (m, id.as_str()))
1340 .collect();
1341 assert_eq!(
1342 all,
1343 vec![
1344 (b"Speex ".as_slice(), "speex"),
1345 (b"speex-alt".as_slice(), "speex"),
1346 ],
1347 );
1348 }
1349
1350 #[test]
1351 fn magic_claims_compose_with_tag_claims_on_one_registration() {
1352 // A codec that lives in both Ogg and Matroska declares both
1353 // claim kinds on one CodecInfo; each resolution path finds it.
1354 let mut reg = CodecRegistry::new();
1355 reg.register(
1356 info("vorbis")
1357 .tag(CodecTag::matroska("A_VORBIS"))
1358 .payload_magic(b"\x01vorbis"),
1359 );
1360 let mk = CodecTag::matroska("A_VORBIS");
1361 assert_eq!(
1362 reg.resolve_tag_ref(&ProbeContext::new(&mk))
1363 .map(|c| c.as_str()),
1364 Some("vorbis"),
1365 );
1366 assert_eq!(
1367 reg.resolve_payload_magic_ref(b"\x01vorbis\x00")
1368 .map(|c| c.as_str()),
1369 Some("vorbis"),
1370 );
1371 }
1372
1373 #[test]
1374 fn resolver_trait_payload_magic_surface() {
1375 // The owned-value trait form mirrors the inherent form, and
1376 // the default implementation (NullCodecResolver) resolves
1377 // nothing.
1378 let reg = ogg_family_registry();
1379 let resolver: &dyn CodecResolver = ®
1380 assert_eq!(
1381 resolver
1382 .resolve_payload_magic(b"OpusHead\x01")
1383 .map(|c| c.0.clone()),
1384 Some("opus".to_owned()),
1385 );
1386 assert!(resolver.resolve_payload_magic(b"unknown").is_none());
1387
1388 let null = crate::NullCodecResolver;
1389 assert!(null.resolve_payload_magic(b"OpusHead\x01").is_none());
1390 }
1391
1392 /// The surface is container-agnostic: a raw elementary stream
1393 /// identified by a file-head magic resolves through the same path
1394 /// as the Ogg family — nothing about the mechanism is Ogg-shaped.
1395 #[test]
1396 fn payload_magic_serves_non_ogg_carriage() {
1397 let mut reg = CodecRegistry::new();
1398 reg.register(info("flac").payload_magic(b"fLaC"));
1399 reg.register(info("shorten").payload_magic(b"ajkg"));
1400
1401 assert_eq!(
1402 reg.resolve_payload_magic_ref(b"fLaC\x00\x00\x00\x22"),
1403 Some(&CodecId::new("flac"))
1404 );
1405 assert_eq!(
1406 reg.resolve_payload_magic_ref(b"ajkg\x02"),
1407 Some(&CodecId::new("shorten"))
1408 );
1409 assert_eq!(reg.resolve_payload_magic_ref(b"RIFF"), None);
1410 }
1411}
1412
1413/// Resolution-order tests. Every id / tag / magic here is invented:
1414/// the rule is format-agnostic and must be provable with synthetic
1415/// registrations only.
1416#[cfg(test)]
1417mod resolution_order_tests {
1418 use super::*;
1419 use crate::CodecCapabilities;
1420
1421 fn info(id: &str) -> CodecInfo {
1422 CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1423 }
1424
1425 fn tag_ids<'a>(cands: &[TagCandidate<'a>]) -> Vec<&'a str> {
1426 cands.iter().map(|c| c.id.as_str()).collect()
1427 }
1428
1429 fn magic_ids<'a>(cands: &[PayloadMagicCandidate<'a>]) -> Vec<&'a str> {
1430 cands.iter().map(|c| c.id.as_str()).collect()
1431 }
1432
1433 #[test]
1434 fn codec_info_resolution_priority_defaults_and_builder() {
1435 assert_eq!(info("x").resolution_priority, crate::DEFAULT_PRIORITY);
1436 assert_eq!(info("x").with_resolution_priority(7).resolution_priority, 7);
1437 }
1438
1439 #[test]
1440 fn equal_tag_claims_resolve_by_registration_order_across_fresh_contexts() {
1441 const ORDER: [&str; 5] = ["zeta", "alpha", "omicron", "beta", "kappa"];
1442 let tag = CodecTag::fourcc(b"SYNT");
1443 for _ in 0..1000 {
1444 let mut ctx = crate::RuntimeContext::new();
1445 for id in ORDER {
1446 ctx.codecs.register(info(id).tag(tag.clone()));
1447 }
1448 let pc = ProbeContext::new(&tag);
1449 assert_eq!(
1450 ctx.codecs.resolve_tag_ref(&pc).map(|c| c.as_str()),
1451 Some("zeta")
1452 );
1453 let cands = ctx.codecs.resolve_tag_candidates(&pc);
1454 assert_eq!(tag_ids(&cands), ORDER.to_vec());
1455 assert_eq!(
1456 cands.iter().map(|c| c.order).collect::<Vec<_>>(),
1457 vec![0, 1, 2, 3, 4]
1458 );
1459 }
1460 }
1461
1462 #[test]
1463 fn equal_magic_claims_resolve_by_registration_order_across_fresh_contexts() {
1464 const ORDER: [&str; 4] = ["zeta", "alpha", "omicron", "beta"];
1465 for _ in 0..1000 {
1466 let mut ctx = crate::RuntimeContext::new();
1467 for id in ORDER {
1468 ctx.codecs.register(info(id).payload_magic(b"SYNTHMAGIC"));
1469 }
1470 assert_eq!(
1471 ctx.codecs
1472 .resolve_payload_magic_ref(b"SYNTHMAGIC\x00")
1473 .map(|c| c.as_str()),
1474 Some("zeta")
1475 );
1476 let cands = ctx
1477 .codecs
1478 .resolve_payload_magic_candidates(b"SYNTHMAGIC\x00");
1479 assert_eq!(magic_ids(&cands), ORDER.to_vec());
1480 }
1481 }
1482
1483 #[test]
1484 fn tag_priority_beats_registration_order() {
1485 let tag = CodecTag::wave_format(0x7777);
1486 let mut reg = CodecRegistry::new();
1487 reg.register(info("earlier").tag(tag.clone()));
1488 reg.register(
1489 info("later-preferred")
1490 .with_resolution_priority(10)
1491 .tag(tag.clone()),
1492 );
1493 let pc = ProbeContext::new(&tag);
1494 assert_eq!(
1495 reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1496 Some("later-preferred")
1497 );
1498 let cands = reg.resolve_tag_candidates(&pc);
1499 assert_eq!(tag_ids(&cands), vec!["later-preferred", "earlier"]);
1500 assert_eq!(cands[0].priority, 10);
1501 assert_eq!(cands[0].order, 1);
1502 assert_eq!(cands[1].priority, crate::DEFAULT_PRIORITY);
1503 assert_eq!(cands[1].order, 0);
1504 assert_eq!(cands[0].confidence, 1.0);
1505 }
1506
1507 #[test]
1508 fn tag_confidence_beats_priority() {
1509 // A probe that positively identifies the payload wins against
1510 // an unprobed-but-top-priority claim only when its confidence
1511 // is higher; at 1.0 vs 1.0 the priority decides.
1512 let tag = CodecTag::fourcc(b"AMBI");
1513 let mut reg = CodecRegistry::new();
1514 reg.register(
1515 info("prioritised")
1516 .with_resolution_priority(i32::MIN)
1517 .probe(|_| 0.6)
1518 .tag(tag.clone()),
1519 );
1520 reg.register(info("certain").tag(tag.clone()));
1521 let pc = ProbeContext::new(&tag);
1522 assert_eq!(
1523 reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1524 Some("certain")
1525 );
1526 let cands = reg.resolve_tag_candidates(&pc);
1527 assert_eq!(tag_ids(&cands), vec!["certain", "prioritised"]);
1528 }
1529
1530 #[test]
1531 fn tag_candidates_drop_refusing_and_non_finite_probes() {
1532 let tag = CodecTag::fourcc(b"NANP");
1533 let mut reg = CodecRegistry::new();
1534 reg.register(info("refuses").probe(|_| 0.0).tag(tag.clone()));
1535 reg.register(info("broken").probe(|_| f32::NAN).tag(tag.clone()));
1536 reg.register(info("negative").probe(|_| -1.0).tag(tag.clone()));
1537 reg.register(info("accepts").probe(|_| 0.2).tag(tag.clone()));
1538 let pc = ProbeContext::new(&tag);
1539 assert_eq!(tag_ids(®.resolve_tag_candidates(&pc)), vec!["accepts"]);
1540 assert_eq!(
1541 reg.resolve_tag_ref(&pc).map(|c| c.as_str()),
1542 Some("accepts")
1543 );
1544 // An unclaimed tag yields an empty list, not an error.
1545 let other = CodecTag::fourcc(b"NONE");
1546 assert!(reg
1547 .resolve_tag_candidates(&ProbeContext::new(&other))
1548 .is_empty());
1549 }
1550
1551 #[test]
1552 fn duplicate_tag_claim_by_one_id_is_visible_not_folded() {
1553 // The registry contract allows registering one id several
1554 // times (multi-implementation codecs). A tag claimed twice by
1555 // the same id resolves to that id either way; the candidate
1556 // list shows both claims so an audit can flag the redundancy.
1557 let tag = CodecTag::fourcc(b"TWIC");
1558 let mut reg = CodecRegistry::new();
1559 reg.register(info("dup").tag(tag.clone()));
1560 reg.register(info("other").tag(tag.clone()));
1561 reg.register(info("dup").tag(tag.clone()));
1562 let pc = ProbeContext::new(&tag);
1563 let cands = reg.resolve_tag_candidates(&pc);
1564 assert_eq!(tag_ids(&cands), vec!["dup", "other", "dup"]);
1565 assert_eq!(
1566 cands.iter().map(|c| c.order).collect::<Vec<_>>(),
1567 vec![0, 1, 2]
1568 );
1569 assert_eq!(reg.resolve_tag_ref(&pc).map(|c| c.as_str()), Some("dup"));
1570 }
1571
1572 #[test]
1573 fn magic_priority_beats_registration_order_but_not_length() {
1574 let mut reg = CodecRegistry::new();
1575 reg.register(info("earlier").payload_magic(b"MAGIC"));
1576 reg.register(
1577 info("later-preferred")
1578 .with_resolution_priority(1)
1579 .payload_magic(b"MAGIC"),
1580 );
1581 reg.register(info("longer").payload_magic(b"MAGICAL"));
1582 // Equal-length tie: priority decides.
1583 let cands = reg.resolve_payload_magic_candidates(b"MAGIC\x00");
1584 assert_eq!(magic_ids(&cands), vec!["later-preferred", "earlier"]);
1585 assert_eq!(cands[0].order, 1);
1586 assert_eq!(cands[0].magic, b"MAGIC");
1587 // A longer matching magic beats both regardless of priority.
1588 let cands = reg.resolve_payload_magic_candidates(b"MAGICAL\x00");
1589 assert_eq!(
1590 magic_ids(&cands),
1591 vec!["longer", "later-preferred", "earlier"]
1592 );
1593 assert_eq!(
1594 reg.resolve_payload_magic_ref(b"MAGICAL\x00")
1595 .map(|c| c.as_str()),
1596 Some("longer")
1597 );
1598 // Nothing matches: empty list, None.
1599 assert!(reg.resolve_payload_magic_candidates(b"other").is_empty());
1600 assert!(reg.resolve_payload_magic_ref(b"other").is_none());
1601 }
1602
1603 fn dec(_p: &crate::CodecParameters) -> crate::Result<Box<dyn Decoder>> {
1604 Err(Error::unsupported("dummy"))
1605 }
1606 fn enc(_p: &crate::CodecParameters) -> crate::Result<Box<dyn Encoder>> {
1607 Err(Error::unsupported("dummy"))
1608 }
1609
1610 #[test]
1611 fn enumerations_follow_registration_order() {
1612 for _ in 0..200 {
1613 let mut reg = CodecRegistry::new();
1614 reg.register(
1615 info("zeta")
1616 .decoder(dec)
1617 .tags([CodecTag::fourcc(b"ZET1"), CodecTag::fourcc(b"ZET2")]),
1618 );
1619 reg.register(info("alpha").encoder(enc).tag(CodecTag::fourcc(b"ALPH")));
1620 reg.register(info("omicron").decoder(dec).encoder(enc));
1621 // Second impl of an existing id does not move it.
1622 reg.register(info("zeta").encoder(enc).tag(CodecTag::fourcc(b"ZET3")));
1623 // Tag-only registration: no impl, but its tags are listed.
1624 reg.register(info("beta").tag(CodecTag::fourcc(b"BETA")));
1625
1626 let ids: Vec<&str> = reg
1627 .all_implementations()
1628 .map(|(id, _)| id.as_str())
1629 .collect();
1630 assert_eq!(ids, vec!["zeta", "zeta", "alpha", "omicron"]);
1631 let dec_ids: Vec<&str> = reg.decoder_ids().map(|i| i.as_str()).collect();
1632 assert_eq!(dec_ids, vec!["zeta", "omicron"]);
1633 let enc_ids: Vec<&str> = reg.encoder_ids().map(|i| i.as_str()).collect();
1634 assert_eq!(enc_ids, vec!["zeta", "alpha", "omicron"]);
1635 let tags: Vec<(String, &str)> = reg
1636 .all_tag_registrations()
1637 .map(|(t, id)| (format!("{t:?}"), id.as_str()))
1638 .collect();
1639 let want: Vec<(String, &str)> = [
1640 (CodecTag::fourcc(b"ZET1"), "zeta"),
1641 (CodecTag::fourcc(b"ZET2"), "zeta"),
1642 (CodecTag::fourcc(b"ALPH"), "alpha"),
1643 (CodecTag::fourcc(b"ZET3"), "zeta"),
1644 (CodecTag::fourcc(b"BETA"), "beta"),
1645 ]
1646 .into_iter()
1647 .map(|(t, id)| (format!("{t:?}"), id))
1648 .collect();
1649 assert_eq!(tags, want);
1650 }
1651 }
1652
1653 #[test]
1654 fn resolution_priority_is_independent_of_capabilities_priority() {
1655 // A registration that ranks itself first as an *implementation*
1656 // (HW-style capabilities priority) does not thereby win a tag
1657 // tie against a different codec id registered earlier.
1658 let tag = CodecTag::fourcc(b"SHRD");
1659 let mut reg = CodecRegistry::new();
1660 reg.register(info("sw-id").tag(tag.clone()));
1661 reg.register(
1662 CodecInfo::new(CodecId::new("hw-id"))
1663 .capabilities(CodecCapabilities::video("hw-id").with_priority(10))
1664 .tag(tag.clone()),
1665 );
1666 let pc = ProbeContext::new(&tag);
1667 assert_eq!(reg.resolve_tag_ref(&pc).map(|c| c.as_str()), Some("sw-id"));
1668 let cands = reg.resolve_tag_candidates(&pc);
1669 assert!(cands.iter().all(|c| c.priority == crate::DEFAULT_PRIORITY));
1670 }
1671}
1672
1673#[cfg(test)]
1674mod engine_tests {
1675 use super::*;
1676 use crate::engine::HwDeviceInfo;
1677
1678 #[test]
1679 fn codec_info_engine_id_and_probe_default_to_none() {
1680 let ci = CodecInfo::new(CodecId::new("h264"));
1681 assert!(ci.engine_id.is_none());
1682 assert!(ci.engine_probe.is_none());
1683 }
1684
1685 #[test]
1686 fn codec_info_engine_builder_methods_set_fields() {
1687 fn dummy_probe() -> Vec<HwDeviceInfo> {
1688 vec![]
1689 }
1690 let ci = CodecInfo::new(CodecId::new("h264"))
1691 .with_engine_id("nvidia")
1692 .with_engine_probe(dummy_probe);
1693 assert_eq!(ci.engine_id, Some("nvidia"));
1694 assert!(ci.engine_probe.is_some());
1695 let probe = ci.engine_probe.unwrap();
1696 let result = probe();
1697 assert!(result.is_empty());
1698 }
1699
1700 #[test]
1701 fn registering_codec_with_engine_metadata_does_not_panic() {
1702 // The new fields are passthrough metadata — register() should
1703 // accept them without affecting existing id/tag bookkeeping.
1704 fn dummy_probe() -> Vec<HwDeviceInfo> {
1705 vec![]
1706 }
1707 let mut reg = CodecRegistry::new();
1708 reg.register(
1709 CodecInfo::new(CodecId::new("h264"))
1710 .capabilities(CodecCapabilities::audio("h264_nvdec"))
1711 .tag(CodecTag::fourcc(b"H264"))
1712 .with_engine_id("nvidia")
1713 .with_engine_probe(dummy_probe),
1714 );
1715 let t = CodecTag::fourcc(b"H264");
1716 assert_eq!(
1717 reg.resolve_tag_ref(&ProbeContext::new(&t))
1718 .map(|c| c.as_str()),
1719 Some("h264"),
1720 );
1721 }
1722
1723 /// No-op decoder factory so the registration produces a real
1724 /// CodecImplementation (the registry skips tag-only entries —
1725 /// without a factory there'd be nothing in `implementations()`
1726 /// to assert against).
1727 fn dummy_decoder_factory(
1728 _params: &crate::CodecParameters,
1729 ) -> crate::Result<Box<dyn super::Decoder>> {
1730 Err(crate::Error::unsupported("dummy decoder"))
1731 }
1732
1733 #[test]
1734 fn engine_metadata_propagates_through_register() {
1735 fn dummy_probe() -> Vec<HwDeviceInfo> {
1736 vec![]
1737 }
1738 let mut reg = CodecRegistry::default();
1739 reg.register(
1740 CodecInfo::new(CodecId::new("h264"))
1741 .capabilities(CodecCapabilities::video("h264_test"))
1742 .decoder(dummy_decoder_factory)
1743 .with_engine_id("test-backend")
1744 .with_engine_probe(dummy_probe),
1745 );
1746 let impls = reg.implementations(&CodecId::new("h264"));
1747 assert_eq!(impls.len(), 1);
1748 assert_eq!(impls[0].engine_id, Some("test-backend"));
1749 assert!(impls[0].engine_probe.is_some());
1750 }
1751
1752 #[test]
1753 fn engine_metadata_absent_for_sw_codecs() {
1754 // SW codecs don't call the engine builders — both fields
1755 // should land as None on the resulting CodecImplementation.
1756 let mut reg = CodecRegistry::default();
1757 reg.register(
1758 CodecInfo::new(CodecId::new("flac"))
1759 .capabilities(CodecCapabilities::audio("flac_sw"))
1760 .decoder(dummy_decoder_factory),
1761 );
1762 let impls = reg.implementations(&CodecId::new("flac"));
1763 assert_eq!(impls.len(), 1);
1764 assert!(impls[0].engine_id.is_none());
1765 assert!(impls[0].engine_probe.is_none());
1766 }
1767}