Skip to main content

oxideav_videotoolbox/
lib.rs

1#![cfg(target_os = "macos")]
2//! macOS VideoToolbox hardware decode/encode bridge.
3//!
4//! This crate is a **runtime-loaded** bridge to Apple's
5//! [VideoToolbox](https://developer.apple.com/documentation/videotoolbox)
6//! framework. It uses [`libloading`] to `dlopen` the framework on
7//! first use, so:
8//!
9//! * macOS builds have **no compile-time link dependency** on
10//!   VideoToolbox; if the framework can't be loaded, the registered
11//!   factories return `Error::Unsupported` and the framework registry
12//!   falls back to the pure-Rust codec implementation.
13//! * No Objective-C / Swift involved. VideoToolbox is a C API; symbol
14//!   resolution + Core Foundation refcounting is all FFI.
15//!
16//! The crate is gated to `cfg(target_os = "macos")` at the source
17//! level: on Linux / Windows the entire crate compiles to an empty
18//! rlib, and consumers (umbrella `oxideav`) gate the `register` call
19//! behind the same cfg.
20//!
21//! # Status
22//!
23//! H.264 + HEVC decode + encode and JPEG + ProRes decode + encode are wired
24//! via `VTDecompressionSession` / `VTCompressionSession`. Round 4 added
25//! **MPEG-2 video decode** (`kCMVideoCodecType_MPEG2Video`, decode-only —
26//! VideoToolbox has no MPEG-2 encoder), with an elementary-stream framer
27//! that carves per-picture access units. Round 5 added **VP9 decode**
28//! (`kCMVideoCodecType_VP9` = `'vp09'`, decode-only — VT has no VP9 encoder
29//! either); VP9 frames are container-framed (IVF / Matroska / MP4) so each
30//! demuxed `Packet` is one access unit and the existing blob
31//! `FrameSplit::Whole` path applies unchanged. Round 6 added **MPEG-4
32//! Part 2 video decode** (`kCMVideoCodecType_MPEG4Video` = `'mp4v'` — the
33//! DivX / Xvid / ASP family, **not** H.264). VT exposes no MPEG-4 Part 2
34//! compression session, so it is decode-only as well; a new
35//! `FrameSplit::Mpeg4PartTwoEs` framer splits on VOP start codes
36//! (`00 00 01 B6`) and attaches preceding VOS / VOL / GOV headers to the
37//! first VOP. **Round 7 (this commit) closes the VOL-extradata follow-up**
38//! for MPEG-4 Part 2: the decoder sniffs the configuration prefix from the
39//! first packet, wraps it in an ISO/IEC 14496-1 ESDS descriptor, and feeds
40//! the result to `CMVideoFormatDescriptionCreate` via
41//! `kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms = { "esds"
42//! : CFData }`. PSNR_Y vs ffmpeg's software decode reaches ≈ 72.8 dB
43//! (sample-exact within IDCT tolerance) on the integration fixture.
44//! **Round 8 (this commit) adds AV1 video decode** via
45//! `kCMVideoCodecType_AV1 = 'av01'`. Decode-only — AV1 hardware decode
46//! is gated to Apple Silicon M3+, and VT falls back to its internal SW
47//! AV1 path elsewhere where available; an encoder factory is a
48//! follow-up round (VT's AV1 encode session is M3+ / macOS 14+ only).
49//! AV1 frames are container-framed (IVF / Matroska / MP4) so each
50//! demuxed `Packet` is one temporal unit and the blob
51//! `FrameSplit::Whole` path applies unchanged. All codec ids register
52//! with `priority = 10` and `hardware_accelerated = true`.
53//!
54//! # Workspace policy
55//!
56//! Calling a system OS framework via FFI is the same shape as calling
57//! `libc::malloc` — it's the platform, not a copied algorithm. The
58//! workspace's clean-room rule (no embedding source from libvpx,
59//! libwebp, libjxl, etc.) doesn't apply here.
60
61pub mod sys;
62
63#[cfg(feature = "registry")]
64pub mod blob;
65#[cfg(feature = "registry")]
66pub mod decoder;
67#[cfg(feature = "registry")]
68pub mod encoder;
69
70/// Register VideoToolbox hardware factories: H.264 / HEVC / JPEG / ProRes
71/// decode + encode, plus MPEG-2 video, VP9, MPEG-4 Part 2, and AV1 decode
72/// (the last four are decode-only — VideoToolbox exposes no MPEG-2 / VP9 /
73/// MPEG-4 Part 2 compression session at all, and the AV1 compression
74/// session is a follow-up round). If the framework cannot be loaded
75/// (older OS, sandboxed environment, non-macOS) the function logs and
76/// returns without registering anything — the runtime falls back to the
77/// pure-Rust impls.
78#[cfg(feature = "registry")]
79pub fn register(ctx: &mut oxideav_core::RuntimeContext) {
80    use oxideav_core::{CodecCapabilities, CodecId, CodecInfo, CodecTag};
81
82    // Confirm the framework loads before registering factories.
83    match sys::vtable() {
84        Ok(_) => {}
85        Err(e) => {
86            // Library not available (e.g. running under Rosetta on an old
87            // OS, or in a Linux cross-build test). Graceful no-op.
88            eprintln!("oxideav-videotoolbox: framework unavailable, skipping registration: {e}");
89            return;
90        }
91    }
92
93    // ── H.264 decoder ──────────────────────────────────────────────────────
94    let h264_caps = CodecCapabilities::video("h264_videotoolbox")
95        .with_lossy(true)
96        .with_intra_only(false)
97        .with_hardware(true)
98        .with_priority(10);
99
100    ctx.codecs.register(
101        CodecInfo::new(CodecId::new("h264"))
102            .capabilities(h264_caps.clone().with_decode())
103            .decoder(decoder::H264VtDecoder::make)
104            .tags([
105                CodecTag::fourcc(b"H264"),
106                CodecTag::fourcc(b"h264"),
107                CodecTag::fourcc(b"AVC1"),
108                CodecTag::fourcc(b"avc1"),
109                CodecTag::fourcc(b"X264"),
110                CodecTag::matroska("V_MPEG4/ISO/AVC"),
111            ]),
112    );
113
114    // ── H.264 encoder ──────────────────────────────────────────────────────
115    ctx.codecs.register(
116        CodecInfo::new(CodecId::new("h264"))
117            .capabilities(
118                CodecCapabilities::video("h264_videotoolbox")
119                    .with_lossy(true)
120                    .with_intra_only(false)
121                    .with_hardware(true)
122                    .with_priority(10)
123                    .with_encode(),
124            )
125            .encoder(encoder::make_h264_encoder),
126    );
127
128    // ── HEVC decoder ───────────────────────────────────────────────────────
129    let hevc_caps = CodecCapabilities::video("hevc_videotoolbox")
130        .with_lossy(true)
131        .with_intra_only(false)
132        .with_hardware(true)
133        .with_priority(10);
134
135    ctx.codecs.register(
136        CodecInfo::new(CodecId::new("hevc"))
137            .capabilities(hevc_caps.clone().with_decode())
138            .decoder(decoder::HevcVtDecoder::make)
139            .tags([
140                CodecTag::fourcc(b"hvc1"),
141                CodecTag::fourcc(b"hev1"),
142                CodecTag::matroska("V_MPEGH/ISO/HEVC"),
143            ]),
144    );
145
146    // ── HEVC encoder ───────────────────────────────────────────────────────
147    ctx.codecs.register(
148        CodecInfo::new(CodecId::new("hevc"))
149            .capabilities(
150                CodecCapabilities::video("hevc_videotoolbox")
151                    .with_lossy(true)
152                    .with_intra_only(false)
153                    .with_hardware(true)
154                    .with_priority(10)
155                    .with_encode(),
156            )
157            .encoder(encoder::make_hevc_encoder),
158    );
159
160    // ── JPEG decoder ───────────────────────────────────────────────────────
161    let jpeg_caps = CodecCapabilities::video("mjpeg_videotoolbox")
162        .with_lossy(true)
163        .with_intra_only(true)
164        .with_hardware(true)
165        .with_priority(10);
166
167    ctx.codecs.register(
168        CodecInfo::new(CodecId::new("mjpeg"))
169            .capabilities(jpeg_caps.clone().with_decode())
170            .decoder(blob::make_jpeg_decoder)
171            .tags([
172                CodecTag::fourcc(b"jpeg"),
173                CodecTag::fourcc(b"JPEG"),
174                CodecTag::fourcc(b"MJPG"),
175                CodecTag::fourcc(b"mjpg"),
176            ]),
177    );
178
179    // ── JPEG encoder ───────────────────────────────────────────────────────
180    ctx.codecs.register(
181        CodecInfo::new(CodecId::new("mjpeg"))
182            .capabilities(jpeg_caps.clone().with_encode())
183            .encoder(blob::make_jpeg_encoder),
184    );
185
186    // ── ProRes decoder ─────────────────────────────────────────────────────
187    let prores_caps = CodecCapabilities::video("prores_videotoolbox")
188        .with_lossy(true)
189        .with_intra_only(true)
190        .with_hardware(true)
191        .with_priority(10);
192
193    ctx.codecs.register(
194        CodecInfo::new(CodecId::new("prores"))
195            .capabilities(prores_caps.clone().with_decode())
196            .decoder(blob::make_prores_decoder)
197            .tags([
198                CodecTag::fourcc(b"apco"),
199                CodecTag::fourcc(b"apcs"),
200                CodecTag::fourcc(b"apcn"),
201                CodecTag::fourcc(b"apch"),
202                CodecTag::fourcc(b"ap4h"),
203                CodecTag::fourcc(b"ap4x"),
204            ]),
205    );
206
207    // ── ProRes encoder ─────────────────────────────────────────────────────
208    ctx.codecs.register(
209        CodecInfo::new(CodecId::new("prores"))
210            .capabilities(prores_caps.clone().with_encode())
211            .encoder(blob::make_prores_encoder),
212    );
213
214    // ── MPEG-2 decoder (decode-only — VT has no MPEG-2 encoder) ─────────────
215    let mpeg2_caps = CodecCapabilities::video("mpeg2_videotoolbox")
216        .with_lossy(true)
217        .with_intra_only(false)
218        .with_hardware(true)
219        .with_priority(10);
220
221    ctx.codecs.register(
222        CodecInfo::new(CodecId::new("mpeg2video"))
223            .capabilities(mpeg2_caps.clone().with_decode())
224            .decoder(blob::make_mpeg2_decoder)
225            .tags([
226                CodecTag::fourcc(b"mp2v"),
227                CodecTag::fourcc(b"MPG2"),
228                CodecTag::fourcc(b"mpg2"),
229                CodecTag::fourcc(b"hdv2"),
230                CodecTag::fourcc(b"m2v1"),
231                CodecTag::matroska("V_MPEG2"),
232            ]),
233    );
234
235    // ── VP9 decoder (decode-only — VT has no VP9 encoder) ───────────────────
236    let vp9_caps = CodecCapabilities::video("vp9_videotoolbox")
237        .with_lossy(true)
238        .with_intra_only(false)
239        .with_hardware(true)
240        .with_priority(10);
241
242    ctx.codecs.register(
243        CodecInfo::new(CodecId::new("vp9"))
244            .capabilities(vp9_caps.clone().with_decode())
245            .decoder(blob::make_vp9_decoder)
246            .tags([
247                CodecTag::fourcc(b"vp09"),
248                CodecTag::fourcc(b"VP90"),
249                CodecTag::matroska("V_VP9"),
250            ]),
251    );
252
253    // ── MPEG-4 Part 2 decoder (decode-only — VT has no MPEG-4 Pt 2 encoder) ─
254    //
255    // This is MPEG-4 Part 2 (Visual / ASP / SP) — the family that includes
256    // DivX and Xvid — **not** MPEG-4 Part 10 (H.264). H.264 is registered
257    // separately above with `kCMVideoCodecType_H264` (`'avc1'`).
258    let mpeg4_caps = CodecCapabilities::video("mpeg4_videotoolbox")
259        .with_lossy(true)
260        .with_intra_only(false)
261        .with_hardware(true)
262        .with_priority(10);
263
264    ctx.codecs.register(
265        CodecInfo::new(CodecId::new("mpeg4"))
266            .capabilities(mpeg4_caps.clone().with_decode())
267            .decoder(blob::make_mpeg4_part_two_decoder)
268            .tags([
269                CodecTag::fourcc(b"mp4v"),
270                CodecTag::fourcc(b"MP4V"),
271                CodecTag::fourcc(b"M4S2"),
272                CodecTag::fourcc(b"m4s2"),
273                CodecTag::fourcc(b"DIVX"),
274                CodecTag::fourcc(b"divx"),
275                CodecTag::fourcc(b"DX50"),
276                CodecTag::fourcc(b"XVID"),
277                CodecTag::fourcc(b"xvid"),
278                CodecTag::fourcc(b"FMP4"),
279                CodecTag::fourcc(b"fmp4"),
280                CodecTag::matroska("V_MPEG4/ISO/ASP"),
281            ]),
282    );
283
284    // ── AV1 decoder (decode-only — VT AV1 encoder is M3+/macOS 14+, future round)
285    //
286    // AV1 hardware decode is gated to Apple Silicon M3+ chips. On older
287    // hardware VideoToolbox falls back to its internal software AV1 path
288    // on macOS versions where it exists, and otherwise returns a non-zero
289    // `OSStatus` at session creation — the registry's SW fallback to the
290    // pure-Rust `oxideav-av1` decoder handles that case.
291    let av1_caps = CodecCapabilities::video("av1_videotoolbox")
292        .with_lossy(true)
293        .with_intra_only(false)
294        .with_hardware(true)
295        .with_priority(10);
296
297    ctx.codecs.register(
298        CodecInfo::new(CodecId::new("av1"))
299            .capabilities(av1_caps.clone().with_decode())
300            .decoder(blob::make_av1_decoder)
301            .tags([
302                CodecTag::fourcc(b"av01"),
303                CodecTag::fourcc(b"AV01"),
304                CodecTag::matroska("V_AV1"),
305            ]),
306    );
307
308    let _ = (
309        h264_caps,
310        hevc_caps,
311        jpeg_caps,
312        prores_caps,
313        mpeg2_caps,
314        vp9_caps,
315        mpeg4_caps,
316        av1_caps,
317    ); // suppress unused warnings
318}
319
320#[cfg(feature = "registry")]
321oxideav_core::register!("videotoolbox", register);