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