oxideav-prores 0.1.0

Pure-Rust Apple ProRes codec — decoder + encoder for 422 Proxy/LT/Standard/HQ and 4444 / 4444 XQ
Documentation

oxideav-prores

CI crates.io docs.rs License: MIT

Pure-Rust Apple ProRes codec — decoder + encoder for all six ProRes video profiles (422 Proxy / LT / Standard / HQ and 4444 / 4444 XQ). 8-bit, 10-bit, and 12-bit Y'CbCr; lossless alpha plane on the 4444 / 4444 XQ profiles, with an alpha-typed Yuva422P / Yuva444P frame surface on both codec directions. Progressive and interlaced (TFF/BFF) in every profile.

Part of the oxideav framework but usable standalone. Implemented from SMPTE RDD 36 (no C codec libraries linked or wrapped, no *-sys crates).

Supported profiles

Every profile decodes and encodes.

Profile FourCC Pixel formats
422 Proxy apco Yuv422P, Yuv422P10Le, Yuv422P12Le, Yuva422P
422 LT apcs Yuv422P, Yuv422P10Le, Yuv422P12Le, Yuva422P
422 Standard apcn Yuv422P, Yuv422P10Le, Yuv422P12Le, Yuva422P
422 HQ apch Yuv422P, Yuv422P10Le, Yuv422P12Le, Yuva422P
4444 ap4h Yuv444P, Yuv444P10Le, Yuv444P12Le, Yuva444P
4444 XQ ap4x Yuv444P, Yuv444P10Le, Yuv444P12Le, Yuva444P

Using it through the oxideav framework

You don't register anything yourself: oxideav-meta's register_all(&mut RuntimeContext) sets up every enabled sibling, and ProRes tracks inside MOV/MP4/MKV then route here through the generic demux→decode flow. The codec id is "prores"; all frames are intra (keyframe-only).

use oxideav_core::{CodecId, CodecParameters, PixelFormat, RuntimeContext};

let mut ctx = RuntimeContext::new();
oxideav_meta::register_all(&mut ctx);

let mut params = CodecParameters::video(CodecId::new("prores"));
params.width = Some(1920);
params.height = Some(1080);
params.pixel_format = Some(PixelFormat::Yuv422P);
params.bit_rate = Some(220_000_000); // -> 422 HQ

let mut enc = ctx.codecs.make_encoder(&params)?;
let mut dec = ctx.codecs.make_decoder(&params)?;

The encoder picks a profile from pixel_format + bit_rate:

pixel_format bit_rate hint (bps) Profile
Yuv422P / Yuva422P <= 70_000_000 Proxy
Yuv422P / Yuva422P <= 125_000_000 LT
Yuv422P / Yuva422P <= 180_000_000 or None Standard
Yuv422P / Yuva422P > 180_000_000 HQ
Yuv444P / Yuva444P >= 400_000_000 4444 XQ
Yuv444P / Yuva444P anything else 4444

Using the codec directly

Without the framework, pull oxideav-core for the shared types and use the crate's own entry points:

[dependencies]
oxideav-core = "0.1"
oxideav-prores = "0.0"

The direct factories decoder::make_decoder(&params) / encoder::make_encoder(&params) take the same CodecParameters as above; encoder::make_encoder_with_config(&params, cfg) adds the EncoderConfig knobs, and the free functions (decoder::decode_packet_with_depth / decode_packet_with_format / decode_packet_with_options, encoder::encode_frame_with_alpha / encode_frame_interlaced) work packet-by-packet with no state beyond the call. See docs.rs for the full surface.

Bit depth and output range

RDD 36 carries no per-frame bit-depth syntax; §7.5.1 defines the conversion to samples of arbitrary depth b. Request the depth via CodecParameters::pixel_format: Yuv422P/Yuv444P (or nothing) for 8-bit, *P10Le for 10-bit (0..=1023), *P12Le for 12-bit (0..=4095).

The §7.5.1 clamp has two spec-offered choices, exposed as decoder::OutputRange: Full (default — samples use all 2^b levels) and Video (1..=2^b−2, avoiding the BT.601/709 sync-reference codes). Only the Y/Cb/Cr clamp changes; alpha always maps the full opacity range per §7.5.2.

Alpha

4444 / 4444 XQ code a lossless per-pixel alpha channel (§5.3.3 + §7.1.2 run-length + differential VLC); 4:2:2 + alpha is emitted as bitstream_version 1 per §6.4 — a combination this decoder and the reference decoder both honour even though common encoders never emit it.

Two ways to consume/produce it:

  • Typed contract — request Yuva422P / Yuva444P (oxideav-core ≥ 0.1.30): decode always returns 4 planes (16-bit coded alpha is demoted per §7.5.2, streams without alpha get an opaque plane); encode requires 4-plane 8-bit-alpha input and rejects anything else with a self-explaining error.
  • Untyped convention — request Yuv4(2|4)4P{,10Le,12Le}: a 4th plane is appended when the stream codes alpha (detect via planes.len() == 4), preserving 10/12-bit colour with §7.5.2-converted alpha and 16-bit alpha input on the encoder. Deep typed alpha would need Yuva*P10/12/16Le core formats (not yet in the enum).

The encoder auto-detects 4-plane input on the config path (8- vs 16-bit alpha inferred from the plane), and rate control carries the lossless alpha blob through every trial. One reference-bitstream finding worth knowing: the §7.5.3 per-slice alpha array is always the full 16-row macroblock height — the visible-row exclusion governs which rows are written, not the coded length (pinned directly against reference bytes; standing DOCS-GAP candidate on the §7.5.3 wording).

Frame-header metadata

The descriptive header fields (aspect ratio, frame rate, colour primaries / transfer / matrix) are filled automatically from CodecParameters::frame_rate or explicitly via EncoderConfig::with_meta(FrameMeta { .. }). Each field has symmetric code⇄value helpers and typed Option<…> accessors on the parsed FrameHeader (Tables 3–6, §6.1.1), and FrameHeader::meta folds a parsed header back into a FrameMeta so a transcode forwards the source's metadata verbatim. This crate writes encoder identifier oxav.

4:4:4 interop note: all-zero (= "unknown") colour metadata is spec-legal, but at least one common third-party decoder guesses RGB for unknown 4444 metadata and then rejects the stream downstream. Tag real metadata (e.g. 1/1/1 for BT.709) via with_meta on any 4444 / 4444 XQ / alpha-bearing stream that leaves this crate.

Quantisation matrices

The §6.1.1 flags select custom vs default (all-4s) weight matrices, with the chroma flag falling back to the luma matrix when clear. The parsed header exposes both raw flags plus FrameHeader::quantization_matrix_source (CustomChroma / LumaCustom / Default). The encoder emits the smallest carriage that reproduces the configured pair (0, 1, or 2 inline tables — 20-, 84-, or 148-byte header); EncoderConfig::with_explicit_qmat_carriage opts into the always-both-tables form the reference streams ship, for byte-level header parity. Presets: EncoderConfig::perceptual* (JPEG-derived, quality-blended) and signature_for_profile (each profile's native in-the-wild matrix, pinned byte-for-byte against the reference corpus). Tables are stored in natural raster order (DC at index 0) — never zigzag.

Encoder controls

All through EncoderConfig + make_encoder_with_config:

  • with_quantization_index — §7.3/Table 15, 1..=224; per-profile defaults 8/6/4/2/2/1.
  • with_mbs_per_slice{1,2,4,8} (§5.3), default 8.
  • with_profile / for_profile — override the bit-rate heuristic.
  • with_rate_control — two-pass per-frame binary search to bit_rate / fps within ±5 %.
  • with_alpha_channel_type — explicit 8-/16-bit alpha; default auto-detects 4-plane input.
  • with_quant_matrices / perceptual* / signature_for_profile — see above.
  • with_min_frame_size — §5.1.2 constant-frame-size stuffing (padded frames decode bit-identically).
  • with_interlace_mode — 0 progressive / 1 TFF / 2 BFF.

Inputs are validated up front (ranges above, matrix weights 2..=63, dimensions 1..=65535 per the u16 header fields — out-of-range is refused, never truncated).

Conformance notes

  • Every §6.4 "decoder shall refuse" clause is enforced: bitstream_version > 1, v0 streams that aren't 4:2:2-no-alpha, reserved interlace_mode 3, matrix entries outside 2..=63. The encoder picks the lowest legal version (v0 / v1).
  • Declared-size advance (§6.4 version-variant forward compatibility): the decoder locates the next syntax structure from the declared frame_header_size / picture_size / slice_header_size — never by summing what it parsed — so future version variants that append informative bytes decode identically, while payloads exceeding their declared size are refused as corrupt.
  • FrameHeader::picture_geometry surfaces the §6.2 derivation (MB grid, interlaced field split, §7.5.3 crop) for muxer / stream-inspection callers.
  • ProRes RAW is detected and refused cleanly (aprn/aprh FourCCs + in-stream marker → typed Unsupported). RAW has no public bitstream specification — no SMPTE document, no published syntax — so a typed refusal is the correct behaviour until one exists.
  • FourCC ⇄ profile routing helpers live at the crate root (profile_for_fourcc / codec_id_for_fourcc / fourcc_for_profile); sample-table assembly belongs to the container crates.

Validation

Every profile self-roundtrips bit-exactly and cross-decodes against an external ProRes decoder (black-box only) at 58–68 dB luma PSNR across 8/10/12-bit, progressive + interlaced, and 4444 ± alpha; the alpha-typed surface is black-box validated in both directions. The full RDD 36 Annex A IDCT accuracy qualification passes with margin, and reference fixtures under docs/video/prores/fixtures/ pin decode and encode SHAs. At equal rate with signature matrices, the encoder measures above the reference encoder on every profile:

Profile reference ours @ equal rate Δ PSNR
Proxy 3 637 B, 46.2 dB 3 774 B, 46.4 dB +0.2
LT 10 815 B, 51.0 dB 11 415 B, 54.9 dB +3.9
Standard 15 686 B, 53.8 dB 14 697 B, 59.3 dB +5.5
HQ 23 801 B, 59.5 dB 22 589 B, 67.1 dB +7.6
4444 35 893 B, 59.0 dB 34 556 B, 64.7 dB +5.7
4444 XQ 51 640 B, 63.4 dB 48 572 B, 69.6 dB +6.2

Five panic-free cargo-fuzz targets drive the decode entry points, header parsers, and entropy coders (30-minute daily CI run), with the malformed-input behaviour also pinned by ordinary cargo test cases; Criterion benches cover the decode/encode hot paths with constant-block fast-path dispatch verified bit-exact against the general DCT loops.

License

MIT — see LICENSE.