Skip to content

Commit 9f8cdac

Browse files
kixelatedclaude
andauthored
feat(moq-video): native V4L2 capture, NVENC via dlopen, drop VAAPI + nokhwa(Linux) (#1704)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ff85227 commit 9f8cdac

14 files changed

Lines changed: 448 additions & 673 deletions

File tree

Cargo.lock

Lines changed: 89 additions & 408 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,11 @@ opt-level = "z"
101101
lto = "fat"
102102
codegen-units = 1
103103
strip = true
104+
105+
# moq-video's `nvenc` feature needs a `dynamic-loading` mode on
106+
# nvidia-video-codec-sdk so libnvidia-encode is dlopen'd at runtime instead of
107+
# linked: that lets a portable moq-cli build (and load) on machines without the
108+
# NVIDIA driver, and lets a GPU-less CI/release builder compile it. Upstream
109+
# doesn't have that feature yet, so consume a fork branch until it's merged.
110+
[patch.crates-io]
111+
nvidia-video-codec-sdk = { git = "https://github.com/kixelated/nvidia-video-codec-sdk.git", branch = "dynamic-loading" }

deny.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ allow = [
4747
"BSD-3-Clause",
4848
"CC0-1.0",
4949
"CDLA-Permissive-2.0", # webpki-roots cert bundle
50+
"IJG", # mozjpeg-sys (MJPEG decode via nokhwa in moq-video)
5051
"ISC",
5152
"MIT",
5253
"MPL-2.0",
@@ -66,3 +67,7 @@ allow-wildcard-paths = true
6667
unknown-registry = "deny"
6768
unknown-git = "deny"
6869
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
70+
# moq-video patches nvidia-video-codec-sdk to a fork that adds dlopen
71+
# (`dynamic-loading`); see the root Cargo.toml [patch.crates-io]. Drop this once
72+
# the feature is upstreamed and the patch is removed.
73+
allow-git = ["https://github.com/kixelated/nvidia-video-codec-sdk"]

rs/justfile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,19 @@ ci FILES="":
3838
fi
3939
just check --workspace
4040
cargo check --workspace --no-default-features
41-
cargo check --workspace --all-features
41+
# NVENC's SDK crate has no macOS bindings, so moq-video can't be part of the
42+
# blanket --all-features runs on a Mac. Exclude it and cover it on Linux only,
43+
# where the `nvenc` feature dlopens libnvidia-encode (so it builds, links, and
44+
# falls back to software without a GPU/driver).
45+
cargo check --workspace --all-features --exclude moq-video
4246
cargo deny check --show-stats
43-
just rs test --all-features
47+
cargo test --workspace --all-targets --all-features --exclude moq-video
48+
if [[ "$(uname)" == "Linux" ]]; then
49+
cargo check -p moq-video --all-targets --all-features
50+
cargo test -p moq-video --all-targets --all-features
51+
else
52+
cargo test -p moq-video --all-targets
53+
fi
4454
just build
4555
4656
# Auto-fix clippy/format/shear/sort, then sweep stale target/.

rs/moq-video/Cargo.toml

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,21 @@ categories = ["multimedia", "multimedia::video", "multimedia::encoding"]
1616
doctest = false
1717

1818
[features]
19-
# NVIDIA NVENC hardware encoder (Linux only). Off by default; release builds
20-
# for Linux enable it. cudarc loads CUDA dynamically (no build-time CUDA), and
21-
# the NVENC API lives in the driver, so this is not a build dependency.
22-
nvenc = ["dep:nvidia-video-codec-sdk", "cudarc/dynamic-loading", "cudarc/cuda-12020"]
23-
# Intel/AMD VAAPI hardware encoder (Linux only). Off by default; release builds
24-
# for Linux enable it. Links libva (stable soname); pairs with a V4L2 dmabuf
25-
# capture for a fully zero-copy path.
26-
vaapi = ["dep:cros-codecs"]
19+
# NVIDIA NVENC hardware encoder (Linux). Off by default; enable for a build that
20+
# should use NVENC where present. Everything is dlopen'd at runtime: cudarc loads
21+
# CUDA, and nvidia-video-codec-sdk's `dynamic-loading` (a fork feature, see the
22+
# root [patch.crates-io]) loads libnvidia-encode. So an nvenc-enabled binary still
23+
# links on a GPU-less builder and still starts on machines without the NVIDIA
24+
# driver, falling back to software (see backend::open). `cuda-12020` pins the CUDA
25+
# API version so the build needs no CUDA toolkit.
26+
nvenc = ["dep:nvidia-video-codec-sdk", "nvidia-video-codec-sdk/dynamic-loading", "cudarc/fallback-dynamic-loading", "cudarc/cuda-12020"]
2727

2828
[dependencies]
2929
anyhow = "1"
3030
bytes = "1"
31-
# VAAPI H.264 encoder + dmabuf import. Gated behind `vaapi` + a linux cfg; pinned
32-
# (pre-1.0 churns). Pulls libva (cros-libva) when the feature is on.
33-
cros-codecs = { version = "=0.0.6", optional = true, default-features = false, features = ["vaapi"] }
34-
cudarc = { version = "0.16", optional = true, default-features = false, features = ["driver"] }
31+
cudarc = { version = "0.19", optional = true, default-features = false, features = ["driver"] }
3532
moq-mux = { workspace = true }
3633
moq-net = { workspace = true }
37-
# Pure-Rust webcam capture (avfoundation / v4l2 / msmf), decoding MJPEG/YUYV to
38-
# RGBA. Platform input backends are enabled per-target below.
39-
nokhwa = { version = "0.10.11", default-features = false, features = ["decoding"] }
4034
# NVENC backend, gated behind the `nvenc` feature + a linux cfg. Listed here
4135
# (not under a target table) so the feature can reference it on any host; the
4236
# code that uses it is `cfg(target_os = "linux")`.
@@ -50,12 +44,14 @@ tracing = "0.1"
5044
yuv = "0.8.14"
5145

5246
[target.'cfg(target_os="linux")'.dependencies]
53-
nokhwa = { version = "0.10.11", features = ["input-v4l"] }
47+
# Native V4L2 webcam capture (replaces nokhwa). `v4l` streams MMAP buffers;
48+
# MJPEG cameras are decoded with the pure-Rust `zune-jpeg` (no libjpeg/IJG).
49+
v4l = "0.14"
50+
zune-jpeg = "0.5"
5451

5552
[target.'cfg(target_os="macos")'.dependencies]
5653
block2 = "0.6.2"
5754
dispatch2 = "0.3.1"
58-
nokhwa = { version = "0.10.11", features = ["input-avfoundation"] }
5955
objc2 = "0.6.4"
6056
objc2-av-foundation = "0.3.2"
6157
objc2-core-foundation = "0.3.2"
@@ -66,4 +62,6 @@ objc2-screen-capture-kit = "0.3.2"
6662
objc2-video-toolbox = "0.3.2"
6763

6864
[target.'cfg(target_os="windows")'.dependencies]
69-
nokhwa = { version = "0.10.11", features = ["input-msmf"] }
65+
# Windows still uses nokhwa (Media Foundation) until a native MF capture lands;
66+
# `decoding` handles its MJPEG/YUYV -> RGBA conversion.
67+
nokhwa = { version = "0.10.11", default-features = false, features = ["input-msmf", "decoding"] }

rs/moq-video/DESIGN-native-codecs.md

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,63 @@ Where the implementation differs from the plan above:
351351
`write` matches NVENC's pitch (safe only for 64-aligned widths; we warn otherwise);
352352
(3) forced-IDR via `picture_type` with picture-type-decision enabled.
353353

354+
### VAAPI removed (cros-codecs is not buildable on modern libva)
355+
356+
The VAAPI backend (added behind the `vaapi` feature in #1691) and the V4L2 dmabuf
357+
capture written to feed it have been **removed**. `cros-codecs 0.0.6` (the latest)
358+
caret-pins `cros-libva 0.0.12`, which does not compile against libva >= 2.23: the
359+
newer headers add `seg_id_block_size` / `va_reserved8` to
360+
`VAEncPictureParameterBufferVP9`, and `cros-libva 0.0.12`'s struct literal omits
361+
them (`cros-libva 0.0.13` fixes it, but `cros-codecs 0.0.6` won't accept it). Since
362+
modern distros and the nix toolchain ship libva 2.23+, the feature could not build
363+
or ship, and CI's `--all-features` could not be made green while it existed.
364+
`cros-codecs` is the only pure-Rust VAAPI H.264 encoder, and reintroducing ffmpeg
365+
for VAAPI defeats the purpose of this effort, so VAAPI is parked until `cros-codecs`
366+
releases against `cros-libva >= 0.0.13` (or a better VAAPI path appears). Intel/AMD
367+
Linux falls back to the openh264 software encoder meanwhile; NVIDIA uses NVENC.
368+
369+
When it returns: re-add `backend/vaapi.rs`, `capture/v4l2.rs`, the `vaapi` feature
370+
(+ `cros-codecs`/`v4l`/`libc` deps), `Frame::DmaBuf`, the `requires_dmabuf` capture
371+
coupling, and `libva` in the nix devShell. See this PR's history for the prior
372+
implementation; the V4L2 capture (REQBUFS/EXPBUF/QBUF/DQBUF) was compile-verified
373+
but never runtime-tested.
374+
375+
### NVENC ships via dlopen (no driver dependency at build or load)
376+
377+
For the "single binary reaches the GPU at runtime" goal, NVENC must not hard-link
378+
the driver. The stock `nvidia-video-codec-sdk` emits
379+
`cargo:rustc-link-lib=nvidia-encode` / `nvcuvid`, which would make an
380+
`--features nvenc` binary (a) impossible to link on a GPU-less builder and (b)
381+
fail to even load on a machine without the NVIDIA driver (`DT_NEEDED
382+
libnvidia-encode.so.1`), before `backend::open`'s software fallback could run.
383+
384+
So `nvenc` dlopens everything at runtime, like `cudarc` does for CUDA:
385+
386+
- `cudarc/fallback-dynamic-loading` dlopens `libcuda`; `cudarc/cuda-12020` pins the
387+
CUDA API version so the build needs no CUDA toolkit.
388+
- `nvidia-video-codec-sdk/dynamic-loading` dlopens `libnvidia-encode`. This is a
389+
small fork feature (see the root `[patch.crates-io]`): the SDK already routes
390+
every call through a function table built from two entry points
391+
(`NvEncodeAPICreateInstance` / `GetMaxSupportedVersion`); `dynamic-loading`
392+
resolves those two via `dlopen` instead of linking them, and `build.rs` skips the
393+
link directives.
394+
395+
Result, verified on a GPU-less Linux box: `--features nvenc` builds, links, and the
396+
test suite runs and passes (NVENC unavailable -> falls back to openh264), and the
397+
binary has no `libnvidia-encode` / `libcuda` `DT_NEEDED`. So one portable `moq-cli`
398+
can carry NVENC and use it only where the driver is present. Upstream the
399+
`dynamic-loading` feature and drop the patch once merged.
400+
354401
### Follow-ups
355402

356-
- VAAPI backend (phase 6) in a separate PR, behind a `vaapi` feature.
357-
- NVENC hardware validation on Linux+GPU / CI.
358-
- Live camera run (capture needs camera permission; only synthetic-frame encode is
359-
tested here).
403+
- CI builds + tests NVENC normally on Linux (`cargo {check,test} -p moq-video
404+
--all-features`); the dlopen feature means no GPU/driver and no special flags are
405+
needed. moq-video stays excluded from the *workspace* `--all-features` runs only
406+
because its SDK crate has no macOS bindings (see `rs/justfile`'s `ci` recipe).
407+
- Still needed on real hardware: NVENC encode validation on a Linux+GPU box (pitch
408+
alignment, forced-IDR); only synthetic-frame software encode is tested here.
409+
- Live camera run (capture needs camera/screen TCC permission, which a headless or
410+
agent-spawned process can't obtain; run `moq-cli ... capture` from a user terminal).
360411
- `doc/bin/cli.md`: the `capture` device-string semantics changed (index-or-path).
361412
- Consider reusing NVENC input/output buffers across frames (currently allocated
362413
per frame to sidestep the self-referential Session borrow).

rs/moq-video/src/capture/mod.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
//! per-source:
33
//! - macOS camera -> AVFoundation, screen -> ScreenCaptureKit, both yielding
44
//! zero-copy `CVPixelBuffer` surfaces straight to VideoToolbox.
5-
//! - other platforms -> [`nokhwa`](https://crates.io/crates/nokhwa) camera
6-
//! (CPU RGBA -> I420) until a native zero-copy path (V4L2 dmabuf / PipeWire)
7-
//! lands.
5+
//! - Linux camera -> native V4L2 (YUYV / MJPEG -> CPU I420).
6+
//! - Windows camera -> [`nokhwa`](https://crates.io/crates/nokhwa) (Media
7+
//! Foundation), CPU RGBA -> I420, until a native MF capture lands.
88
//!
99
//! [`encode::publish_capture`](crate::encode::publish_capture) consumes [`Config`].
1010
@@ -18,7 +18,12 @@ mod queue;
1818
#[cfg(target_os = "macos")]
1919
mod screencapture;
2020

21-
#[cfg(not(target_os = "macos"))]
21+
// Native V4L2 camera capture on Linux.
22+
#[cfg(target_os = "linux")]
23+
mod v4l2;
24+
25+
// nokhwa CPU webcam: Windows only, until a native Media Foundation capture lands.
26+
#[cfg(target_os = "windows")]
2227
mod webcam;
2328

2429
/// What to capture.
@@ -72,10 +77,20 @@ pub(crate) fn open(config: &Config) -> Result<Box<dyn FrameSource>, Error> {
7277
{
7378
Ok(Box::new(avfoundation::Camera::open(config)?))
7479
}
75-
#[cfg(not(target_os = "macos"))]
80+
#[cfg(target_os = "linux")]
81+
{
82+
Ok(Box::new(v4l2::Camera::open(config)?))
83+
}
84+
#[cfg(target_os = "windows")]
7685
{
7786
Ok(Box::new(webcam::Camera::open(config)?))
7887
}
88+
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
89+
{
90+
Err(Error::Codec(anyhow::anyhow!(
91+
"camera capture is not supported on this platform"
92+
)))
93+
}
7994
}
8095
Source::Display => {
8196
#[cfg(target_os = "macos")]

0 commit comments

Comments
 (0)