Skip to content

Commit 8396359

Browse files
kixelatedclaude
andauthored
feat(moq-video): native Windows Media Foundation capture, drop nokhwa (#1716)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9f8cdac commit 8396359

9 files changed

Lines changed: 422 additions & 283 deletions

File tree

Cargo.lock

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

doc/bin/cli.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,14 @@ moq-cli publish --url https://relay.example.com --broadcast cam.hang capture --n
8888
moq-cli publish --url https://relay.example.com --broadcast cam.hang capture --no-video
8989
```
9090

91-
Video capture uses the platform backend (avfoundation on macOS, v4l2 on Linux,
92-
dshow on Windows) and picks a hardware encoder (`h264_videotoolbox` /
93-
`h264_nvenc` / `h264_vaapi`) when one is present, falling back to software
94-
(`libx264`); force either with `--hardware` / `--software`. Audio capture uses
95-
cpal (CoreAudio / WASAPI / ALSA) and encodes Opus.
91+
Video capture uses a native per-platform backend (AVFoundation on macOS, V4L2 on
92+
Linux, Media Foundation on Windows) and picks a hardware H.264 encoder
93+
(VideoToolbox on macOS, NVENC on Linux NVIDIA) when one is present, falling back
94+
to the built-in software encoder (openh264); force either with `--hardware` /
95+
`--software`. `--camera` takes a bare integer as a device index, otherwise a
96+
device path (Linux) or name (a friendly-name substring on Windows, the
97+
AVFoundation `uniqueID` on macOS). Audio capture uses cpal (CoreAudio / WASAPI /
98+
ALSA) and encodes Opus.
9699

97100
Alternatively, pipe an external FFmpeg process as MPEG-TS:
98101

rs/moq-video/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ objc2-screen-capture-kit = "0.3.2"
6262
objc2-video-toolbox = "0.3.2"
6363

6464
[target.'cfg(target_os="windows")'.dependencies]
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"] }
65+
# Native Media Foundation webcam capture (replaces nokhwa). An `IMFSourceReader`
66+
# with its video processor enabled converts the camera's native format to NV12,
67+
# which we deinterleave to I420. Links only system import libs, no vendor SDK.
68+
windows = { version = "0.62", features = ["Win32_Foundation", "Win32_System_Com", "Win32_Media_MediaFoundation"] }

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

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# Native H.264 codecs for moq-video (drop ffmpeg)
22

33
Status: **phases 2-5 implemented** (openh264, VideoToolbox, NVENC, capture swap +
4-
ffmpeg removal). VAAPI (phase 6) is a separate follow-up PR. See "As built"
5-
at the bottom for where the implementation diverged from this plan.
4+
ffmpeg removal). Capture is now native on all three platforms (AVFoundation /
5+
ScreenCaptureKit on macOS, V4L2 on Linux, Media Foundation on Windows), so
6+
**nokhwa is fully removed**. VAAPI (phase 6) stays parked until cros-codecs
7+
builds against modern libva. See "As built" at the bottom for where the
8+
implementation diverged from this plan.
69

710
## Goal
811

@@ -329,10 +332,15 @@ Where the implementation differs from the plan above:
329332
negotiated resolution, so capture frames already match the encoder; `encode_rgba`
330333
now requires input dims == encoder dims (it errors otherwise) instead of
331334
rescaling. This dropped swscale entirely.
332-
- **Capture (nokhwa) yields RGBA.** `Camera::read` decodes each frame to RGBA via
333-
`decode_image::<RgbAFormat>`, uniformly handling MJPEG/YUYV/NV12 sources. Device
334-
string is now "bare integer = index, else path/name" (the avfoundation `:none` /
335-
dshow `video=` specifics are gone).
335+
- **Capture is per-platform native (nokhwa fully removed).** Each platform's
336+
`Camera::read` yields a `Frame` the encoder can take: macOS hands VideoToolbox a
337+
zero-copy `CVPixelBuffer` surface, Linux V4L2 and Windows Media Foundation hand
338+
the software/NVENC path a CPU `I420`. macOS AVFoundation/ScreenCaptureKit and
339+
Linux V4L2 (YUYV resampled, MJPEG via `zune-jpeg`) landed first; Windows uses an
340+
`IMFSourceReader` with its video processor enabled to coerce the camera's native
341+
format to NV12, which we deinterleave to I420 (`I420::from_nv12`). The device
342+
string is uniform: "bare integer = index, else path/name" (a friendly-name
343+
substring on Windows).
336344
- **`Error` slimmed.** The ffmpeg-specific variants (`Ffmpeg`, `NoCaptureBackend`,
337345
`NoVideoStream`) and the `From<ffmpeg_next::Error>` impl are removed; capture and
338346
encode failures now flow through `Error::Codec(anyhow)`.
@@ -350,6 +358,15 @@ Where the implementation differs from the plan above:
350358
default - feature unification here may need tweaking; (2) the flat input-buffer
351359
`write` matches NVENC's pitch (safe only for 64-aligned widths; we warn otherwise);
352360
(3) forced-IDR via `picture_type` with picture-type-decision enabled.
361+
- **Windows Media Foundation capture is UNVERIFIED on hardware.** The `windows`
362+
0.62 FFI is fully type-checked against the `x86_64-pc-windows-msvc` target (the
363+
whole crate can't cross-compile from the dev Mac because openh264's vendored C++
364+
build needs MSVC, but a scratch crate confirms every Media Foundation call), and
365+
the COM/refcount lifecycle is reviewed. Still needs a real Windows + webcam run
366+
to confirm: (1) the source reader's video processor actually delivers NV12 for
367+
common cameras (MJPEG/YUY2 sources); (2) `IMF2DBuffer::ContiguousCopyTo` yields
368+
unpadded NV12 so the I420 deinterleave is correct; (3) the on-demand
369+
open/`source.Shutdown()` cycle releases the camera (LED off) and reopens cleanly.
353370

354371
### VAAPI removed (cros-codecs is not buildable on modern libva)
355372

@@ -406,8 +423,9 @@ can carry NVENC and use it only where the driver is present. Upstream the
406423
because its SDK crate has no macOS bindings (see `rs/justfile`'s `ci` recipe).
407424
- Still needed on real hardware: NVENC encode validation on a Linux+GPU box (pitch
408425
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).
411-
- `doc/bin/cli.md`: the `capture` device-string semantics changed (index-or-path).
426+
- Live camera run (capture needs camera/screen permission, which a headless or
427+
agent-spawned process can't obtain; run `moq-cli ... capture` from a user
428+
terminal). On Windows this is also where the Media Foundation path gets its
429+
first real exercise (see the unverified note above).
412430
- Consider reusing NVENC input/output buffers across frames (currently allocated
413431
per frame to sidestep the self-referential Session borrow).

0 commit comments

Comments
 (0)