Skip to content

Commit e3664b8

Browse files
kixelatedmoq-bot[bot]claudequidamschwarznuts-rice
authored
feat(moq-video)!: make hardware encoders always-on (openh264 stays the software fallback) (#1819)
Co-authored-by: moq-bot[bot] <186640430+moq-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: quidamschwarz <113550619+quidamschwarz@users.noreply.github.com> Co-authored-by: nuts-rice <red44443@gmail.com>
1 parent d968662 commit e3664b8

14 files changed

Lines changed: 136 additions & 92 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ lto = "fat"
108108
codegen-units = 1
109109
strip = true
110110

111-
# moq-video's `nvenc` feature needs a `dynamic-loading` mode on
111+
# moq-video's always-on NVENC backend (Linux) needs a `dynamic-loading` mode on
112112
# nvidia-video-codec-sdk so libnvidia-encode is dlopen'd at runtime instead of
113113
# linked: that lets a portable moq-cli build (and load) on machines without the
114114
# NVIDIA driver, and lets a GPU-less CI/release builder compile it. Upstream

flake.nix

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@
107107
# cpal's `alsa-sys` (moq-audio `capture` feature) links libasound on
108108
# Linux via pkg-config; macOS uses CoreAudio, so no dep there.
109109
pkgs.alsa-lib
110-
# moq-video `vaapi` feature: the moq-vaapi crate (moq-dev/vaapi) vendors
111-
# the libva headers so bindgen needs no system libva, but libva is
112-
# dlopen'd at runtime, so this provides libva.so for actually running
113-
# vaapi encode in the devShell. macOS has no VAAPI.
110+
# moq-video's VAAPI backend (always-on for Linux): the moq-vaapi crate
111+
# (moq-dev/vaapi) vendors the libva headers so bindgen needs no system
112+
# libva, but libva is dlopen'd at runtime, so this provides libva.so for
113+
# actually running vaapi encode in the devShell. macOS has no VAAPI.
114114
pkgs.libva
115115
];
116116

nix/overlay.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ let
240240

241241
# Every system lib the workspace needs to compile with all features on:
242242
# bindgen (clang) for ffmpeg/vaapi, GStreamer for moq-gst, ALSA for
243-
# moq-audio's capture feature, libva for moq-video's vaapi feature. The
243+
# moq-audio's capture feature, libva for moq-video's VAAPI backend. The
244244
# Linux-only libs match the devShell's `lib.optionals (!isDarwin)` set.
245245
checkCommonArgs = {
246246
src = checkSrc;

rs/moq-boy/src/video.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ fn encoder_thread(
8989
let enc = match encoder.as_mut() {
9090
Some(enc) => enc,
9191
None => {
92-
// Game Boy is 160x144; force software (libx264) since hardware
93-
// encoders can reject such tiny resolutions.
92+
// Game Boy is 160x144; force the openh264 software encoder since
93+
// hardware encoders can reject such tiny resolutions.
9494
let mut config = moq_video::encode::Config::new(WIDTH, HEIGHT, 60);
9595
config.kind = moq_video::encode::Kind::Software;
9696
match moq_video::encode::Encoder::new(&config) {

rs/moq-video/Cargo.toml

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,24 @@ license = "MIT OR Apache-2.0"
88
version = "0.0.4"
99
edition = "2024"
1010
rust-version.workspace = true
11-
# Not publishable to crates.io until `moq-vaapi` (the `vaapi` feature's dep) is
12-
# published; it's an in-workspace path dependency for now. Flip back then.
13-
publish = false
1411

1512
keywords = ["quic", "http3", "webtransport", "media", "video"]
1613
categories = ["multimedia", "multimedia::video", "multimedia::encoding"]
1714

1815
[lib]
1916
doctest = false
2017

21-
[features]
22-
# NVIDIA NVENC hardware encoder (Linux). Off by default; enable for a build that
23-
# should use NVENC where present. Everything is dlopen'd at runtime: cudarc loads
24-
# CUDA, and nvidia-video-codec-sdk's `dynamic-loading` (a fork feature, see the
25-
# root [patch.crates-io]) loads libnvidia-encode. So an nvenc-enabled binary still
26-
# links on a GPU-less builder and still starts on machines without the NVIDIA
27-
# driver, falling back to software (see backend::open). `cuda-12020` pins the CUDA
28-
# API version so the build needs no CUDA toolkit.
29-
nvenc = ["dep:nvidia-video-codec-sdk", "nvidia-video-codec-sdk/dynamic-loading", "cudarc/fallback-dynamic-loading", "cudarc/cuda-12020"]
30-
# Intel/AMD VAAPI hardware encoder (Linux). Off by default; enable for a build
31-
# that should use VAAPI where present. The `moq-vaapi` crate (moq-dev/vaapi) builds
32-
# against its own vendored libva headers and dlopen's libva at runtime, so a
33-
# vaapi-enabled binary builds without system libva-dev, links on a libva-less
34-
# builder, and still loads on machines without libva, falling back to software
35-
# (see backend::open).
36-
vaapi = ["dep:moq-vaapi"]
37-
3818
[dependencies]
3919
anyhow = "1"
4020
bytes = "1"
41-
cudarc = { version = "0.19", optional = true, default-features = false, features = ["driver"] }
4221
# Catalog types (VideoConfig / VideoCodec) the decode consumer reads.
4322
hang = { workspace = true }
4423
moq-mux = { workspace = true }
4524
moq-net = { workspace = true }
46-
# VAAPI H.264 encoder, gated behind the `vaapi` feature + a linux cfg. Listed here
47-
# (not under a target table) so the feature can reference it on any host; the code
48-
# that uses it is `cfg(target_os = "linux")` and it only builds when `vaapi` is
49-
# enabled (which only happens on Linux, where libva headers exist).
50-
moq-vaapi = { workspace = true, optional = true }
51-
# NVENC backend, gated behind the `nvenc` feature + a linux cfg. Listed here
52-
# (not under a target table) so the feature can reference it on any host; the
53-
# code that uses it is `cfg(target_os = "linux")`.
54-
nvidia-video-codec-sdk = { version = "0.4", optional = true }
5525
# Vendored, statically-linked software H.264 fallback (no system dependency).
26+
# Always compiled in: hardware encoders (VideoToolbox / Media Foundation /
27+
# NVENC / VAAPI) are preferred, but openh264 is the fallback when none is usable
28+
# (GPU-less box, or a resolution a hardware encoder rejects).
5629
openh264 = "0.9.3"
5730
thiserror = "2"
5831
tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
@@ -61,6 +34,26 @@ tracing = "0.1"
6134
yuv = "0.8.14"
6235

6336
[target.'cfg(target_os="linux")'.dependencies]
37+
# NVENC hardware encoder, always-on for Linux. Everything is dlopen'd at runtime:
38+
# cudarc loads CUDA, and nvidia-video-codec-sdk's `dynamic-loading` (a fork
39+
# feature, see the root [patch.crates-io]) loads libnvidia-encode. So the binary
40+
# links on a GPU-less builder and still starts on machines without the NVIDIA
41+
# driver, falling back to the next encoder (see backend::open). `cuda-12020` pins
42+
# the CUDA API version so the build needs no CUDA toolkit.
43+
cudarc = { version = "0.19", default-features = false, features = ["driver", "fallback-dynamic-loading", "cuda-12020"] }
44+
# Probe for the NVIDIA driver libraries before calling cudarc / the NVENC SDK,
45+
# which panic (process-abort under release `panic = "abort"`) if their library is
46+
# absent. Lets a GPU-less host fall back to the next encoder instead of crashing.
47+
libloading = "0.8"
48+
# Intel/AMD VAAPI hardware encoder, always-on for Linux. Builds against its own
49+
# vendored libva headers and dlopen's libva at runtime, so it builds without
50+
# system libva-dev and still loads on machines without libva, falling back to the
51+
# next encoder (see backend::open).
52+
moq-vaapi = { workspace = true }
53+
# default-features off so the crate's `cuda-version-from-build-system` default
54+
# stays off: we pin the CUDA version via our own cudarc dep (cuda-12020) instead,
55+
# so the build needs no CUDA toolkit.
56+
nvidia-video-codec-sdk = { version = "0.4", default-features = false, features = ["dynamic-loading"] }
6457
# Native V4L2 webcam capture (replaces nokhwa). `v4l` streams MMAP buffers;
6558
# MJPEG cameras are decoded with the pure-Rust `zune-jpeg` (no libjpeg/IJG).
6659
v4l = "0.14"

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

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ encode/
102102
backend/
103103
mod.rs # Backend trait + open_backend(kind, config) fallback chain
104104
videotoolbox.rs # cfg(target_os = "macos")
105-
nvenc.rs # cfg(all(target_os = "linux", feature = "nvenc"))
106-
vaapi.rs # cfg(all(target_os = "linux", feature = "vaapi"))
105+
nvenc.rs # cfg(target_os = "linux")
106+
vaapi.rs # cfg(target_os = "linux")
107107
openh264.rs # software fallback, all platforms
108108
```
109109

@@ -214,36 +214,36 @@ Wrinkles to handle:
214214
```toml
215215
[features]
216216
default = [] # capture pulled in by moq-cli's `capture` feature
217-
nvenc = ["dep:nvidia-video-codec-sdk"] # linux only, opt-in
218-
vaapi = ["dep:moq-vaapi", "moq-vaapi/dlopen"] # linux only, opt-in
219-
# videotoolbox + openh264 need no feature: gated by cfg(target_os) / always-on
217+
software = ["dep:openh264"] # opt-in software fallback, all targets
220218

221219
[target.'cfg(target_os = "macos")'.dependencies]
222220
objc2-video-toolbox = "..."
223221
objc2-core-media = "..."
224222
objc2-core-video = "..."
225223

226224
[target.'cfg(target_os = "linux")'.dependencies]
227-
nvidia-video-codec-sdk = { version = "0.4", optional = true }
228-
moq-vaapi = { workspace = true, optional = true } # in-tree; vendored cros-libva + cros-codecs
225+
# Hardware encoders are always-on for Linux (cfg-gated, no feature). Both
226+
# dlopen their drivers at runtime, so they link on a GPU-less builder.
227+
nvidia-video-codec-sdk = { version = "0.4", features = ["dynamic-loading"] }
228+
moq-vaapi = "0.0.2" # standalone; vendored cros-libva + cros-codecs
229229

230230
[dependencies]
231-
openh264 = "..." # software fallback, all targets
232-
nokhwa = { version = "...", features = ["input-avfoundation", "input-v4l", "input-msmf", "decoding-mjpeg"] }
233-
dcv-color-primitives = "..."
231+
openh264 = "..." # always-on software fallback
234232
```
235233

236-
Release builds for Linux enable both `nvenc` and `vaapi`; the runtime fallback
237-
chain skips whichever driver is absent. Neither is a build-time hard dep on the
238-
driver, so the binary still builds and runs on a box with neither GPU (it falls
239-
to openh264).
234+
Hardware encoders are always-on (VideoToolbox on macOS, Media Foundation on
235+
Windows, NVENC + VAAPI on Linux); the runtime fallback chain skips whichever
236+
driver is absent. None is a build-time hard dep on the driver, so the binary
237+
still builds and runs on a box with no GPU. openh264 is always compiled in as
238+
the software fallback, so a GPU-less box still encodes (it's also what moq-boy
239+
uses for its tiny 160x144 frames, which hardware encoders may reject).
240240

241241
### Selection / fallback (`Kind` mapping)
242242

243243
`open_backend(kind, config)` builds an ordered candidate list and returns the
244244
first that opens, mirroring today's `open_encoder` loop:
245245

246-
- `Auto` -> \[videotoolbox | nvenc | vaapi] (cfg-filtered) then openh264.
246+
- `Auto` -> \[videotoolbox | nvenc | vaapi] (cfg-filtered), then openh264.
247247
- `Hardware` -> hardware-only; `NoEncoder` if none opens.
248248
- `Software` -> openh264 only.
249249
- `Named(id)` -> that backend only.
@@ -254,14 +254,13 @@ A backend "fails to open" (driver missing, no device) the same way an ffmpeg
254254

255255
## Packaging payoff
256256

257-
- **macOS**: VideoToolbox + openh264 link only system frameworks + a vendored
258-
static lib. One brew bottle per arch, no `Depends`.
257+
- **macOS**: VideoToolbox links only system frameworks. One brew bottle per
258+
arch, no `Depends`.
259259
- **Linux**: one binary that
260260

261-
- `dlopen`s `libnvidia-encode.so` if an NVIDIA driver is present (no build dep),
262-
- links `libva` (tiny, stable soname across releases) for Intel/AMD; package
263-
`Depends: libva2`, `Recommends: intel-media-va-driver | mesa-va-drivers`,
264-
- statically carries openh264 as the always-works fallback.
261+
- `dlopen`s `libnvidia-encode` if an NVIDIA driver is present (no build dep),
262+
- `dlopen`s `libva` for Intel/AMD (no build dep on libva-dev),
263+
- falls back to the always-compiled-in openh264 when no GPU encoder is usable.
265264

266265
That single artifact runs across Ubuntu 20.04 -> 24.04, Debian, Fedora, etc.,
267266
which is the whole reason for the change.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! GPU-resident NV12 texture ([`Frame::Texture`]) that the hardware encoder MFT
77
//! consumes zero-copy. Without a GPU (e.g. a headless VM) it falls back to the
88
//! source reader's software video processor, copying each sample to a packed CPU
9-
//! [`I420`] ([`Frame::I420`]) for openh264.
9+
//! [`I420`] ([`Frame::I420`]) the encoder uploads.
1010
1111
use std::ffi::c_void;
1212
use std::ptr;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! [`I420`] for the encoder. Two source formats cover essentially all UVC
55
//! webcams: YUYV (raw 4:2:2, resampled directly) and MJPEG (decoded to RGB with
66
//! the pure-Rust [`zune_jpeg`], then converted). This is the CPU path feeding
7-
//! openh264 / NVENC; there's no GPU surface here.
7+
//! NVENC / VAAPI / openh264; there's no GPU surface here.
88
99
use v4l::buffer::Type as BufType;
1010
use v4l::io::mmap::Stream as MmapStream;

rs/moq-video/src/encode/backend/mod.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
//!
1010
//! [`open`] picks the best backend for a [`Codec`](super::Codec) +
1111
//! [`Kind`](super::Kind): only candidates that support the requested codec are
12-
//! considered, hardware (platform-gated) before software.
12+
//! considered, hardware (platform-gated) before the always-available openh264
13+
//! software fallback.
1314
1415
use bytes::Bytes;
1516

@@ -25,10 +26,10 @@ mod videotoolbox;
2526
#[cfg(target_os = "windows")]
2627
mod mediafoundation;
2728

28-
#[cfg(all(target_os = "linux", feature = "nvenc"))]
29+
#[cfg(target_os = "linux")]
2930
mod nvenc;
3031

31-
#[cfg(all(target_os = "linux", feature = "vaapi"))]
32+
#[cfg(target_os = "linux")]
3233
mod vaapi;
3334

3435
/// An opened video encoder. Feed it frames at the configured resolution; get
@@ -67,21 +68,22 @@ const HARDWARE: &[Candidate] = &[
6768
codecs: &[Codec::H264, Codec::H265],
6869
open: mediafoundation::MediaFoundation::open,
6970
},
70-
#[cfg(all(target_os = "linux", feature = "nvenc"))]
71+
#[cfg(target_os = "linux")]
7172
Candidate {
7273
name: nvenc::NAME,
7374
codecs: &[Codec::H264, Codec::H265],
7475
open: nvenc::Nvenc::open,
7576
},
76-
#[cfg(all(target_os = "linux", feature = "vaapi"))]
77+
#[cfg(target_os = "linux")]
7778
Candidate {
7879
name: vaapi::NAME,
7980
codecs: &[Codec::H264],
8081
open: vaapi::Vaapi::open,
8182
},
8283
];
8384

84-
/// Software fallbacks, all platforms. Only H.264 (openh264) has one; H.265 is
85+
/// Software fallbacks, all platforms, always available so a box with no usable
86+
/// hardware encoder can still encode. Only H.264 (openh264) has one; H.265 is
8587
/// hardware-only. A slice so future software codecs slot in.
8688
const SOFTWARE: &[Candidate] = &[Candidate {
8789
name: openh264::NAME,

0 commit comments

Comments
 (0)