oxideav-core
Core types for the oxideav pure-Rust media framework:
-
Packet— one compressed chunk belonging to one stream, with timestamps. Chainablewith_*builders cover everyPacketFlagsfield (with_keyframe/with_header/with_corrupt/with_discard/with_unit_boundary, plus a bulkwith_flags) and the stream-index / time-base / pts / dts / duration setters used by demuxers and remuxers. Anend_pts()accessor returns the overflow-checkedpts + durationfor muxers that need a per- packet end timestamp. -
Frame— one uncompressed audio / video / subtitle chunk.VideoFramecan carry typed in-band side-channels alongside its pixel planes: a palette for palette-indexed (Pal8) content (palette()/set_palette/take_palette) with an optional per-entry alpha record (palette_alpha()/set_palette_alpha,palette_rgba(i)/set_palette_rgba— see Palettes) and a per-plane significant-bits record for mixed depths no singlePixelFormatnames — e.g. 12-bit luma with 10-bit chroma from a custom signal range (significant_bits()/set_significant_bits/take_significant_bits, LSB-anchored values), a colour-signal record (color_signal()/set_color_signal/take_color_signal) and a layer / view identity (layer()/set_layer/take_layer). The records compose on one frame;image_planes()iterates pixel data side-channel-agnostically. See Colour signal and Layers and views. Per-picture metadata blobs ride the same way (blobs()/set_blobs/push_blob/take_blobs, see Metadata blobs). -
StreamInfo/CodecParameters— what a demuxer advertises and what a decoder / encoder consumes, including the stream'scolor_signal, itslayersdescription and its metadatablobs(ICC / Exif / XMP / IPTC / cover art / custom). -
TimeBase/Timestamp/Rational— rational time per stream; timestamps are integers in that base. Named constants (MILLIS/MICROS/NANOS/MPEG_TS/AUDIO_48K/AUDIO_44K1/AUDIO_8K/SECONDS) replace the workspace'sTimeBase::new(1, …)magic-numbers;TimeBase::from_rate(u32)constructs the inverse-of-rate form, andticks_of(seconds: f64)is the overflow-clamped inverse of the existingseconds_of(ticks).Timestamp::from_seconds/checked_add_ticks/checked_sub_ticks/checked_diff/checked_rescalecover per-stream timestamp arithmetic (including cross-base differences for remux pipelines).The whole numeric core is total — no panic, no silent wrap, even on
i64::MINterms or zero denominators.rescalecomputes in 128-bit sign+magnitude space, rounds half-away-from-zero, and saturates at thei64boundaries;rescale_checkedreturnsNoneinstead whereverrescalewould saturate or default;rescale_rndtakes an explicitRoundingmode (NearestAway/Floorfor DTS-safe stamps /Ceil/TowardZero).Rationalsupports+ - * /and unary-(exact viai128intermediates, reduced, closest-representable approximation when even the reduced result exceedsi64),checked_add/sub/mul/divthat reportNoneexactly where the operators approximate, pluscmp_value/equals_valuefor value comparison (30000/1001vs30/1) that doesn't disturb the structuralEq/Hashcallers rely on to preserve the on-wire fraction. Property-tested against independenti128oracles (~200k edge-biased cases intests/props.rs). -
PixelFormat/SampleFormat— enum of supported raw formats (70 pixel variants including 8/10/12/16-bit YUV at 4:2:0/4:2:2/4:4:4/4:1:1/4:4:0, YUV+alpha at 4:2:0/4:2:2/4:4:4 in both 8-bit and deep 10/12/16-bit flavours, planar GBR(A) across the full 8/10/12/14/16-bit depth ladder with an alpha companion at every depth, scene-referred 32-bit float gray/RGB(A)/planar-GBR(A) for linear-light HDR, packed RGB/RGBA, gray+alpha at 8 and 16 bits, CMYK in both ink conventions, NV12/NV21, all common sample layouts), plus plane-geometry helpers on every variant:chroma_subsampling()(log2 shifts per sampling class),plane_dimensions()(ceil-division subsampled grids), and tightly-packed sizing viaplane_row_bytes()/plane_size_bytes()/frame_size_bytes()with checked arithmetic. -
AttachedPicture/PictureType— ID3v2APICtaxonomy shared by ID3v2 / FLAC / MP4 / Vorbis cover-art carriage.PictureTyperound-trips byte-for-byte throughfrom_u8↔to_u8over the spec- assigned0x00..=0x14range; unassigned bytes collapse toUnknown, flagged viais_known()so strict writers can refuse to emit the0xFFsentinel.AttachedPicture::new(mime, kind)plus chainablewith_description/with_data/with_picture_typebuilders cover the producer side (parsers writing into a partially-decoded picture as bytes arrive), andis_external_link()distinguishes ID3v2's"-->"URL-sentinel mime from inline image bytes without having to hardcode the string at every call site. -
CodecTag/CodecResolver— neutral abstraction for mapping container-level tags (AVI FourCC, WAVEFORMATEXwFormatTag, MP4 OTI, Matroska CodecID strings) to oxideavCodecIds. Lets codec crates own their own tag claims without pulling a codec registry into every container. Tag-less identification gets its own container-agnostic path: codecs declare the payload magic prefixes they answer to (CodecInfo::payload_magic(b"\x01vorbis"),b"OpusHead",b"fLaC", …) and callers resolve a stream's leading payload bytes withCodecResolver::resolve_payload_magic(first_bytes)— longest matching magic wins, then resolution priority, then registration order. Serves Ogg's BOS packets and raw elementary-stream sniffing alike. Every lookup follows the documented resolution order and exposes its ranked candidate list. -
bits— shared MSB-first / LSB-firstBitReader/BitWriterplus unary helpers. Used by the FLAC, AAC, H.264, HEVC, Vorbis and a dozen other codecs in the workspace. The LSB pair (the Vorbis §2.1.4 layout) exposes the full MSB surface —peek_u32(Huffman lookup windows),skip/consume,align_to_byte,read_bytes, positional bookkeeping,write_bytesand the alias set. Criterion baselines live inbenches/primitives.rs(~1.3 GiB/s read, ~430 MiB/s write on a mixed-width field schedule). -
register!— the sibling entry-point macro. Expands topub fn __oxideav_entry(ctx)(hidden dispatch plumbing) thatoxideav-meta's generatedregister_allcalls at the sibling's crate root — invoke it inlib.rs, or re-export it there (pub use registry::__oxideav_entry;) when it lives in a submodule. Contract spelled out inregistry::slice. -
SourceRegistry— URI scheme dispatch for sources. Drivers register as one of three shapes —BytesSource(file / http),PacketSource(transport-layer protocols that pre-demux), orFrameSource(synthetic generators that emit decoded frames) — andopen(uri)returns aSourceOutputenum the pipeline executor branches on. -
Error— one unified error enum used across the ecosystem, with a documented caller-action taxonomy (verdict vs starvation vs backpressure), constructors for every string variant, andis_eof/is_need_more/is_starved/is_resource_exhaustedpredicates (the enum can't bePartialEq—Iowrapsstd::io::Error).
Every public item is documented (#![warn(missing_docs)] is enforced
at the crate root, promoted to deny by CI's clippy gate) and
cargo doc is warning-clean under docs.rs-strict settings.
Zero C dependencies. Zero FFI. Zero *-sys crates.
Palettes
Palette-indexed content (PixelFormat::Pal8) carries its colour table
on the frame as two in-band side-channel records:
- Palette (
stride == 0) — packed RGB triplets, entryiat bytes3*i .. 3*i + 3;palette()/set_palette/with_palette/take_palette, per-entrypalette_rgb(i). Up to 256 entries; producers attach exactly as many as the source declares. - Palette alpha (
stride == usize::MAX - 3) — one alpha byte per entry in the same order, for a GIF transparent index, a PNGtRNSchunk on an indexed image, TGA / BMP alpha palettes;palette_alpha()/set_palette_alpha/with_palette_alpha/take_palette_alpha. The record may be shorter than the palette (a GIF with transparent index 2 needs three bytes) and every entry it does not cover is opaque. It is only meaningful next to a palette: without one, or when it is longer than the palette's entry count, it reads asNoneand consumers fall back to opaque. Validation is at read time, so the two records may be attached in either order.
palette_rgba(i) is the one lookup a Pal8 → RGBA expander needs: the
RGB triplet plus the alpha (255 when absent), None exactly when
palette_rgb(i) is. set_palette_rgba(&[[u8; 4]]) /
with_palette_rgba write both records from one RGBA table. Frames
without a transparent entry attach no alpha record and stay
byte-for-byte what they were.
Colour signal
A PixelFormat says how samples are laid out; it does not say what
the values mean. ColorSignal (module signal) carries the
coding-independent description of Rec. ITU-T H.273 | ISO/IEC 23091-2:
| field | type | H.273 name |
|---|---|---|
range |
ColorRange |
VideoFullRangeFlag (+ Unspecified) |
primaries |
ColorPrimaries(u8) |
ColourPrimaries |
transfer |
TransferCharacteristics(u8) |
TransferCharacteristics |
matrix |
MatrixCoefficients(u8) |
MatrixCoefficients |
The three code points are raw 8-bit newtypes with named constants for
every value H.273 (07/2024) defines (ColorPrimaries::BT2020,
TransferCharacteristics::SMPTE_ST2084, MatrixCoefficients::BT709,
…); reserved values pass through unchanged. Every field defaults to
unspecified (code point 2 / ColorRange::Unspecified) and the crate
never substitutes a guess — ColorSignal::or(fallback) layers one
description over another field-wise, and consumers apply their own
policy to what remains open.
Where it lives:
- Stream —
CodecParameters::color_signal(default unspecified), set withwith_color_signal/with_color_range. Demuxers fill it from the container's colour record (an ISOBMFFcolrbox, a MatroskaColourelement, …), decoders from the bitstream's own signalling when the container had none, encoders inoutput_params()so muxers know what to write. - Frame —
VideoFrame::color_signal()/set_color_signal/with_color_signal/take_color_signal, an in-band side-channel record (stride == usize::MAX - 1) for producers whose signal is per-picture or that have no stream object (a still-image item, an auxiliary alpha image whose range differs from its master). A frame record refines the stream value:frame.color_signal().unwrap_or_default().or(params.color_signal). - Pixel-format labels — only the legacy
YuvJ420P/YuvJ422P/YuvJ444Plabels commit to a range (PixelFormat::implied_color_range→Full). Every other format, including every >8-bit and alpha-bearing surface, leaves the range to the signal.CodecParameters::resolved_color_range()combines the two: explicit signal first, then the label, elseUnspecified. Converters should read that instead of inferring range from the format name — a 10-bit full-range stream onYuv420P10Leand an 8-bit alpha plane onGray8are only expressible this way.
Layers and views
Scalable and multi-view coding (spatial / quality / view scalability in
H.264 Annexes G–H, H.265 / H.266 Annex F, AV1 operating points, …) put
more than one layer of pictures in one stream. Module layer gives
that structure a codec-neutral shape; identifiers are the codec's own
values passed through verbatim, 0 is the base layer.
- Per frame —
LayerIdentity { layer_id: u16, view_id: Option<u16>, access_unit: Option<u64> }, attached by the decoder throughVideoFrame::set_layer/with_layerand read withVideoFrame::layer()(orlayer_or_base(), which readsNoneas the base layer). It is an in-band side-channel record (stride == usize::MAX - 2); single-layer decoders attach nothing and their frames are byte-for-byte unchanged.access_unitlets a consumer regroup the per-layer frames of one presentation instant. - Per stream —
CodecParameters::layers: Vec<LayerInfo>withLayerInfo { layer_id, view_id, depends_on }, set withwith_layersand queried withlayer(id)/is_multi_layer(). Empty for single-layer streams. A container that exposes an operating point or a single view filters on these ids and must feed every layer reachable throughdepends_on; a stereo renderer routes frames byview_id.
Both features are strictly additive: no existing field, signature or
variant changed, VideoFrame remains constructible by struct literal,
and CodecParameters gained its two fields behind its existing
#[non_exhaustive] marker.
Metadata blobs
Formats embed metadata the framework cannot and should not parse — an
ICC profile, an Exif block, an XMP packet, IPTC datasets, a track's
cover art, a format's own attribute table. Module blob carries such
payloads unchanged from the producer that found them to the consumer
that wants them, on the same path the samples travel:
MetadataBlob { kind: BlobKind, mime: Option<String>, data: Vec<u8> }(#[non_exhaustive];MetadataBlob::new(kind, data)/with_mime).BlobKindis a name compared byte-for-byte with well-known constantsICC,EXIF,XMP,IPTC,COVER_ARTandBlobKind::custom("exr-attributes")for anything else. The well-known kinds pin a format-neutral payload (the carrying format's framing removed: an Exif blob starts at its byte-order mark whether it came from a JPEGAPP1, a PNGeXIfor a HEIF item; an ICC blob is the complete profile file) so a blob lifted from one format drops into another. The module docs hold the table.- Stream —
CodecParameters::blobs: Vec<MetadataBlob>(empty by default), withblobs()/blob(&kind)/blobs_of(&kind)/push_blob/with_blob/with_blobs/take_blobs. Demuxers fill it from the container, still-image decoders from the codestream, encoders inoutput_params()so muxers know what to embed. Not part ofmatches_core. - Frame —
VideoFrame::blobs()/blob(&kind)/set_blobs/push_blob/with_blob/with_blobs/take_blobs, an in-band side-channel record (stride == usize::MAX - 4, wire formblob::encode_blobs/decode_blobs) for pictures whose metadata is genuinely per-picture — a multi-page TIFF's per-page Exif, HEIF burst items with their own profile. A malformed record reads as the empty list, never half a list. A frame blob refines the stream blob of the same kind:frame.blob(&k).or_else(|| params.blob(&k).cloned()).
The mechanism is medium- and container-agnostic: the same API carries
an ICC profile on an image stream and cover art on an audio stream, and
nothing in core names a container. Container-level file attachments
with filenames remain Attachment; the demuxer-level typed cover-art
pathway remains AttachedPicture.
Resolution order
Every registry lookup that can have more than one candidate ranks them by one rule, so the answer depends only on the registered claims and the input — never on hash-map iteration order or memory layout, and identically in every process:
- Evidence, descending — the probe score (
ContainerRegistrycontent probes), the probe confidence (CodecRegistrytag claims; unprobed claims count as1.0), or the matched prefix length (payload magics). Evidence always beats the two tie-breaks below. - Resolution priority, ascending — an
i32attached at registration; lower is preferred, the same convention asCodecCapabilities::priority, defaultDEFAULT_PRIORITY(100). Set it withContainerRegistry::register_probe_with_priority,ContainerRegistry::register_extension_with_priority, orCodecInfo::with_resolution_priority. It is deliberately notCodecCapabilities::priority: that field ranks implementations of one codec id (hardware before software) and must not let a backend out-rank another codec's identity claim on an ambiguous tag. - Registration order — earlier registration wins for probes, tags and magics. Extension hints are a replacement map and keep their historical contract: at equal priority the most recent claim wins.
Each path exposes the whole ranked list for audits —
ContainerRegistry::probe_candidates / extension_candidates,
CodecRegistry::resolve_tag_candidates /
resolve_payload_magic_candidates — whose first element is exactly
what probe_input / container_for_extension / resolve_tag /
resolve_payload_magic return. Two heads at equal evidence and
equal priority mean the registry settled the claim by order alone; a
sibling that should own such an input pins it with a priority below
the default instead of relying on where it lands in register_all.
Registration contracts: a container name registered twice
(register_demuxer / register_muxer / register_probe) is
replaced in place and keeps its original order slot; a codec id may
register any number of times (multi-implementation codecs), and a tag
claimed twice by one id resolves to that id either way while both
claims stay visible in the candidate list. Non-positive or non-finite
probe confidences are never candidates.
Usage
[]
= "0.1"
Everything downstream in oxideav (codec traits, container traits, codec
implementations, the CLI) depends on this crate transitively, so the
surface is kept deliberately small. The 0.1 series is the first stable
semver line — additive changes are 0.1.x patch bumps; breaking
reshapes go to 0.2.0.
License
MIT — see LICENSE.