Skip to main content

oxideav_avif/
lib.rs

1//! AVIF (AV1 Image File Format) — the AV1 profile of HEIF / MIAF, with
2//! the ISOBMFF / HEIF container served by [`oxideav_heif`] and AV1
3//! pixel decode delegated to [`oxideav_av1`].
4//!
5//! # The image-crate API
6//!
7//! The root follows the workspace image-crate contract (see [`api`]):
8//! [`probe`] / [`info`] standalone; [`decode`] / [`decode_with`] /
9//! [`decode_rgb8`] / [`decode_rgba8`] / [`decode_all`] / [`decode_from`]
10//! and [`encode`] / [`encode_rgb8`] / [`encode_rgba8`] / [`encode_to`]
11//! with the default-on `registry` feature (the pixels come from the
12//! AV1 codec crate, which is framework-only). [`AvifImage`] is the
13//! decoded picture — planes + [`PixelFormat`] + [`ColorInfo`] +
14//! [`Metadata`] — with [`AvifImage::to_rgb8`] / [`AvifImage::to_rgba8`];
15//! [`AvifFile`] is the parsed container ([`parse`]).
16//!
17//! The one-screen example needs the default `registry` feature (the
18//! `decode*` / `encode*` paths go through the AV1 codec crate):
19//!
20#![cfg_attr(feature = "registry", doc = "```no_run")]
21#![cfg_attr(not(feature = "registry"), doc = "```ignore")]
22//! # fn main() -> Result<(), oxideav_avif::Error> {
23//! let bytes = std::fs::read("in.avif").map_err(oxideav_avif::Error::from)?;
24//! if oxideav_avif::probe(&bytes) {
25//!     let info = oxideav_avif::info(&bytes)?;
26//!     let img = oxideav_avif::decode(&bytes)?;
27//!     let rgba: Vec<u8> = img.to_rgba8();
28//!     let out = oxideav_avif::encode_rgba8(
29//!         img.width(),
30//!         img.height(),
31//!         &rgba,
32//!         &oxideav_avif::EncodeOptions::default().with_quality(90),
33//!     )?;
34//!     assert_eq!((info.width, info.height), (img.width(), img.height()));
35//!     std::fs::write("out.avif", out).map_err(oxideav_avif::Error::from)?;
36//! }
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! # Layering
42//!
43//! * **Container (`oxideav-heif`)**: the box reader ([`box_parser`] is
44//!   a thin surface over it), the `meta` item model — `hdlr`, `pitm`,
45//!   `iinf` + `infe`, `iloc` (every construction method), `iref`,
46//!   `iprp` / `ipco` / `ipma`, `idat`, `grpl` — item payload
47//!   resolution, the typed `ispe` / `pixi` / `colr` / `pasp` / `clap` /
48//!   `irot` / `imir` / `iscl` / `auxC` / `clli` / `amve` / `rloc` /
49//!   `lsel` / `a1op` / `a1lx` / `rref` / `crtt` / `mdft` / `udes` /
50//!   `altt` properties, the derived-image descriptors, pixel
51//!   composition (grid / `iovl` / `iden`, alpha attachment, `clap` /
52//!   `irot` / `imir`), the `moov` / `trak` / `stbl` sample tables and
53//!   the still-image writer.
54//! * **AVIF profile (this crate)**: [`parse`] / [`parse_header`] build
55//!   this crate's [`Meta`] from the container's, keep `av1C` raw for
56//!   the AV1 layer, read `mdcv` / `cclv` and the HEIF-extension
57//!   properties (`aebr` … `cmin`, the slideshow transition effects,
58//!   `prdi`, `sstr`, `txlo`, `elng`, `fnch`, `mskC`) from the raw
59//!   property bytes, and enforce the AVIF brand rule; the av1-avif
60//!   §2–§8 audits ([`audit_avif_profile_compliance`],
61//!   [`audit_alpha_bit_depth`], [`audit_sequence_header_obu`],
62//!   [`audit_tone_map`], [`audit_avis_sequence`] …), the `sato`
63//!   descriptor / evaluator (§4.2.3) and `tmap` detection (§4.2.2),
64//!   the AV1 decode of every coded item with `lsel` / `a1op` layer
65//!   selection, the AVIF-side composition rules (av1-avif §4.1
66//!   same-depth alpha, nearest-neighbour alpha resize, the `ispe` /
67//!   grid trim with AV1's ceiling chroma extents, monochrome overlays
68//!   staying monochrome), the `avis` sample handling, the writer's
69//!   AVIF layout (brands, item order, property sets, `auxC` URNs) and
70//!   the pixel encoder's tile election.
71//!
72//! # Status
73//!
74//! * [`AvifDecoder`]'s `receive_frame` composites the result:
75//!   * Grid items (HEIF §6.6.2) — decode each tile via `dimg` iref
76//!     and stitch into the declared output rectangle (see [`grid`]).
77//!   * Alpha auxiliary — AV1-coded monochrome item referenced via
78//!     `auxl` + `auxC` URN (see [`alpha`]). The av1-avif v1.2.0 §4.1
79//!     `shall` "AV1 Alpha Image Item shall be encoded with the same
80//!     bit depth as the associated master AV1 Image Item" is audited
81//!     at the container layer via [`audit_alpha_bit_depth`] /
82//!     [`AlphaBitDepthAudit`], surfaced through
83//!     [`AvifInfo::alpha_bit_depth_compliance`].
84//!   * `irot` / `imir` / `clap` post-transforms (see [`transform`]).
85//! * AVIS image sequences — [`avis::parse_avis`] walks the first
86//!   track through the container's sample-table parser and produces a
87//!   flat frame-offset list with `(timescale, display_dims, samples,
88//!   av1_codec_config, handler, sample_description_types)`.
89//!   [`avis::sample_bytes`] resolves a sample's byte slice inside the
90//!   source file. The registry-gated decoder
91//!   ([`decoder::AvifDecoder::decode_avis_file`]) walks the table end
92//!   to end, lifts the `AV1CodecConfigurationRecord` from `stsd` →
93//!   `av01` → `av1C`, and fans every sample through a shared
94//!   `oxideav_av1` registry decoder so inter-frame state is preserved.
95//!   `Decoder::send_packet` dispatches to this path automatically
96//!   when the file's brand classification flags it as a sequence. The
97//!   av1-avif v1.2.0 §3 `shall`s on an AV1 Image Sequence track
98//!   (handler `'pict'`, single `'av01'` sample description, identical
99//!   Sequence Header OBUs across samples) are audited at the
100//!   container layer via [`audit_avis_sequence`] /
101//!   [`AvisSequenceCompliance`].
102//! * `pixi` (HEIF §6.5.6) and `pasp` (HEIF §6.5.4 / ISO/IEC 14496-12
103//!   §8.5.2.1.1) are surfaced through [`AvifInfo`] — see
104//!   [`AvifInfo::num_channels`], [`AvifInfo::max_bit_depth`],
105//!   [`AvifInfo::is_monochrome`] and [`AvifInfo::has_square_pixels`].
106//!
107//! # Encoder
108//!
109//! The **container muxer** is implemented in [`mux`]: [`AvifMuxer`] /
110//! [`AvifGridMuxer`] / [`AvifOverlayMuxer`] / [`encode_still_av1`] emit
111//! a conformant AVIF file around an already-coded AV1 Image Item Data
112//! payload plus its `av1C` record — this crate decides the brands,
113//! item order and property sets, [`oxideav_heif::HeifWriter`]
114//! serialises the `meta` tree and `mdat` — with `ispe` / `pixi` /
115//! `colr` / `pasp` / `clap` / `irot` / `imir` item properties, an
116//! optional AV1-coded alpha auxiliary (`auxC` + `auxl`), and optional
117//! `grid` tiling (`dimg`). The output round-trips through this crate's
118//! own [`parse`] path pixel-consistently (the coded AV1 stream is copied
119//! verbatim).
120//!
121//! Turning raw pixels *into* that AV1 bitstream is the job of the
122//! [`still`] module ([`StillImage`] + [`encode_still`] /
123//! [`encode_still_grid`]): it drives `oxideav_av1`'s conformance-grade
124//! KEY-frame encoder across the full (bit depth × chroma format)
125//! matrix — including RGB(A) via the H.273 identity matrix, alpha
126//! auxiliaries, arbitrary extents (pad + `clap`), and grid tiling —
127//! then wraps the coded payload through the muxer. The registry
128//! [`Encoder`](encoder) surface ([`make_encoder`] / [`AvifEncoder`])
129//! rides the same pipeline: every video frame sent through
130//! `send_frame` yields one complete AVIF file packet. Callers holding
131//! an already-coded payload keep muxing it directly with
132//! [`AvifMuxer`].
133//!
134//! # Standalone vs registry-integrated
135//!
136//! The crate's default-on `registry` Cargo feature pulls in
137//! `oxideav-core` + `oxideav-av1` and exposes the
138//! `oxideav_core::Decoder` trait surface plus the [`register`] entry
139//! point. Disable the feature (`default-features = false`) for an
140//! `oxideav-core`-free build that still exposes:
141//!
142//! * The container parse ([`parser`], [`parse`], [`parse_header`],
143//!   [`meta`]; [`box_parser`] over `oxideav_heif::boxes`).
144//! * The AVIS sample-table walker ([`avis::parse_avis`]).
145//! * The grid / overlay / alpha / transform composition entry points
146//!   ([`grid`], [`overlay`], [`alpha`], [`transform`]) over the
147//!   container's `compose` layer, operating on crate-local
148//!   [`image::AvifFrame`] / [`image::AvifPixelFormat`].
149//! * Container-side inspection: [`inspect::inspect`],
150//!   [`inspect::AvifInfo`], [`inspect::transforms_for`].
151//! * The CICP signalling helpers ([`cicp`]).
152//!
153//! Standalone callers that want pixel decode must pair this surface
154//! with their own AV1 decoder — the in-tree one ([`oxideav_av1`]) is
155//! pulled in only when `registry` is on.
156
157pub mod alpha;
158pub mod api;
159pub mod avis;
160pub mod box_parser;
161pub mod cicp;
162pub mod derived;
163pub mod error;
164mod frame_bridge;
165pub mod grid;
166pub mod image;
167pub mod inspect;
168pub mod meta;
169pub mod mux;
170pub mod overlay;
171pub mod parser;
172pub mod region;
173pub mod sample_group;
174pub mod transform;
175
176#[cfg(feature = "registry")]
177mod av1_config;
178
179#[cfg(feature = "registry")]
180pub mod api_codec;
181
182#[cfg(feature = "registry")]
183pub mod decoder;
184
185#[cfg(feature = "registry")]
186pub mod encoder;
187
188#[cfg(feature = "registry")]
189pub mod sequence;
190#[cfg(feature = "registry")]
191pub mod signal;
192#[cfg(feature = "registry")]
193pub mod still;
194
195pub use alpha::{composite_alpha, find_alpha_item_id, ALPHA_URN_PREFIX};
196pub use api::{
197    info, probe, AvifImage, ColorInfo, ColorRange, DecodeOptions, EncodeOptions, EncodeSpeed,
198    Frame, ImageInfo, Metadata, PixelFormat, Plane, RgbImage, RgbaImage, StillChroma,
199};
200pub use avis::{
201    audit_avis_profile_compliance, audit_avis_sequence, audit_edit_list, inspect_avis, parse_avis,
202    parse_prft, parse_producer_reference_times, parse_ssix, parse_subsegment_indexes, sample_bytes,
203    AvisInfo, AvisMeta, AvisProfileCompliance, AvisSequenceCompliance, EditListCompliance,
204    EditListEntry, ProducerReferenceTime, Sample, SubsegmentIndex, SubsegmentRange, HANDLER_PICT,
205    NTP_UNIX_EPOCH_OFFSET_SECONDS,
206};
207pub use cicp::{
208    effective_cicp, is_matrix_reserved, is_primaries_reserved, is_transfer_reserved, matrix_name,
209    primaries_name, transfer_name, CicpTriple,
210};
211pub use derived::{
212    audit_alpha_bit_depth, audit_avif_profile_compliance, audit_iden_derivations, audit_pred_brand,
213    audit_sequence_header_obu, audit_tone_map, build_derivation_graph, normalize_full_range_plane,
214    output_dims_from_reconstructed, parse_grpl, reconstructed_dims, resolve_grids,
215    resolve_iden_derivations, resolve_overlays, resolve_tone_maps, transform_chain,
216    AlphaBitDepthAudit, AvifProfile, AvifProfileCompliance, BracketingKind, DerivationGraph,
217    DerivationKind, DerivationNode, DimTransform, EntityGroup, GainMapChannel, GainMapMetadata,
218    GainMapRational, GridResolution, GridTilePlacement, IdenCompliance, IdenResolution,
219    ImageOverlay, Mif1Compliance, OverlayEntry, OverlayPlacement, OverlayResolution,
220    PredBrandCompliance, SampleTransform, SequenceHeaderObuAudit, Token, ToneMapCompliance,
221    ToneMapResolution, MAX_DERIVATION_DEPTH,
222};
223pub use error::{AvifError, Result};
224/// The crate error under the contract's name.
225pub type Error = AvifError;
226pub use grid::{composite_grid, ImageGrid};
227pub use image::{AvifFrame, AvifPixelFormat, AvifPlane};
228pub use inspect::{
229    coded_item_dependencies, derivation_graph, entity_groups, gain_map_metadata, inspect,
230    is_font_item, item_payload_bytes, region_items, region_items_for, text_items, text_items_for,
231    transforms_for, AvifInfo, CodedItemDependencies, ResolvedRegionItem, ResolvedTextItem,
232};
233pub use meta::{
234    A1lx, A1op, Aebr, Afbr, Altt, AuxC, AuxKind, Cclv, Clap, Clli, Cmex, Cmin, Colr, Crtt, Dobr,
235    Elng, Fade, Fnch, Fobr, Imir, IrefEntry, Irot, Iscl, Ispe, ItemInfo, ItemLocation, Lsel, MaskC,
236    Mdcv, Mdft, Meta, Pano, PanoGrid, Pasp, Pixi, Prdi, Property, Rloc, Rref, Splt, Ssld, Sstr,
237    Stpe, Subs, SubsEntry, Tols, Txlo, Udes, Wbbr, Wipe, Zoom, AUX_URN_ALPHA_HEVC,
238    AUX_URN_ALPHA_MPEG, AUX_URN_DEPTH_HEVC, AUX_URN_DEPTH_MPEG, AUX_URN_HDR_GAINMAP,
239    ITEM_TYPE_EXIF, ITEM_TYPE_IDEN, ITEM_TYPE_IOVL, ITEM_TYPE_MIME, ITEM_TYPE_SATO, ITEM_TYPE_TMAP,
240    ITEM_TYPE_URI,
241};
242pub use mux::{
243    encode_still_av1, AvifGridMuxer, AvifMuxer, AvifOverlayMuxer, BurstAlpha, BurstMember,
244    EntityGroupSpec, GridTile, IdentityDerivation, OverlayLayer, ToneMapItem,
245};
246pub use overlay::{composite_overlay, OverlayInput, MAX_OVERLAY_CANVAS_PIXELS};
247pub use parser::{
248    audit_mif1, classify_brands, item_bytes, item_bytes_owned, item_bytes_owned_full,
249    item_bytes_owned_with_idat, item_bytes_with_idat, parse, parse_header, AvifFile, AvifHeader,
250    BrandClass, BRAND_AVIF, BRAND_AVIO, BRAND_AVIS, BRAND_MA1A, BRAND_MA1B, BRAND_MIAF, BRAND_MIF1,
251    BRAND_MSF1, ITEM_TYPE_AV01, ITEM_TYPE_GRID,
252};
253pub use region::{
254    resolve_derived_region_items, DerivedRegionItem, RegionGeometry, RegionItem, ITEM_TYPE_MSKI,
255    ITEM_TYPE_RGAN, REF_TYPE_DRGN,
256};
257pub use sample_group::{
258    parse_csgp, parse_sample_group_descriptions, parse_sample_to_groups, parse_sbgp, parse_sgpd,
259    BracketingEntry, SampleGroupDescription, SampleToGroup, SampleToGroupKind, SampleToGroupRun,
260    VisualEquivalenceEntry,
261};
262pub use transform::{apply_clap, apply_imir, apply_irot, crop_top_left};
263
264#[cfg(feature = "registry")]
265pub use api_codec::{
266    decode, decode_all, decode_all_with, decode_from, decode_rgb8, decode_rgba8, decode_with,
267    encode, encode_all, encode_rgb8, encode_rgba8, encode_to,
268};
269
270#[cfg(feature = "registry")]
271pub use decoder::{
272    make_decoder, AvifDecoder, AvifDecoderOptions, MAX_GRID_CANVAS_PIXELS, MAX_ITEM_DECODES,
273};
274
275#[cfg(feature = "registry")]
276pub use encoder::{
277    make_encoder, quality_to_base_q_idx, AvifEncoder, AvifEncoderOptions, ENCODER_PIXEL_FORMATS,
278};
279
280#[cfg(feature = "registry")]
281pub use sequence::{encode_sequence, SequenceEncodeOptions};
282
283#[cfg(feature = "registry")]
284pub use signal::{color_signal_for, labelled_pixel_format, miaf_default_signal};
285
286#[cfg(feature = "registry")]
287#[allow(deprecated)]
288pub use still::StillEncodeOptions;
289#[cfg(feature = "registry")]
290pub use still::{
291    elect_grid_tiling, encode_still, encode_still_auto, encode_still_burst, encode_still_grid,
292    encode_still_layered, encode_still_overlay, GainMapSpec, OverlayCanvas, OverlayLayerImage,
293    StillImage, StillProperties, GRID_MIN_TILE_DIM, STILL_MAX_CODED_DIM, TILE_LAYOUT_THREADS,
294};
295
296#[cfg(feature = "registry")]
297#[doc(hidden)]
298pub use registry_glue::__oxideav_entry;
299
300/// Public codec id string. Matches the aggregator-crate Cargo feature `avif`.
301pub const CODEC_ID_STR: &str = "avif";
302
303#[cfg(feature = "registry")]
304mod registry_glue {
305    //! Codec registry + AVIF encoder factory. Gated behind `registry`
306    //! because the entire framework integration depends on
307    //! `oxideav_core` + `oxideav_av1`.
308
309    use oxideav_core::{
310        CodecCapabilities, CodecId, CodecInfo, CodecRegistry, ContainerRegistry, Error,
311        RuntimeContext,
312    };
313
314    use crate::decoder::make_decoder;
315    use crate::encoder::make_encoder;
316    use crate::error::AvifError;
317    use crate::CODEC_ID_STR;
318
319    /// Bridge crate-local errors into the framework error type. Mirrors
320    /// the conversion the decoder performs internally so external
321    /// callers using the standalone container API get the same mapping
322    /// when they wrap up their own framework integration.
323    impl From<AvifError> for Error {
324        fn from(e: AvifError) -> Self {
325            match e {
326                AvifError::InvalidData(s) => Error::InvalidData(s),
327                AvifError::Unsupported(s) => Error::Unsupported(s),
328                AvifError::LimitExceeded(s) => Error::ResourceExhausted(s),
329                AvifError::Io(e) => Error::Io(e),
330            }
331        }
332    }
333
334    /// Register the AVIF decoder + encoder factories with a registry.
335    /// The decoder is declared `avif_heif_av1_decode` — we parse the
336    /// HEIF container end to end, hand the AV1 bitstream to
337    /// oxideav-av1, and composite grid / alpha / transform properties
338    /// on the resulting frames. The encoder factory ([`make_encoder`])
339    /// yields the pixel-in AVIF encoder (one complete AVIF file per
340    /// frame; see [`crate::encoder`] for the format/option surface).
341    ///
342    /// The capabilities declare what the encoder truly accepts
343    /// ([`crate::encoder::ENCODER_PIXEL_FORMATS`] — the set a
344    /// gateway's conversion ladder reads) and that it codes both
345    /// losslessly (`q` 0 / `quality` 100, the default) and lossily;
346    /// the option schemas ([`crate::encoder::AvifEncoderOptions`],
347    /// [`crate::decoder::AvifDecoderOptions`]) are attached so the
348    /// framework's advisory `quality` and `threads` reach the codec.
349    pub fn register_codecs(reg: &mut CodecRegistry) {
350        let caps = CodecCapabilities::video("avif_heif_av1_decode")
351            .with_decode()
352            .with_encode()
353            .with_lossy(true)
354            .with_lossless(true)
355            .with_intra_only(true)
356            .with_pixel_formats(crate::encoder::ENCODER_PIXEL_FORMATS.to_vec());
357        reg.register(
358            CodecInfo::new(CodecId::new(CODEC_ID_STR))
359                .capabilities(caps)
360                .decoder(make_decoder)
361                .encoder(make_encoder)
362                .encoder_options::<crate::encoder::AvifEncoderOptions>()
363                .decoder_options::<crate::decoder::AvifDecoderOptions>(),
364        );
365    }
366
367    /// Unified registration entry point: install both the AVIF codec
368    /// factories and the `.avif` / `.avifs` extension hints into a
369    /// [`RuntimeContext`].
370    ///
371    /// Note this does **not** also register the underlying AV1 codec —
372    /// callers that want both in one call should use
373    /// [`register_with_av1`] (which itself only touches the codec
374    /// sub-registry, since neither AVIF nor AV1 have container hooks
375    /// beyond `.avif*`).
376    ///
377    /// This is the preferred entry point for new code — it matches the
378    /// convention every sibling crate now follows. Direct callers that
379    /// only need one of the two sub-registries can keep using
380    /// [`register_codecs`] / [`register_containers`].
381    pub fn register(ctx: &mut RuntimeContext) {
382        register_codecs(&mut ctx.codecs);
383        register_containers(&mut ctx.containers);
384    }
385
386    oxideav_core::register!("avif", register);
387
388    /// Convenience: register AVIF **and** the underlying AV1 codec in
389    /// one call — the historical contract of this entry point,
390    /// restored now that `oxideav-av1`'s clean-room rebuild ships its
391    /// own registry surface again. Callers that only want the AVIF
392    /// factories keep using [`register_codecs`].
393    pub fn register_with_av1(reg: &mut CodecRegistry) {
394        register_codecs(reg);
395        oxideav_av1::registry::register_codecs(reg);
396    }
397
398    /// Register the `.avif` / `.avifs` extensions against the codec id
399    /// `"avif"` so consumers (cli-convert, pipeline output probing) can
400    /// resolve a `.avif` output path through the central
401    /// [`ContainerRegistry`] without a hard-coded extension list.
402    pub fn register_containers(reg: &mut ContainerRegistry) {
403        reg.register_extension("avif", CODEC_ID_STR);
404        reg.register_extension("avifs", CODEC_ID_STR);
405    }
406}
407
408#[cfg(feature = "registry")]
409pub use registry_glue::{register, register_codecs, register_containers, register_with_av1};
410
411#[cfg(all(test, feature = "registry"))]
412mod tests {
413    use super::*;
414    use oxideav_core::{
415        CodecId, CodecParameters, CodecRegistry, ContainerRegistry, Error, Frame, RuntimeContext,
416    };
417
418    #[test]
419    fn register_installs_factories() {
420        let mut reg = CodecRegistry::new();
421        register_codecs(&mut reg);
422        let id = CodecId::new(CODEC_ID_STR);
423        // The encoder factory yields the real pixel-in AVIF encoder:
424        // a registry-resolved encode of an 8x8 gray frame produces a
425        // parseable AVIF file.
426        let mut params = CodecParameters::video(id.clone());
427        params.width = Some(8);
428        params.height = Some(8);
429        params.pixel_format = Some(oxideav_core::PixelFormat::Gray8);
430        let mut enc = reg.first_encoder(&params).expect("encoder factory");
431        let frame = Frame::Video(oxideav_core::frame::VideoFrame {
432            pts: Some(0),
433            planes: vec![oxideav_core::frame::VideoPlane {
434                stride: 8,
435                data: (0..64u8).collect(),
436            }],
437        });
438        enc.send_frame(&frame).expect("send_frame");
439        let pkt = enc.receive_packet().expect("receive_packet");
440        crate::parser::parse(&pkt.data).expect("emitted packet is a parseable AVIF file");
441        // A frame without the required parameters is refused with a
442        // precise error, not a panic.
443        let bare = reg
444            .first_encoder(&CodecParameters::video(id))
445            .expect("encoder factory");
446        let mut bare = bare;
447        let empty = Frame::Video(oxideav_core::frame::VideoFrame {
448            pts: Some(0),
449            planes: vec![],
450        });
451        match bare.send_frame(&empty) {
452            Err(Error::InvalidData(msg)) => assert!(msg.contains("width"), "{msg}"),
453            other => panic!("encoder send_frame: expected InvalidData, got {other:?}"),
454        }
455        // Decoder factory succeeds; `send_packet` exercises the HEIF
456        // parse + AV1 decode pipeline.
457        let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
458        let _ = reg.first_decoder(&params).expect("decoder factory");
459    }
460
461    #[test]
462    fn avif_extension_resolves_to_avif_container() {
463        let mut reg = ContainerRegistry::new();
464        register_containers(&mut reg);
465        assert_eq!(reg.container_for_extension("avif"), Some(CODEC_ID_STR));
466        assert_eq!(reg.container_for_extension("AVIF"), Some(CODEC_ID_STR));
467        assert_eq!(reg.container_for_extension("avifs"), Some(CODEC_ID_STR));
468    }
469
470    #[test]
471    fn register_via_runtime_context_installs_codec_factory() {
472        let mut ctx = RuntimeContext::new();
473        register(&mut ctx);
474        let id = CodecId::new(CODEC_ID_STR);
475        let params = CodecParameters::video(id);
476        let _dec = ctx
477            .codecs
478            .first_decoder(&params)
479            .expect("avif decoder factory");
480        // The unified entry point also wires the .avif / .avifs
481        // extension hints through the same call.
482        assert_eq!(
483            ctx.containers.container_for_extension("avif"),
484            Some(CODEC_ID_STR)
485        );
486        assert_eq!(
487            ctx.containers.container_for_extension("avifs"),
488            Some(CODEC_ID_STR)
489        );
490    }
491}