12-bit ProRes (oxideav)
Status: Phase 0–2 landed (PyO3 bindings + app wiring + CI/Nuitka include)
Date: 2026-08-21 (plan 2026-08-06; implementation revised from sidecar → bindings)
Related code: native/exr_prores/, src/core/oxideav_prores.py, src/core/convert.py,
src/core/constants.py, tests/test_oxideav_prores.py
12-bit ProRes-compatible encode on Windows / Linux / macOS using the
pure-Rust oxideav-prores stack, linked in-process through a PyO3
extension (exr_prores) and included in Nuitka releases. No subprocess
sidecar. 4444 / XQ are 12-bit to standard decoders. 422 profiles code
a 12-bit lattice and present as 10-bit (see Labeling).
User-facing encoder comparison (software vs Apple VideoToolbox vs this path): ProRes and VideoToolbox.
1. Goals
| Goal | Notes |
|---|---|
| True 12-bit encode precision on 4444 / XQ | Mid-bin proof in YUV domain (see tests). 422 codes 12-bit and decodes as 10-bit in other apps |
| Cross-platform | Win / Linux / macOS in release CI |
| Nuitka standalone | --include-module=exr_prores (normal extension module) |
| Honest UI | Experimental · RDD-36 · not Apple-certified |
| Keep FFmpeg for everything else | PyAV remains default for 10-bit ProRes, DNxHR, HEVC, … |
Non-goals (v1):
- Replacing all ProRes with oxideav
- Apple-licensed / certified ProRes branding
- ProRes RAW / alpha on oxideav presets
2. Why not FFmpeg / why not a sidecar
FFmpeg prores_ks only accepts *10le pixel formats. True encode depth is
10-bit even when probe labels say 12. See historical mid-bin table in git
history / earlier revisions of this doc.
The original plan preferred a Rust CLI sidecar to defer PyO3 wheel
complexity. That was revised: a focused PyO3 module + minimal in-crate MOV
writer is cleaner for Nuitka (one extension next to other native deps, no
helper discovery / orphan processes) and matches how contributors already
build optional native code (make oxideav-prores).
oxideav-mp4 0.0.x cannot yet emit ProRes sample entries, so the extension
ships a small moov-at-end QuickTime writer (native/exr_prores/src/mov.rs)
instead of shelling out.
3. Architecture (implemented)
EXR Converter (Python / Nuitka)
OIIO + OCIO → display RGB (rgb48le uint16)
slate / burn-in / watermark (existing path)
│
▼ in-process PyO3
exr_prores.ProResMovWriter
RGB48 → BT.709 limited YUV422P12 (422) or YUV444P12 (4444/XQ)
oxideav-prores encode (signature matrices)
minimal MOV mux (ftyp + mdat + moov, colr/nclc BT.709)
│
▼
out.mov (4444/XQ: ap4h / ap4x, probed 12-bit)
(422 family: apco / apcs / apcn / apch, probed 10-bit)
| Piece | Location |
|---|---|
| Rust crate | native/exr_prores/ (maturin) |
| Python façade | src/core/oxideav_prores.py |
| Presets | prores_ox_proxy / lt / 422 / hq / 4444 / xq in VIDEO_CODECS |
| Convert branch | run_exr_to_video → _e2v_oxideav when key ∈ OXIDEAV_PRORES_KEYS |
| Build | make oxideav-prores → scripts/build_oxideav_prores.py |
| CI / Release | Rust toolchain + maturin develop before pytest / Nuitka |
Presets are omitted from available_video_codecs() when the extension is
not importable, so a Rust-less dev checkout still runs.
4. Acceptance (v1)
| Test | Pass criteria |
|---|---|
| YUV mid-bin | Encode Y=2048 vs Y=2080 → decode Δ > 12 (tests/test_oxideav_prores.py) |
| Probe | 4444/XQ: PyAV opens .mov as prores / ap4h or ap4x / yuv444p12le. 422 family probes as 10-bit (yuv422p10le), which is expected |
| Missing extension | Presets hidden; FFmpeg codecs still work |
| Frozen path | Nuitka includes exr_prores; experimental presets complete end-to-end |
RGB48 mid-bin (±32 on a 10-bit lattice) is not a strong signal after BT.709 limited conversion to 12-bit Y (~2 codes). Prefer the YUV-domain test.
5. Labeling
| Surface | Copy |
|---|---|
| Software FFmpeg ProRes | 10-bit (prores_ks) |
| VideoToolbox 4444/XQ | 12-bit class (macOS) |
| oxideav 4444 / XQ | 12-bit · experimental · RDD-36 ProRes-compatible |
| oxideav 422 Proxy / LT / 422 / HQ | 12-bit internal, 10-bit on decode · experimental · RDD-36 |
This output is not certified by Apple.
Why 422 still says 10-bit on decode
SMPTE RDD 36 does not store a bit depth for 422 profiles. Depth is the fourcc:
apco / apcs / apcn / apch mean 10-bit. Apple defines 12-bit only for
4444 and 4444 XQ. The oxideav writer still requests Yuv422P12Le and codes
DCT coefficients at 12-bit. ProRes level-shift math is v = s / 2^(b-9) - 256,
so a 12-bit encode read with a 10-bit assumption is exactly the top 10 bits.
FFmpeg, Resolve, and QuickTime therefore show a valid 10-bit 422 picture.
The extra two bits are in the file and unreachable to those decoders. GUI and
CLI labels say 12-bit internal, 10-bit on decode so a 422 preset is not
mistaken for a 12-bit delivery to another application. Finer internal
quantization can still improve the 10-bit result slightly; it does not make
ffprobe report 12-bit.
exr_converter exr2video -i seq/frame.0001.exr -o out.mov --codec prores_ox_422
ffprobe -show_entries stream=pix_fmt,bits_per_raw_sample,codec_tag_string out.mov
# yuv422p10le, 10, apcn
6. Remaining work
- Broader NLE interop checklist (Resolve / Premiere / FCP) before dropping “experimental”
- Optional alpha (
Yuva444P12Le) if product needs 4444+alpha on this path - Pipe/raw protocol / speed pass if RGB→YUV or encode becomes hot
- Promote UI copy from experimental → stable when interop is solid
7. Decision log
| Date | Decision |
|---|---|
| 2026-08-06 | Confirmed FFmpeg prores_ks cannot encode true 12-bit; labels fixed |
| 2026-08-06 | Chose oxideav for experimental cross-platform 12-bit ProRes-compatible output |
| 2026-08-21 | Revised: PyO3 bindings + in-process MOV writer (not subprocess sidecar); Nuitka --include-module=exr_prores |
| 2026-09-28 | 422 labels: keep 12-bit coefficient coding; UI/docs say third-party decode is 10-bit (apco/apcs/apcn/apch). 12-bit delivery other apps read remains 4444/XQ |
8. References
native/exr_prores/: PyO3 extension- oxideav-prores · SMPTE RDD 36
- ASWF Encode ProRes
- FFmpeg Trac #8054
Source: EXR Converter on GitHub