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);