oxideav_core/registry/codec.rs
1//! In-process codec registry.
2//!
3//! Every codec crate declares itself with one [`CodecInfo`] value —
4//! capabilities, factory functions, the container tags it claims, and
5//! (optionally) a probe function used to disambiguate genuine tag
6//! collisions. The registry stores those registrations and exposes
7//! three orthogonal lookups:
8//!
9//! - **id-keyed** — `make_decoder(params)` / `make_encoder(params)` walk
10//! the implementations registered under `params.codec_id`, filter by
11//! capability restrictions, and try them in priority order with init-
12//! time fallback.
13//! - **tag-keyed** — `resolve_tag(&ProbeContext)` walks every
14//! registration whose `tags` contains `ctx.tag`, calls each probe
15//! (treating `None` as "returns 1.0"), and returns the id with the
16//! highest resulting confidence. First-registered wins on ties.
17//! - **payload-magic-keyed** — `resolve_payload_magic(first_bytes)`
18//! prefix-matches the claimed payload magic prefixes against a
19//! stream's leading bytes; longest matching magic wins, then
20//! registration order. For containers that identify a codec by the
21//! payload itself rather than a tag (e.g. an Ogg logical stream's
22//! first packet, or raw elementary streams).
23//! - **diagnostic** — `all_implementations`, `all_tag_registrations`,
24//! `all_payload_magic_registrations`.
25//!
26//! The tag path explicitly DOES NOT short-circuit on "first claim with
27//! no probe" — every claimant is asked, so a lower-priority probed
28//! claim can out-rank a higher-priority unprobed one when the content
29//! is actually ambiguous (DIV3 XVID-with-real-MSMPEG4 payload etc.).
30
31use std::collections::HashMap;
32
33use crate::arena;
34use crate::{
35 CodecCapabilities, CodecId, CodecOptionsStruct, CodecParameters, CodecResolver, CodecTag,
36 Error, ExecutionContext, Frame, OptionField, Packet, PixelFormat, ProbeContext, ProbeFn,
37 Result,
38};
39
40// ───────────────────────── codec traits ─────────────────────────
41
42/// A packet-to-frame decoder.
43pub trait Decoder: Send {
44 /// Identifier of the codec this decoder handles.
45 fn codec_id(&self) -> &CodecId;
46
47 /// Feed one compressed packet. May or may not produce a frame immediately —
48 /// call `receive_frame` in a loop afterwards.
49 fn send_packet(&mut self, packet: &Packet) -> Result<()>;
50
51 /// Pull the next decoded frame, if any. Returns `Error::NeedMore` when the
52 /// decoder needs another packet.
53 fn receive_frame(&mut self) -> Result<Frame>;
54
55 /// Pull the next decoded frame as an arena-backed [`arena::sync::Frame`].
56 ///
57 /// Decoders that build their output through an
58 /// [`arena::sync::ArenaPool`] override this to return the pooled
59 /// [`arena::sync::Frame`] **directly**, with no per-plane memcpy
60 /// out — the caller gets true zero-copy plane access via
61 /// [`arena::sync::FrameInner::plane`].
62 ///
63 /// The default implementation delegates to [`Self::receive_frame`]
64 /// and copies the video planes into a freshly-leased one-shot
65 /// `arena::sync::ArenaPool`. This makes the method an additive
66 /// change for every existing [`Decoder`] impl: callers using the
67 /// new API still work, but pay one memcpy per plane.
68 ///
69 /// **Audio / subtitle frames:** the [`arena::sync::Frame`] body is
70 /// video-only (planes + [`arena::sync::FrameHeader`] with
71 /// width/height/pixel format). The default implementation returns
72 /// [`Error::Unsupported`] for non-video frames; an audio decoder
73 /// that wants to expose `receive_arena_frame()` must override it
74 /// with its own arena-backed audio-frame type once the framework
75 /// gains one. Until then, audio decoders should keep using
76 /// [`Self::receive_frame`].
77 fn receive_arena_frame(&mut self) -> Result<arena::sync::Frame> {
78 let frame = self.receive_frame()?;
79 match frame {
80 Frame::Video(v) => video_frame_to_arena_sync_frame(&v),
81 Frame::Audio(_) => Err(Error::unsupported(
82 "receive_arena_frame: audio frames not yet supported by default impl",
83 )),
84 Frame::Subtitle(_) => Err(Error::unsupported(
85 "receive_arena_frame: subtitle frames have no arena-backed representation",
86 )),
87 Frame::Vector(_) => Err(Error::unsupported(
88 "receive_arena_frame: vector frames have no arena-backed representation",
89 )),
90 }
91 }
92
93 /// Signal end-of-stream. After this, `receive_frame` will drain buffered
94 /// frames and eventually return `Error::Eof`.
95 fn flush(&mut self) -> Result<()>;
96
97 /// Discard all carry-over state so the decoder can resume from a new
98 /// bitstream position without producing stale output. Called by the
99 /// player after a container seek.
100 ///
101 /// Unlike [`flush`](Self::flush) (which signals end-of-stream and
102 /// drains buffered frames), `reset` is expected to:
103 /// * drop every buffered input packet and pending output frame;
104 /// * zero any per-stream filter / predictor / overlap memory so the
105 /// next `send_packet` decodes as if it were the first;
106 /// * leave the codec id and stream parameters untouched.
107 ///
108 /// The default is a conservative "drain-then-forget": call
109 /// [`flush`](Self::flush) and ignore any remaining frames. Stateful
110 /// codecs (LPC predictors, backward-adaptive gain, IMDCT overlap,
111 /// reference pictures, …) should override this to wipe their
112 /// internal state explicitly — otherwise the first ~N output
113 /// samples after a seek will be glitchy until the state re-adapts.
114 fn reset(&mut self) -> Result<()> {
115 self.flush()?;
116 // Drain any remaining output frames so the next send_packet
117 // starts clean. NeedMore / Eof both mean "no more frames"; any
118 // other error is surfaced so the caller can see why.
119 loop {
120 match self.receive_frame() {
121 Ok(_) => {}
122 Err(Error::NeedMore) | Err(Error::Eof) => return Ok(()),
123 Err(e) => return Err(e),
124 }
125 }
126 }
127
128 /// Advisory: announce the runtime environment (today: a thread budget
129 /// for codec-internal parallelism). Called at most once, before the
130 /// first `send_packet`. Default no-op; codecs that want to run
131 /// slice-/GOP-/tile-parallel override this to capture the budget.
132 /// Ignoring the hint is always safe — callers must still work with
133 /// a decoder that runs serial.
134 fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
135}
136
137/// A frame-to-packet encoder.
138pub trait Encoder: Send {
139 /// Identifier of the codec this encoder produces.
140 fn codec_id(&self) -> &CodecId;
141
142 /// Parameters describing this encoder's output stream (to feed into a muxer).
143 fn output_params(&self) -> &CodecParameters;
144
145 /// Feed one uncompressed frame. May or may not produce a packet
146 /// immediately — call `receive_packet` in a loop afterwards.
147 fn send_frame(&mut self, frame: &Frame) -> Result<()>;
148
149 /// Pull the next encoded packet, if any. Returns `Error::NeedMore`
150 /// when the encoder needs another frame (or a `flush`).
151 fn receive_packet(&mut self) -> Result<Packet>;
152
153 /// Signal end of input: drain internal lookahead so the remaining
154 /// packets become available via `receive_packet`.
155 fn flush(&mut self) -> Result<()>;
156
157 /// Advisory: announce the runtime environment. Same semantics as
158 /// [`Decoder::set_execution_context`].
159 fn set_execution_context(&mut self, _ctx: &ExecutionContext) {}
160}
161
162/// Default-impl helper for [`Decoder::receive_arena_frame`]: copy a
163/// heap-backed [`crate::VideoFrame`] into a freshly-leased
164/// [`arena::sync::Frame`].
165///
166/// Allocates a single-slot, single-arena `arena::sync::ArenaPool`
167/// sized to fit the planes verbatim. The pool is dropped at the end of
168/// this call; the returned `Frame` keeps its leased buffer alive via
169/// `Arc<FrameInner>` (the `Arena`'s `Weak` handle to the dropped pool
170/// just stops upgrading — the buffer drops normally when the last
171/// `Frame` clone goes away).
172///
173/// Width / height / pixel-format on the returned `FrameHeader` are
174/// derived from the plane shape: `width = plane[0].stride`,
175/// `height = plane[0].data.len() / stride`. Pixel format is left as
176/// [`PixelFormat::Yuv420P`] when there are 3 planes, else the first
177/// per-plane sensible default — this is a best-effort label for the
178/// generic conversion path; decoders that override
179/// `receive_arena_frame` themselves should set the correct pixel
180/// format.
181fn video_frame_to_arena_sync_frame(v: &crate::VideoFrame) -> Result<arena::sync::Frame> {
182 if v.planes.is_empty() {
183 return Err(Error::invalid(
184 "receive_arena_frame: video frame has no planes",
185 ));
186 }
187 let total_bytes: usize = v.planes.iter().map(|p| p.data.len()).sum();
188 if total_bytes == 0 {
189 return Err(Error::invalid(
190 "receive_arena_frame: video frame planes are empty",
191 ));
192 }
193 // One-shot pool sized exactly to the frame. The pool drops at end
194 // of scope; the leased Arena lives on inside the returned Frame
195 // (its Weak<ArenaPool> handle just won't upgrade in Drop, so the
196 // Box<[u8]> falls through to a normal heap free).
197 let pool = arena::sync::ArenaPool::with_alloc_count_cap(
198 1,
199 total_bytes,
200 // One alloc per plane, plus a generous safety margin.
201 (v.planes.len() as u32).saturating_add(4),
202 );
203 let arena = pool.lease()?;
204 let mut plane_offsets: Vec<(usize, usize)> = Vec::with_capacity(v.planes.len());
205 let mut cursor = 0usize;
206 for plane in &v.planes {
207 let dst = arena.alloc::<u8>(plane.data.len())?;
208 dst.copy_from_slice(&plane.data);
209 plane_offsets.push((cursor, plane.data.len()));
210 cursor += plane.data.len();
211 }
212 // Best-effort header: width = stride of plane 0, height inferred
213 // from plane 0's data length. Pixel format defaults to Yuv420P for
214 // the common 3-plane case, Gray8 for single-plane, otherwise
215 // Yuv444P. Decoders that care about exact pixel-format / width /
216 // height should override `receive_arena_frame` themselves so they
217 // can emit a correct `FrameHeader` straight from their arena
218 // build path.
219 let stride0 = v.planes[0].stride.max(1);
220 let width = stride0 as u32;
221 let height = (v.planes[0].data.len() / stride0) as u32;
222 // Count only image planes for the format guess — side-channel
223 // entries (palette, significant bits; copied verbatim above like
224 // any other plane) must not bump e.g. a single-plane palette frame
225 // out of the Gray8 label.
226 let pixel_format = match v.image_plane_count() {
227 1 => PixelFormat::Gray8,
228 3 => PixelFormat::Yuv420P,
229 _ => PixelFormat::Yuv444P,
230 };
231 let header = arena::sync::FrameHeader::new(width, height, pixel_format, v.pts);
232 arena::sync::FrameInner::new(arena, &plane_offsets, header)
233}
234
235/// Factory that builds a decoder for a given codec parameter set.
236pub type DecoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Decoder>>;
237
238/// Factory that builds an encoder for a given codec parameter set.
239pub type EncoderFactory = fn(params: &CodecParameters) -> Result<Box<dyn Encoder>>;
240
241// ───────────────────────── CodecInfo ─────────────────────────
242
243/// A single registration: capabilities, decoder/encoder factories,
244/// optional probe, and the container tags this codec claims.
245///
246/// Codec crates build one of these per codec id inside their
247/// `register(reg)` function and hand it to
248/// [`CodecRegistry::register`]. The struct is `#[non_exhaustive]` so
249/// additional fields can be added without breaking existing codec
250/// crates — construction is only possible through
251/// [`CodecInfo::new`] plus the builder methods below.
252#[non_exhaustive]
253pub struct CodecInfo {
254 /// Canonical codec identifier this entry registers.
255 pub id: CodecId,
256 /// Capability description (media kind, feature flags, priority).
257 pub capabilities: CodecCapabilities,
258 /// Factory producing a fresh decoder instance, if decode is supported.
259 pub decoder_factory: Option<DecoderFactory>,
260 /// Factory producing a fresh encoder instance, if encode is supported.
261 pub encoder_factory: Option<EncoderFactory>,
262 /// Probe function that returns a confidence in `0.0..=1.0` for a
263 /// given [`ProbeContext`]. `None` means "confidence 1.0 for every
264 /// claimed tag" — the correct default for codecs whose tag claims
265 /// are unambiguous.
266 pub probe: Option<ProbeFn>,
267 /// Tags this codec is willing to be looked up under. One codec may
268 /// claim many tags (an AAC decoder covers several WaveFormat ids,
269 /// a FourCC, an MP4 OTI, and a Matroska CodecID string at once).
270 pub tags: Vec<CodecTag>,
271 /// Payload magic prefixes this codec answers to (`\x01vorbis`,
272 /// `OpusHead`, …). Some carriage formats have no codec tag — the
273 /// codec is announced by a magic byte prefix on the payload itself
274 /// (an Ogg logical stream's first packet is the canonical case;
275 /// raw elementary streams are another). Such claims are
276 /// prefix-matched by
277 /// [`CodecRegistry::resolve_payload_magic_ref`] instead of living in
278 /// the exact-match [`CodecTag`] index. Attached with
279 /// [`Self::payload_magic`] / [`Self::payload_magics`]. Empty prefixes are
280 /// ignored at registration time (a zero-length prefix would match
281 /// every stream while carrying no evidence).
282 pub payload_magics: Vec<Vec<u8>>,
283 /// Schema of the encoder's recognised option keys
284 /// (`CodecParameters::options`). Attached with
285 /// [`Self::encoder_options`]. Used for validation / `oxideav list`
286 /// / pipeline JSON checks.
287 pub encoder_options_schema: Option<&'static [OptionField]>,
288 /// Schema of the decoder's recognised option keys.
289 pub decoder_options_schema: Option<&'static [OptionField]>,
290 /// HW backend identifier, e.g. `"nvidia"`, `"vaapi"`, `"vdpau"`,
291 /// `"vulkan-video"`, `"videotoolbox"`. Set by HW siblings on every
292 /// `CodecInfo` they register; SW codecs leave this `None`.
293 /// Consumers (e.g. the CLI's `info` command) use it to group
294 /// codec entries by backend and to dedupe probe calls — multiple
295 /// `CodecInfo` entries with the same `engine_id` typically share
296 /// an `engine_probe` function, and consumers should call the probe
297 /// at most once per `engine_id` per pass. Attached via
298 /// [`Self::with_engine_id`].
299 pub engine_id: Option<&'static str>,
300 /// Optional engine probe function. When `Some`, calling it returns
301 /// one [`crate::engine::HwDeviceInfo`] entry per device the backend
302 /// sees. Phase-2 HW siblings populate this on every `CodecInfo`
303 /// they register; Phase-3 consumers (CLI) call it on demand.
304 /// Attached via [`Self::with_engine_probe`].
305 pub engine_probe: Option<crate::engine::EngineProbeFn>,
306}
307
308impl CodecInfo {
309 /// Start a new registration for `id` with empty capabilities, no
310 /// factories, no probe, and no tags. Chain the builder methods
311 /// below to fill it in, then hand the result to
312 /// [`CodecRegistry::register`].
313 pub fn new(id: CodecId) -> Self {
314 Self {
315 capabilities: CodecCapabilities::audio(id.as_str()),
316 id,
317 decoder_factory: None,
318 encoder_factory: None,
319 probe: None,
320 tags: Vec::new(),
321 payload_magics: Vec::new(),
322 encoder_options_schema: None,
323 decoder_options_schema: None,
324 engine_id: None,
325 engine_probe: None,
326 }
327 }
328
329 /// Replace the capability description. The default built by
330 /// [`Self::new`] is a placeholder (audio-flavoured, no flags); every
331 /// real registration should call this.
332 pub fn capabilities(mut self, caps: CodecCapabilities) -> Self {
333 self.capabilities = caps;
334 self
335 }
336
337 /// Builder: attach the decoder factory.
338 pub fn decoder(mut self, factory: DecoderFactory) -> Self {
339 self.decoder_factory = Some(factory);
340 self
341 }
342
343 /// Builder: attach the encoder factory.
344 pub fn encoder(mut self, factory: EncoderFactory) -> Self {
345 self.encoder_factory = Some(factory);
346 self
347 }
348
349 /// Builder: attach a confidence probe (see [`CodecInfo::probe`]).
350 pub fn probe(mut self, probe: ProbeFn) -> Self {
351 self.probe = Some(probe);
352 self
353 }
354
355 /// Claim a single container tag for this codec. Equivalent to
356 /// `.tags([tag])` but avoids the array ceremony for single-tag
357 /// claims.
358 pub fn tag(mut self, tag: CodecTag) -> Self {
359 self.tags.push(tag);
360 self
361 }
362
363 /// Claim a set of container tags for this codec. Takes any
364 /// iterable (arrays, `Vec`, `Option`, …) so the common case of a
365 /// codec with 3-6 tags reads as one clean block.
366 pub fn tags(mut self, tags: impl IntoIterator<Item = CodecTag>) -> Self {
367 self.tags.extend(tags);
368 self
369 }
370
371 /// Claim one payload magic prefix for this codec (see
372 /// [`Self::payload_magics`]). Chain repeatedly for codecs that answer
373 /// to more than one magic:
374 ///
375 /// ```
376 /// # use oxideav_core::registry::CodecInfo;
377 /// # use oxideav_core::CodecId;
378 /// let info = CodecInfo::new(CodecId::new("vorbis")).payload_magic(b"\x01vorbis");
379 /// # let _ = info;
380 /// ```
381 pub fn payload_magic(mut self, magic: impl Into<Vec<u8>>) -> Self {
382 self.payload_magics.push(magic.into());
383 self
384 }
385
386 /// Claim a set of payload magic prefixes for this codec — the
387 /// iterable companion to [`Self::payload_magic`], mirroring the
388 /// [`Self::tag`] / [`Self::tags`] pair.
389 pub fn payload_magics<I>(mut self, magics: I) -> Self
390 where
391 I: IntoIterator,
392 I::Item: Into<Vec<u8>>,
393 {
394 self.payload_magics
395 .extend(magics.into_iter().map(Into::into));
396 self
397 }
398
399 /// Declare the options struct this codec's encoder factory expects.
400 /// Attaches `T::SCHEMA` so the registry can enumerate recognised
401 /// option keys (for `oxideav list`, pipeline JSON validation, etc.).
402 /// The factory itself still has to call
403 /// [`crate::parse_options::<T>()`] against
404 /// `CodecParameters::options` at init time.
405 pub fn encoder_options<T: CodecOptionsStruct>(mut self) -> Self {
406 self.encoder_options_schema = Some(T::SCHEMA);
407 self
408 }
409
410 /// Declare the options struct this codec's decoder factory expects.
411 /// See [`Self::encoder_options`] for the encoder counterpart.
412 pub fn decoder_options<T: CodecOptionsStruct>(mut self) -> Self {
413 self.decoder_options_schema = Some(T::SCHEMA);
414 self
415 }
416
417 /// Tag this codec as belonging to a HW backend identified by
418 /// `engine_id`. Should match the `engine_id` of every other
419 /// `CodecInfo` registered by the same backend, and the corresponding
420 /// `engine_id` field used by the CLI for grouping. SW codecs leave
421 /// this unset.
422 pub fn with_engine_id(mut self, engine_id: &'static str) -> Self {
423 self.engine_id = Some(engine_id);
424 self
425 }
426
427 /// Attach a probe function. Consumers call it to enumerate the
428 /// engines (devices) this backend can dispatch to. Probes are
429 /// expected to be idempotent and side-effect free; consumers may
430 /// call them more than once per process and should dedupe by
431 /// [`Self::engine_id`].
432 pub fn with_engine_probe(mut self, probe: crate::engine::EngineProbeFn) -> Self {
433 self.engine_probe = Some(probe);
434 self
435 }
436}
437
438/// Internal per-impl record held inside the registry's id map. Kept
439/// distinct from [`CodecInfo`] so the id map stays cheap to walk
440/// during `make_decoder` / `make_encoder` lookups.
441#[derive(Clone)]
442pub struct CodecImplementation {
443 /// Capability description copied from the originating [`CodecInfo`].
444 pub caps: CodecCapabilities,
445 /// Decoder factory, if this implementation can decode.
446 pub make_decoder: Option<DecoderFactory>,
447 /// Encoder factory, if this implementation can encode.
448 pub make_encoder: Option<EncoderFactory>,
449 /// Encoder options schema declared via
450 /// [`CodecInfo::encoder_options`]. `None` means the encoder accepts
451 /// no tuning knobs (any non-empty `CodecParameters::options` will
452 /// still be rejected by the factory if the encoder calls
453 /// `parse_options` — this is purely informational for discovery).
454 pub encoder_options_schema: Option<&'static [OptionField]>,
455 /// Decoder options schema declared via
456 /// [`CodecInfo::decoder_options`]; same semantics as the encoder
457 /// schema above.
458 pub decoder_options_schema: Option<&'static [OptionField]>,
459 /// HW backend identifier copied verbatim from the originating
460 /// [`CodecInfo::engine_id`]. `Some("nvidia"/"vaapi"/...)` on HW
461 /// backends; `None` on SW codecs. Consumers (CLI `info` command,
462 /// pipeline dispatcher, bench loop) read this to group entries by
463 /// backend without grepping `caps.implementation`.
464 pub engine_id: Option<&'static str>,
465 /// Engine probe function copied verbatim from the originating
466 /// [`CodecInfo::engine_probe`]. `Some(fn)` on HW backends with a
467 /// probe wired; `None` on SW codecs. Consumers call it on demand
468 /// to enumerate per-device info ([`crate::engine::HwDeviceInfo`]).
469 pub engine_probe: Option<crate::engine::EngineProbeFn>,
470}
471
472/// Registry mapping codec ids and container tags to their registered
473/// implementations; the lookup point behind `make_decoder` /
474/// `make_encoder` / `resolve_tag`.
475#[derive(Default)]
476pub struct CodecRegistry {
477 /// id → list of implementations. Each registered codec appends one
478 /// entry here. `make_decoder` / `make_encoder` walk this list in
479 /// preference order.
480 impls: HashMap<CodecId, Vec<CodecImplementation>>,
481 /// Append-only list of every registration — the `tag_index` stores
482 /// offsets into this vector.
483 registrations: Vec<RegistrationRecord>,
484 /// Tag → indices into `registrations`. Indices are stored in
485 /// registration order so tie-breaking in `resolve_tag` is
486 /// deterministic (first-registered wins).
487 tag_index: HashMap<CodecTag, Vec<usize>>,
488 /// Payload magic-prefix claims: `(magic, registration index)` in
489 /// registration order. Kept as a flat list rather than a map
490 /// because resolution is prefix matching (see
491 /// [`Self::resolve_payload_magic_ref`]), not exact-key lookup.
492 magic_index: Vec<(Vec<u8>, usize)>,
493}
494
495/// Internal registry record. Mirrors the subset of [`CodecInfo`]
496/// needed at resolve time.
497struct RegistrationRecord {
498 id: CodecId,
499 probe: Option<ProbeFn>,
500}
501
502impl CodecRegistry {
503 /// An empty registry (same as `Default`).
504 pub fn new() -> Self {
505 Self::default()
506 }
507
508 /// Register one codec. Expands into:
509 /// * an entry in the id → implementations map (for
510 /// `make_decoder` / `make_encoder`);
511 /// * an entry in the tag index for every claimed tag (for
512 /// `resolve_tag`).
513 ///
514 /// Calling `register` multiple times with the same id is allowed
515 /// and how multi-implementation codecs (software-plus-hardware
516 /// FLAC, for example) are expressed.
517 pub fn register(&mut self, info: CodecInfo) {
518 let CodecInfo {
519 id,
520 capabilities,
521 decoder_factory,
522 encoder_factory,
523 probe,
524 tags,
525 payload_magics,
526 encoder_options_schema,
527 decoder_options_schema,
528 // engine_id / engine_probe are metadata attached to a
529 // CodecInfo for backends that want consumers (CLI `info`,
530 // pipeline bench) to enumerate the underlying devices on
531 // demand. They're surfaced verbatim on the resulting
532 // CodecImplementation so consumers can read them without
533 // grepping `caps.implementation`. Tag-only CodecInfo entries
534 // (no factories) drop the values on the floor — there's no
535 // CodecImplementation built in that branch.
536 engine_id,
537 engine_probe,
538 } = info;
539
540 let caps = {
541 let mut c = capabilities;
542 if decoder_factory.is_some() {
543 c = c.with_decode();
544 }
545 if encoder_factory.is_some() {
546 c = c.with_encode();
547 }
548 c
549 };
550
551 // Only record an implementation entry when at least one factory
552 // is present. A "tag-only" CodecInfo — used to attach extra tag
553 // claims to a codec that was already registered with factories —
554 // shouldn't pollute the impl list.
555 if decoder_factory.is_some() || encoder_factory.is_some() {
556 self.impls
557 .entry(id.clone())
558 .or_default()
559 .push(CodecImplementation {
560 caps,
561 make_decoder: decoder_factory,
562 make_encoder: encoder_factory,
563 encoder_options_schema,
564 decoder_options_schema,
565 engine_id,
566 engine_probe,
567 });
568 }
569
570 let record_idx = self.registrations.len();
571 self.registrations.push(RegistrationRecord {
572 id: id.clone(),
573 probe,
574 });
575 for tag in tags {
576 self.tag_index.entry(tag).or_default().push(record_idx);
577 }
578 for magic in payload_magics {
579 // A zero-length prefix would match every stream while
580 // carrying no evidence — drop it here so resolution never
581 // has to special-case it.
582 if !magic.is_empty() {
583 self.magic_index.push((magic, record_idx));
584 }
585 }
586 }
587
588 /// Whether at least one registered implementation of `id` can decode.
589 pub fn has_decoder(&self, id: &CodecId) -> bool {
590 self.impls
591 .get(id)
592 .map(|v| v.iter().any(|i| i.make_decoder.is_some()))
593 .unwrap_or(false)
594 }
595
596 /// Whether at least one registered implementation of `id` can encode.
597 pub fn has_encoder(&self, id: &CodecId) -> bool {
598 self.impls
599 .get(id)
600 .map(|v| v.iter().any(|i| i.make_encoder.is_some()))
601 .unwrap_or(false)
602 }
603
604 /// First registered decoder factory for `params.codec_id`, invoked
605 /// with `params`. No priority walk, no preference filter, no
606 /// init-time fallback to a lower-priority impl. Errors if no
607 /// decoder is registered for the codec.
608 ///
609 /// Intended for single-impl scenarios — typically a codec crate's
610 /// own self-tests, where exactly one impl has been registered into
611 /// a freshly-constructed registry. Production callers selecting
612 /// among multiple candidates (e.g. h264_sw vs h264_videotoolbox)
613 /// should use `oxideav_pipeline::make_decoder_with` instead, which
614 /// applies `CodecPreferences` and walks priorities.
615 pub fn first_decoder(&self, params: &CodecParameters) -> Result<Box<dyn Decoder>> {
616 let imp = self
617 .implementations(¶ms.codec_id)
618 .iter()
619 .find(|i| i.make_decoder.is_some())
620 .ok_or_else(|| {
621 Error::CodecNotFound(format!("no decoder for codec {}", params.codec_id))
622 })?;
623 (imp.make_decoder.expect("checked above"))(params)
624 }
625
626 /// First registered encoder factory — see [`first_decoder`].
627 ///
628 /// [`first_decoder`]: Self::first_decoder
629 pub fn first_encoder(&self, params: &CodecParameters) -> Result<Box<dyn Encoder>> {
630 let imp = self
631 .implementations(¶ms.codec_id)
632 .iter()
633 .find(|i| i.make_encoder.is_some())
634 .ok_or_else(|| {
635 Error::CodecNotFound(format!("no encoder for codec {}", params.codec_id))
636 })?;
637 (imp.make_encoder.expect("checked above"))(params)
638 }
639
640 /// Look up a decoder by exact implementation name
641 /// (`"h264_sw"`, `"aac_audiotoolbox"`, ...). Errors if the impl
642 /// isn't registered or if it has no decoder factory.
643 pub fn decoder_by_impl(
644 &self,
645 impl_name: &str,
646 params: &CodecParameters,
647 ) -> Result<Box<dyn Decoder>> {
648 let imp = self
649 .implementations(¶ms.codec_id)
650 .iter()
651 .find(|i| i.caps.implementation == impl_name)
652 .ok_or_else(|| {
653 Error::CodecNotFound(format!(
654 "no implementation `{impl_name}` for codec {}",
655 params.codec_id
656 ))
657 })?;
658 let factory = imp
659 .make_decoder
660 .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is encoder-only")))?;
661 factory(params)
662 }
663
664 /// Look up an encoder by exact implementation name — see
665 /// [`decoder_by_impl`].
666 ///
667 /// [`decoder_by_impl`]: Self::decoder_by_impl
668 pub fn encoder_by_impl(
669 &self,
670 impl_name: &str,
671 params: &CodecParameters,
672 ) -> Result<Box<dyn Encoder>> {
673 let imp = self
674 .implementations(¶ms.codec_id)
675 .iter()
676 .find(|i| i.caps.implementation == impl_name)
677 .ok_or_else(|| {
678 Error::CodecNotFound(format!(
679 "no implementation `{impl_name}` for codec {}",
680 params.codec_id
681 ))
682 })?;
683 let factory = imp
684 .make_encoder
685 .ok_or_else(|| Error::CodecNotFound(format!("`{impl_name}` is decoder-only")))?;
686 factory(params)
687 }
688
689 /// Iterate codec ids that have at least one decoder implementation.
690 pub fn decoder_ids(&self) -> impl Iterator<Item = &CodecId> {
691 self.impls
692 .iter()
693 .filter(|(_, v)| v.iter().any(|i| i.make_decoder.is_some()))
694 .map(|(id, _)| id)
695 }
696
697 /// Iterate codec ids that have at least one encoder implementation.
698 pub fn encoder_ids(&self) -> impl Iterator<Item = &CodecId> {
699 self.impls
700 .iter()
701 .filter(|(_, v)| v.iter().any(|i| i.make_encoder.is_some()))
702 .map(|(id, _)| id)
703 }
704
705 /// All registered implementations of a given codec id.
706 pub fn implementations(&self, id: &CodecId) -> &[CodecImplementation] {
707 self.impls.get(id).map(|v| v.as_slice()).unwrap_or(&[])
708 }
709
710 /// Lookup the encoder options schema for a registered codec. Walks
711 /// implementations in registration order and returns the first
712 /// schema found. `None` means either the codec isn't registered or
713 /// no implementation declared an encoder schema.
714 pub fn encoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
715 self.impls
716 .get(id)?
717 .iter()
718 .find_map(|i| i.encoder_options_schema)
719 }
720
721 /// Lookup the decoder options schema — see
722 /// [`encoder_options_schema`](Self::encoder_options_schema).
723 pub fn decoder_options_schema(&self, id: &CodecId) -> Option<&'static [OptionField]> {
724 self.impls
725 .get(id)?
726 .iter()
727 .find_map(|i| i.decoder_options_schema)
728 }
729
730 /// Iterator over every (codec_id, impl) pair — useful for `oxideav list`
731 /// to show capability flags per implementation.
732 pub fn all_implementations(&self) -> impl Iterator<Item = (&CodecId, &CodecImplementation)> {
733 self.impls
734 .iter()
735 .flat_map(|(id, v)| v.iter().map(move |i| (id, i)))
736 }
737
738 /// Iterator over every `(tag, codec_id)` pair currently registered —
739 /// used by `oxideav tags` debug output and by tests that want to
740 /// walk the tag surface.
741 pub fn all_tag_registrations(&self) -> impl Iterator<Item = (&CodecTag, &CodecId)> {
742 self.tag_index.iter().flat_map(move |(tag, idxs)| {
743 idxs.iter().map(move |&i| (tag, &self.registrations[i].id))
744 })
745 }
746
747 /// Inherent form of tag resolution that returns a reference.
748 /// The owned-value form used by container code lives behind the
749 /// [`CodecResolver`] trait impl below.
750 ///
751 /// Walks every registration that claimed `ctx.tag`, calls its
752 /// probe with `ctx`, and returns the id of the registration that
753 /// scored highest. Probes that return `0.0` are discarded; ties
754 /// on confidence are broken by registration order (first wins).
755 /// Registrations with no probe are treated as returning `1.0`.
756 pub fn resolve_tag_ref(&self, ctx: &ProbeContext) -> Option<&CodecId> {
757 let idxs = self.tag_index.get(ctx.tag)?;
758 let mut best: Option<(f32, usize)> = None;
759 for &i in idxs {
760 let rec = &self.registrations[i];
761 let conf = match rec.probe {
762 Some(f) => f(ctx),
763 None => 1.0,
764 };
765 if conf <= 0.0 {
766 continue;
767 }
768 best = match best {
769 None => Some((conf, i)),
770 Some((bc, _)) if conf > bc => Some((conf, i)),
771 other => other,
772 };
773 }
774 best.map(|(_, i)| &self.registrations[i].id)
775 }
776
777 /// Inherent form of payload-magic resolution that returns a
778 /// reference. The owned-value form used by container code lives
779 /// behind the [`CodecResolver`] trait impl below.
780 ///
781 /// Walks every registered payload magic prefix (declared via
782 /// [`CodecInfo::payload_magic`] / [`CodecInfo::payload_magics`]) and
783 /// returns the codec whose magic is a prefix of `first_bytes` —
784 /// however much of the stream's leading payload the caller has
785 /// (an Ogg demuxer passes the first packet of a logical stream; a
786 /// raw-stream prober passes the file head). The **longest**
787 /// matching magic wins (most specific claim); remaining ties are
788 /// broken by registration order (first wins). Unlike the tag path
789 /// there is no probe step: a payload magic is itself the bitstream
790 /// evidence a probe would look for, and specificity is expressed
791 /// by prefix length instead of a confidence value.
792 pub fn resolve_payload_magic_ref(&self, first_bytes: &[u8]) -> Option<&CodecId> {
793 let mut best: Option<(usize, usize)> = None; // (magic_len, reg idx)
794 for (magic, idx) in &self.magic_index {
795 if !first_bytes.starts_with(magic) {
796 continue;
797 }
798 // Strict `>` keeps the earlier registration on equal
799 // lengths — `magic_index` is in registration order.
800 best = match best {
801 None => Some((magic.len(), *idx)),
802 Some((len, _)) if magic.len() > len => Some((magic.len(), *idx)),
803 other => other,
804 };
805 }
806 best.map(|(_, i)| &self.registrations[i].id)
807 }
808
809 /// Iterator over every `(payload magic, codec_id)` pair currently
810 /// registered, in registration order — the payload-magic companion
811 /// to [`all_tag_registrations`](Self::all_tag_registrations).
812 pub fn all_payload_magic_registrations(&self) -> impl Iterator<Item = (&[u8], &CodecId)> {
813 self.magic_index
814 .iter()
815 .map(move |(magic, i)| (magic.as_slice(), &self.registrations[*i].id))
816 }
817}
818
819/// Implement the shared [`CodecResolver`] interface so container
820/// demuxers can accept `&dyn CodecResolver` without depending on
821/// this crate directly — the trait lives in oxideav-core.
822impl CodecResolver for CodecRegistry {
823 fn resolve_tag(&self, ctx: &ProbeContext) -> Option<CodecId> {
824 self.resolve_tag_ref(ctx).cloned()
825 }
826
827 fn resolve_payload_magic(&self, first_packet: &[u8]) -> Option<CodecId> {
828 self.resolve_payload_magic_ref(first_packet).cloned()
829 }
830}
831
832#[cfg(test)]
833mod tag_tests {
834 use super::*;
835 use crate::CodecCapabilities;
836
837 /// Probe: return 1.0 iff the peeked bytes look like MS-MPEG4 (no
838 /// 0x000001 start code in the first few bytes).
839 fn probe_msmpeg4(ctx: &ProbeContext) -> f32 {
840 match ctx.packet {
841 Some(d) if !d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
842 Some(_) => 0.0,
843 None => 0.5, // no data yet — weak evidence
844 }
845 }
846
847 /// Probe: return 1.0 iff the peeked bytes look like MPEG-4 Part 2
848 /// (starts with a 0x000001 start code in the first few bytes).
849 fn probe_mpeg4_part2(ctx: &ProbeContext) -> f32 {
850 match ctx.packet {
851 Some(d) if d.windows(3).take(6).any(|w| w == [0x00, 0x00, 0x01]) => 1.0,
852 Some(_) => 0.0,
853 None => 0.5,
854 }
855 }
856
857 fn info(id: &str) -> CodecInfo {
858 CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
859 }
860
861 #[test]
862 fn resolve_single_claim_no_probe() {
863 let mut reg = CodecRegistry::new();
864 reg.register(info("flac").tag(CodecTag::fourcc(b"FLAC")));
865 let t = CodecTag::fourcc(b"FLAC");
866 assert_eq!(
867 reg.resolve_tag_ref(&ProbeContext::new(&t))
868 .map(|c| c.as_str()),
869 Some("flac"),
870 );
871 }
872
873 #[test]
874 fn resolve_missing_tag_returns_none() {
875 let reg = CodecRegistry::new();
876 let t = CodecTag::fourcc(b"????");
877 assert!(reg.resolve_tag_ref(&ProbeContext::new(&t)).is_none());
878 }
879
880 #[test]
881 fn unprobed_claims_tie_first_registered_wins() {
882 // Two unprobed claims on the same tag: deterministic order.
883 let mut reg = CodecRegistry::new();
884 reg.register(info("first").tag(CodecTag::fourcc(b"TEST")));
885 reg.register(info("second").tag(CodecTag::fourcc(b"TEST")));
886 let t = CodecTag::fourcc(b"TEST");
887 assert_eq!(
888 reg.resolve_tag_ref(&ProbeContext::new(&t))
889 .map(|c| c.as_str()),
890 Some("first"),
891 );
892 }
893
894 #[test]
895 fn probe_picks_matching_bitstream() {
896 // The core bug fix: every probe is asked and the highest
897 // confidence wins regardless of registration order.
898 let mut reg = CodecRegistry::new();
899 reg.register(
900 info("msmpeg4v3")
901 .probe(probe_msmpeg4)
902 .tag(CodecTag::fourcc(b"DIV3")),
903 );
904 reg.register(
905 info("mpeg4video")
906 .probe(probe_mpeg4_part2)
907 .tag(CodecTag::fourcc(b"DIV3")),
908 );
909
910 let mpeg4_part2 = [0x00u8, 0x00, 0x01, 0xB0, 0x01, 0x00];
911 let ms_mpeg4 = [0x85u8, 0x3F, 0xD4, 0x80, 0x00, 0xA2];
912 let tag = CodecTag::fourcc(b"DIV3");
913
914 let ctx_part2 = ProbeContext::new(&tag).packet(&mpeg4_part2);
915 assert_eq!(
916 reg.resolve_tag_ref(&ctx_part2).map(|c| c.as_str()),
917 Some("mpeg4video"),
918 );
919 let ctx_ms = ProbeContext::new(&tag).packet(&ms_mpeg4);
920 assert_eq!(
921 reg.resolve_tag_ref(&ctx_ms).map(|c| c.as_str()),
922 Some("msmpeg4v3"),
923 );
924 }
925
926 #[test]
927 fn unprobed_claim_wins_against_low_confidence_probe() {
928 // One codec claims a tag without a probe (→ confidence 1.0)
929 // and another claims it with a probe returning 0.3. The
930 // unprobed one wins — a codec that knows it owns the tag
931 // outright should not lose to a speculative probe.
932 let mut reg = CodecRegistry::new();
933 reg.register(info("owner").tag(CodecTag::fourcc(b"OWN_")));
934 reg.register(
935 info("speculative")
936 .probe(|_| 0.3)
937 .tag(CodecTag::fourcc(b"OWN_")),
938 );
939 let t = CodecTag::fourcc(b"OWN_");
940 assert_eq!(
941 reg.resolve_tag_ref(&ProbeContext::new(&t))
942 .map(|c| c.as_str()),
943 Some("owner"),
944 );
945 }
946
947 #[test]
948 fn probe_returning_zero_is_skipped() {
949 let mut reg = CodecRegistry::new();
950 reg.register(
951 info("refuses")
952 .probe(|_| 0.0)
953 .tag(CodecTag::fourcc(b"MAYB")),
954 );
955 reg.register(info("fallback").tag(CodecTag::fourcc(b"MAYB")));
956 let t = CodecTag::fourcc(b"MAYB");
957 let ctx = ProbeContext::new(&t).packet(b"hello");
958 assert_eq!(
959 reg.resolve_tag_ref(&ctx).map(|c| c.as_str()),
960 Some("fallback"),
961 );
962 }
963
964 #[test]
965 fn fourcc_case_insensitive_lookup() {
966 let mut reg = CodecRegistry::new();
967 reg.register(info("vid").tag(CodecTag::fourcc(b"div3")));
968 // Registered as "DIV3" (uppercase via ctor); lookup using
969 // lowercase / mixed case also hits.
970 let upper = CodecTag::fourcc(b"DIV3");
971 let lower = CodecTag::fourcc(b"div3");
972 let mixed = CodecTag::fourcc(b"DiV3");
973 assert!(reg.resolve_tag_ref(&ProbeContext::new(&upper)).is_some());
974 assert!(reg.resolve_tag_ref(&ProbeContext::new(&lower)).is_some());
975 assert!(reg.resolve_tag_ref(&ProbeContext::new(&mixed)).is_some());
976 }
977
978 #[test]
979 fn wave_format_and_matroska_tags_work() {
980 let mut reg = CodecRegistry::new();
981 reg.register(info("mp3").tag(CodecTag::wave_format(0x0055)));
982 reg.register(info("h264").tag(CodecTag::matroska("V_MPEG4/ISO/AVC")));
983 let wf = CodecTag::wave_format(0x0055);
984 let mk = CodecTag::matroska("V_MPEG4/ISO/AVC");
985 assert_eq!(
986 reg.resolve_tag_ref(&ProbeContext::new(&wf))
987 .map(|c| c.as_str()),
988 Some("mp3"),
989 );
990 assert_eq!(
991 reg.resolve_tag_ref(&ProbeContext::new(&mk))
992 .map(|c| c.as_str()),
993 Some("h264"),
994 );
995 }
996
997 #[test]
998 fn mp4_object_type_tag_works() {
999 let mut reg = CodecRegistry::new();
1000 reg.register(info("aac").tag(CodecTag::mp4_object_type(0x40)));
1001 let t = CodecTag::mp4_object_type(0x40);
1002 assert_eq!(
1003 reg.resolve_tag_ref(&ProbeContext::new(&t))
1004 .map(|c| c.as_str()),
1005 Some("aac"),
1006 );
1007 }
1008
1009 #[test]
1010 fn multi_tag_claim_all_resolve() {
1011 let mut reg = CodecRegistry::new();
1012 reg.register(info("aac").tags([
1013 CodecTag::fourcc(b"MP4A"),
1014 CodecTag::wave_format(0x00FF),
1015 CodecTag::mp4_object_type(0x40),
1016 CodecTag::matroska("A_AAC"),
1017 ]));
1018 for t in [
1019 CodecTag::fourcc(b"MP4A"),
1020 CodecTag::wave_format(0x00FF),
1021 CodecTag::mp4_object_type(0x40),
1022 CodecTag::matroska("A_AAC"),
1023 ] {
1024 assert_eq!(
1025 reg.resolve_tag_ref(&ProbeContext::new(&t))
1026 .map(|c| c.as_str()),
1027 Some("aac"),
1028 "tag {t:?} did not resolve",
1029 );
1030 }
1031 }
1032}
1033
1034#[cfg(test)]
1035mod payload_magic_tests {
1036 use super::*;
1037 use crate::CodecCapabilities;
1038
1039 fn info(id: &str) -> CodecInfo {
1040 CodecInfo::new(CodecId::new(id)).capabilities(CodecCapabilities::audio(id))
1041 }
1042
1043 /// Registry with the classic Ogg family registered, each under its
1044 /// real BOS magic.
1045 fn ogg_family_registry() -> CodecRegistry {
1046 let mut reg = CodecRegistry::new();
1047 reg.register(info("vorbis").payload_magic(b"\x01vorbis"));
1048 reg.register(info("opus").payload_magic(b"OpusHead"));
1049 reg.register(info("theora").payload_magic(b"\x80theora"));
1050 reg.register(info("flac").payload_magic(b"\x7fFLAC"));
1051 reg
1052 }
1053
1054 #[test]
1055 fn resolve_payload_magic_matches_first_packet_prefix() {
1056 let reg = ogg_family_registry();
1057 // A Vorbis identification header: magic + version + channels +
1058 // rate + ... — the resolver only needs the prefix to match.
1059 let vorbis_id_header = b"\x01vorbis\x00\x00\x00\x00\x02\x44\xac\x00\x00";
1060 assert_eq!(
1061 reg.resolve_payload_magic_ref(vorbis_id_header)
1062 .map(|c| c.as_str()),
1063 Some("vorbis"),
1064 );
1065 assert_eq!(
1066 reg.resolve_payload_magic_ref(b"OpusHead\x01\x02\x38\x01")
1067 .map(|c| c.as_str()),
1068 Some("opus"),
1069 );
1070 assert_eq!(
1071 reg.resolve_payload_magic_ref(b"\x80theora\x03\x02\x01")
1072 .map(|c| c.as_str()),
1073 Some("theora"),
1074 );
1075 assert_eq!(
1076 reg.resolve_payload_magic_ref(b"\x7fFLAC\x01\x00")
1077 .map(|c| c.as_str()),
1078 Some("flac"),
1079 );
1080 }
1081
1082 #[test]
1083 fn resolve_payload_magic_exact_length_packet_matches() {
1084 // A packet that is exactly the magic (nothing after it) still
1085 // resolves — starts_with is inclusive of equality.
1086 let reg = ogg_family_registry();
1087 assert_eq!(
1088 reg.resolve_payload_magic_ref(b"OpusHead")
1089 .map(|c| c.as_str()),
1090 Some("opus"),
1091 );
1092 }
1093
1094 #[test]
1095 fn resolve_payload_magic_unknown_or_short_packet_is_none() {
1096 let reg = ogg_family_registry();
1097 // Unknown magic.
1098 assert!(reg.resolve_payload_magic_ref(b"Speex 1.2.0").is_none());
1099 // Packet shorter than every registered magic.
1100 assert!(reg.resolve_payload_magic_ref(b"Opus").is_none());
1101 // Empty packet.
1102 assert!(reg.resolve_payload_magic_ref(b"").is_none());
1103 }
1104
1105 #[test]
1106 fn resolve_payload_magic_longest_prefix_wins_regardless_of_order() {
1107 // A shorter magic that is itself a prefix of a longer one must
1108 // lose to the more specific claim, whichever registered first.
1109 let mut reg = CodecRegistry::new();
1110 reg.register(info("generic").payload_magic(b"Opus"));
1111 reg.register(info("opus").payload_magic(b"OpusHead"));
1112 assert_eq!(
1113 reg.resolve_payload_magic_ref(b"OpusHead\x01")
1114 .map(|c| c.as_str()),
1115 Some("opus"),
1116 );
1117 // ...but the shorter claim still wins packets only it matches.
1118 assert_eq!(
1119 reg.resolve_payload_magic_ref(b"OpusTags")
1120 .map(|c| c.as_str()),
1121 Some("generic"),
1122 );
1123
1124 // Same result with the registration order flipped.
1125 let mut reg = CodecRegistry::new();
1126 reg.register(info("opus").payload_magic(b"OpusHead"));
1127 reg.register(info("generic").payload_magic(b"Opus"));
1128 assert_eq!(
1129 reg.resolve_payload_magic_ref(b"OpusHead\x01")
1130 .map(|c| c.as_str()),
1131 Some("opus"),
1132 );
1133 }
1134
1135 #[test]
1136 fn resolve_payload_magic_equal_length_tie_first_registered_wins() {
1137 let mut reg = CodecRegistry::new();
1138 reg.register(info("first").payload_magic(b"SameMagic"));
1139 reg.register(info("second").payload_magic(b"SameMagic"));
1140 assert_eq!(
1141 reg.resolve_payload_magic_ref(b"SameMagic\x00")
1142 .map(|c| c.as_str()),
1143 Some("first"),
1144 );
1145 }
1146
1147 #[test]
1148 fn empty_payload_magic_is_ignored_at_registration() {
1149 let mut reg = CodecRegistry::new();
1150 reg.register(info("greedy").payload_magic(b""));
1151 assert!(reg.resolve_payload_magic_ref(b"anything at all").is_none());
1152 assert_eq!(reg.all_payload_magic_registrations().count(), 0);
1153 }
1154
1155 #[test]
1156 fn payload_magics_plural_builder_and_diagnostics() {
1157 // One codec answering to several magics via the iterable
1158 // builder; the diagnostic iterator surfaces each claim in
1159 // registration order.
1160 let mut reg = CodecRegistry::new();
1161 reg.register(info("speex").payload_magics([b"Speex ".to_vec(), b"speex-alt".to_vec()]));
1162 assert_eq!(
1163 reg.resolve_payload_magic_ref(b"Speex 1.2")
1164 .map(|c| c.as_str()),
1165 Some("speex"),
1166 );
1167 assert_eq!(
1168 reg.resolve_payload_magic_ref(b"speex-alt\x00")
1169 .map(|c| c.as_str()),
1170 Some("speex"),
1171 );
1172 let all: Vec<(&[u8], &str)> = reg
1173 .all_payload_magic_registrations()
1174 .map(|(m, id)| (m, id.as_str()))
1175 .collect();
1176 assert_eq!(
1177 all,
1178 vec![
1179 (b"Speex ".as_slice(), "speex"),
1180 (b"speex-alt".as_slice(), "speex"),
1181 ],
1182 );
1183 }
1184
1185 #[test]
1186 fn magic_claims_compose_with_tag_claims_on_one_registration() {
1187 // A codec that lives in both Ogg and Matroska declares both
1188 // claim kinds on one CodecInfo; each resolution path finds it.
1189 let mut reg = CodecRegistry::new();
1190 reg.register(
1191 info("vorbis")
1192 .tag(CodecTag::matroska("A_VORBIS"))
1193 .payload_magic(b"\x01vorbis"),
1194 );
1195 let mk = CodecTag::matroska("A_VORBIS");
1196 assert_eq!(
1197 reg.resolve_tag_ref(&ProbeContext::new(&mk))
1198 .map(|c| c.as_str()),
1199 Some("vorbis"),
1200 );
1201 assert_eq!(
1202 reg.resolve_payload_magic_ref(b"\x01vorbis\x00")
1203 .map(|c| c.as_str()),
1204 Some("vorbis"),
1205 );
1206 }
1207
1208 #[test]
1209 fn resolver_trait_payload_magic_surface() {
1210 // The owned-value trait form mirrors the inherent form, and
1211 // the default implementation (NullCodecResolver) resolves
1212 // nothing.
1213 let reg = ogg_family_registry();
1214 let resolver: &dyn CodecResolver = ®
1215 assert_eq!(
1216 resolver
1217 .resolve_payload_magic(b"OpusHead\x01")
1218 .map(|c| c.0.clone()),
1219 Some("opus".to_owned()),
1220 );
1221 assert!(resolver.resolve_payload_magic(b"unknown").is_none());
1222
1223 let null = crate::NullCodecResolver;
1224 assert!(null.resolve_payload_magic(b"OpusHead\x01").is_none());
1225 }
1226
1227 /// The surface is container-agnostic: a raw elementary stream
1228 /// identified by a file-head magic resolves through the same path
1229 /// as the Ogg family — nothing about the mechanism is Ogg-shaped.
1230 #[test]
1231 fn payload_magic_serves_non_ogg_carriage() {
1232 let mut reg = CodecRegistry::new();
1233 reg.register(info("flac").payload_magic(b"fLaC"));
1234 reg.register(info("shorten").payload_magic(b"ajkg"));
1235
1236 assert_eq!(
1237 reg.resolve_payload_magic_ref(b"fLaC\x00\x00\x00\x22"),
1238 Some(&CodecId::new("flac"))
1239 );
1240 assert_eq!(
1241 reg.resolve_payload_magic_ref(b"ajkg\x02"),
1242 Some(&CodecId::new("shorten"))
1243 );
1244 assert_eq!(reg.resolve_payload_magic_ref(b"RIFF"), None);
1245 }
1246}
1247
1248#[cfg(test)]
1249mod engine_tests {
1250 use super::*;
1251 use crate::engine::HwDeviceInfo;
1252
1253 #[test]
1254 fn codec_info_engine_id_and_probe_default_to_none() {
1255 let ci = CodecInfo::new(CodecId::new("h264"));
1256 assert!(ci.engine_id.is_none());
1257 assert!(ci.engine_probe.is_none());
1258 }
1259
1260 #[test]
1261 fn codec_info_engine_builder_methods_set_fields() {
1262 fn dummy_probe() -> Vec<HwDeviceInfo> {
1263 vec![]
1264 }
1265 let ci = CodecInfo::new(CodecId::new("h264"))
1266 .with_engine_id("nvidia")
1267 .with_engine_probe(dummy_probe);
1268 assert_eq!(ci.engine_id, Some("nvidia"));
1269 assert!(ci.engine_probe.is_some());
1270 let probe = ci.engine_probe.unwrap();
1271 let result = probe();
1272 assert!(result.is_empty());
1273 }
1274
1275 #[test]
1276 fn registering_codec_with_engine_metadata_does_not_panic() {
1277 // The new fields are passthrough metadata — register() should
1278 // accept them without affecting existing id/tag bookkeeping.
1279 fn dummy_probe() -> Vec<HwDeviceInfo> {
1280 vec![]
1281 }
1282 let mut reg = CodecRegistry::new();
1283 reg.register(
1284 CodecInfo::new(CodecId::new("h264"))
1285 .capabilities(CodecCapabilities::audio("h264_nvdec"))
1286 .tag(CodecTag::fourcc(b"H264"))
1287 .with_engine_id("nvidia")
1288 .with_engine_probe(dummy_probe),
1289 );
1290 let t = CodecTag::fourcc(b"H264");
1291 assert_eq!(
1292 reg.resolve_tag_ref(&ProbeContext::new(&t))
1293 .map(|c| c.as_str()),
1294 Some("h264"),
1295 );
1296 }
1297
1298 /// No-op decoder factory so the registration produces a real
1299 /// CodecImplementation (the registry skips tag-only entries —
1300 /// without a factory there'd be nothing in `implementations()`
1301 /// to assert against).
1302 fn dummy_decoder_factory(
1303 _params: &crate::CodecParameters,
1304 ) -> crate::Result<Box<dyn super::Decoder>> {
1305 Err(crate::Error::unsupported("dummy decoder"))
1306 }
1307
1308 #[test]
1309 fn engine_metadata_propagates_through_register() {
1310 fn dummy_probe() -> Vec<HwDeviceInfo> {
1311 vec![]
1312 }
1313 let mut reg = CodecRegistry::default();
1314 reg.register(
1315 CodecInfo::new(CodecId::new("h264"))
1316 .capabilities(CodecCapabilities::video("h264_test"))
1317 .decoder(dummy_decoder_factory)
1318 .with_engine_id("test-backend")
1319 .with_engine_probe(dummy_probe),
1320 );
1321 let impls = reg.implementations(&CodecId::new("h264"));
1322 assert_eq!(impls.len(), 1);
1323 assert_eq!(impls[0].engine_id, Some("test-backend"));
1324 assert!(impls[0].engine_probe.is_some());
1325 }
1326
1327 #[test]
1328 fn engine_metadata_absent_for_sw_codecs() {
1329 // SW codecs don't call the engine builders — both fields
1330 // should land as None on the resulting CodecImplementation.
1331 let mut reg = CodecRegistry::default();
1332 reg.register(
1333 CodecInfo::new(CodecId::new("flac"))
1334 .capabilities(CodecCapabilities::audio("flac_sw"))
1335 .decoder(dummy_decoder_factory),
1336 );
1337 let impls = reg.implementations(&CodecId::new("flac"));
1338 assert_eq!(impls.len(), 1);
1339 assert!(impls[0].engine_id.is_none());
1340 assert!(impls[0].engine_probe.is_none());
1341 }
1342}