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