Search and browse the EXR Converter docs

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

GoalNotes
True 12-bit encode precision on 4444 / XQMid-bin proof in YUV domain (see tests). 422 codes 12-bit and decodes as 10-bit in other apps
Cross-platformWin / Linux / macOS in release CI
Nuitka standalone--include-module=exr_prores (normal extension module)
Honest UIExperimental · RDD-36 · not Apple-certified
Keep FFmpeg for everything elsePyAV 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)
PieceLocation
Rust cratenative/exr_prores/ (maturin)
Python façadesrc/core/oxideav_prores.py
Presetsprores_ox_proxy / lt / 422 / hq / 4444 / xq in VIDEO_CODECS
Convert branchrun_exr_to_video → _e2v_oxideav when key ∈ OXIDEAV_PRORES_KEYS
Buildmake oxideav-prores → scripts/build_oxideav_prores.py
CI / ReleaseRust 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)

TestPass criteria
YUV mid-binEncode Y=2048 vs Y=2080 → decode Δ > 12 (tests/test_oxideav_prores.py)
Probe4444/XQ: PyAV opens .mov as prores / ap4h or ap4x / yuv444p12le. 422 family probes as 10-bit (yuv422p10le), which is expected
Missing extensionPresets hidden; FFmpeg codecs still work
Frozen pathNuitka 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

SurfaceCopy
Software FFmpeg ProRes10-bit (prores_ks)
VideoToolbox 4444/XQ12-bit class (macOS)
oxideav 4444 / XQ12-bit · experimental · RDD-36 ProRes-compatible
oxideav 422 Proxy / LT / 422 / HQ12-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

DateDecision
2026-08-06Confirmed FFmpeg prores_ks cannot encode true 12-bit; labels fixed
2026-08-06Chose oxideav for experimental cross-platform 12-bit ProRes-compatible output
2026-08-21Revised: PyO3 bindings + in-process MOV writer (not subprocess sidecar); Nuitka --include-module=exr_prores
2026-09-28422 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

Source: EXR Converter on GitHub