Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,5 +327,10 @@ jobs:
- name: Install just
run: cargo install --locked just@1.52.0

# `--all-features` includes moq-video's `vpx`, whose build script finds
# libvpx through pkg-config. Nix provides it everywhere else.
- name: Install libvpx
run: brew install libvpx pkgconf

- name: Check
run: just rs macos
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ moq ... play --delay 500ms # trade latency for a jittery link
```

Decodes H.264, H.265, and AV1 video and Opus, PCM, and AAC-LC audio using
the platform hardware decoder where available. `--video-name` and
the platform hardware decoder where available. The opt-in `vpx` feature adds
software VP8 and VP9 (8-bit 4:2:0) through libvpx, which the build host must
provide. `--video-name` and
`--audio-name` pick a rendition.

Playback runs on a clock it owns. `--delay` (default 100 ms) is how far it
Expand Down
5 changes: 3 additions & 2 deletions doc/lib/rs/moq-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ ffmpeg, no GStreamer, no system codec to install.
| --- | --- | --- |
| `capture` | Camera, display, window, or application frames | AVFoundation + ScreenCaptureKit (macOS), V4L2 + X11/portal + PipeWire (Linux), Media Foundation + DXGI (Windows) |
| `encode` | Frames to H.264/H.265, published as a hang track | VideoToolbox, Media Foundation, NVENC, VAAPI, V4L2 M2M, MediaCodec (Android), openh264 |
| `decode` | A subscribed track back to frames | VideoToolbox, Media Foundation/DXVA, NVDEC, VAAPI, V4L2 M2M, MediaCodec (Android), openh264 |
| `decode` | A subscribed track back to frames | VideoToolbox, Media Foundation/DXVA, NVDEC, VAAPI, V4L2 M2M, MediaCodec (Android), openh264, libvpx |
| `render` | A frame as a `wgpu` texture | wgpu, with zero-copy Metal and Vulkan imports |

Highlights:

- **Automatic backend selection**, hardware first. Linux GPU libraries are `dlopen`ed at runtime, so one binary starts anywhere and warns when it falls back to software. openh264 (the default-on `openh264` feature) is statically linked as the H.264 fallback; H.265 is hardware-only; AV1 decodes via NVDEC. The VAAPI encoder, decoder, and GPU resize share one render node: the first whose driver does all three, or the one the `MOQ_VAAPI_DEVICE` environment variable names (for example `/dev/dri/renderD129`).
- **Automatic backend selection**, hardware first. Linux GPU libraries are `dlopen`ed at runtime, so one binary starts anywhere and warns when it falls back to software. openh264 (the default-on `openh264` feature) is statically linked as the H.264 fallback; H.265 is hardware-only; AV1 decodes via NVDEC. VP8 and VP9 decode in software through libvpx (the opt-in `vpx` feature), 8-bit 4:2:0 only: other VP9 profiles are refused rather than converted. The VAAPI encoder, decoder, and GPU resize share one render node: the first whose driver does all three, or the one the `MOQ_VAAPI_DEVICE` environment variable names (for example `/dev/dri/renderD129`).
- **Publish on demand.** `encode::publish_capture` advertises the track up front and opens the camera only while someone subscribes.
- **GPU ownership where the platform allows.** Matching codec backends consume their native GPU surfaces directly. The renderer imports `CVPixelBuffer` and supported DMA-BUF formats. Linux/NVIDIA producers can import dedicated Vulkan RGBA8 slots into CUDA with timeline-semaphore ordering and completion-driven slot return. Vulkan/CUDA surfaces deliberately have no CPU pixel fallback; other surfaces use the typed `Surface::into_i420()` and configured `Surface::to_rgba(config)` when needed.
- **Live bitrate control** where the selected backend supports it, without forcing a keyframe. An unsupported backend keeps its opening rate.
Expand Down Expand Up @@ -71,6 +71,7 @@ cargo add moq-video --features render # wgpu rendering
cargo add moq-video --features v4l2 # Linux V4L2 M2M codecs, no system build deps
cargo add moq-video --features vaapi # Linux VAAPI codecs (bindgen needs libclang)
cargo add moq-video --features pipewire # Wayland screen + PipeWire cameras (links libpipewire)
cargo add moq-video --features vpx # VP8/VP9 decode (links libvpx; VPX_STATIC=1 for the archive)
cargo add moq-video --no-default-features --features openh264 # software H.264 only
cargo add moq-video --no-default-features --features nvidia # Linux NVIDIA only, no C++ or wgpu
```
Expand Down
8 changes: 8 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
glib
libressl
ffmpeg
# moq-video's `vpx` feature (VP8/VP9 software decode): libvpx-native-sys
# finds it through pkg-config, and `VPX_STATIC` below links the archive
# so nothing built here needs libvpx.so at runtime.
libvpx
curl
# MPEG-TS validation (tsp, tsanalyze) for the ts-compliance harness.
tsduck
Expand Down Expand Up @@ -515,6 +519,10 @@
# Exported rather than read back out of nix, so the guard costs a
# variable lookup instead of a nested evaluation of this flake.
OBS_LINKED_VERSION = obs-linked-version;

# Link libvpx statically for moq-video's `vpx` feature, the shape the
# quest ships: no system codec library at runtime.
VPX_STATIC = "1";
}
// pkgs.lib.optionalAttrs (!pkgs.stdenv.hostPlatform.isDarwin) {
ALSA_PLUGIN_DIR = "${alsaPlugins}/lib/alsa-lib";
Expand Down
2 changes: 1 addition & 1 deletion quest/m1/obs-moq-video/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The quests separate portable decoding, platform GPU delivery, audio, and publish
- [Rate control](/quest/m1/obs-moq-video/rate-control.md) - the plugin reserves its bitrate and retunes the OBS encoder to the grant
- [macOS GPU input](/quest/m1/obs-moq-video/macos.md) - feed the encoder from the OBS compositor without CPU readback
- [Windows GPU input](/quest/m1/obs-moq-video/windows.md) - import or blit OBS D3D11 textures with explicit synchronization
- [VP8/VP9 decoding](/quest/m1/obs-moq-video/vpx.md) - restore those playback codecs without an FFmpeg ABI dependency
- [VP8/VP9 in OBS](/quest/m1/obs-moq-video/vpx-obs.md) - play VP8 and VP9 through moq-video's libvpx backend on every OBS platform

## Related

Expand Down
2 changes: 1 addition & 1 deletion quest/m1/obs-moq-video/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ The MoQ source loads and plays supported video without directly linking FFmpeg l
- [Decoded frame ownership](/quest/m1/decoded-frames.md) - the decoded-frame consumer moq-ffi does not have yet
## Related

- [VP8/VP9 decoding](/quest/m1/obs-moq-video/vpx.md) - restores deferred codec coverage independently
- [VP8/VP9 in OBS](/quest/m1/obs-moq-video/vpx-obs.md) - restores deferred codec coverage once this lands
19 changes: 19 additions & 0 deletions quest/m1/obs-moq-video/vpx-obs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# [M] VP8/VP9 in the OBS source

## Goal

The OBS MoQ source plays VP8 and VP9 (8-bit 4:2:0) on every platform it ships for, through moq-video's libvpx backend with libvpx statically linked, and no FFmpeg ABI or undeclared system codec library.

## Plan

- moq-video decodes both codecs behind its opt-in `vpx` feature, with libvpx supplied by the build host (`libvpx-native-sys`, `VPX_STATIC=1`). Turn it on in moq-ffi and libmoq for the OBS builds only, not for every binding artifact.
- macOS and Linux build through Nix, which already provides libvpx. The Windows MSVC build runs without Nix and needs a static libvpx: evaluate vcpkg's `libvpx:x64-windows-static` against an in-tree vendored libvpx build (C-only config first, SIMD later) and pick one. The vendored build is also what a crates.io consumer without libvpx would need.
- Verify decoded pixels in the source, CPU delivery, reconnect, and rendition changes, and inspect the plugin's imports for libvpx, avcodec, and avutil on each platform.

## Required

- [Video source replacement](/quest/m1/obs-moq-video/source.md) - the source decodes through moq-video first

## Related

- [Codec coverage](/quest/m2/video-codec-coverage.md) - hardware VP8/VP9 decode belongs to that review
17 changes: 0 additions & 17 deletions quest/m1/obs-moq-video/vpx.md

This file was deleted.

7 changes: 4 additions & 3 deletions quest/m2/video-codec-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ and a portable software AV1 fallback such as
OS, extension, hardware, format, and fixture requirements; a browser playing
a codec is not proof that our native backend can open it.

NVIDIA AV1 encode/10-bit, VAAPI expansion, and VP8/VP9 already have quests.
Keep those owners. Windows NVIDIA support needs a demonstrated advantage over
NVIDIA AV1 encode/10-bit and VAAPI expansion already have quests. Keep those
owners. Software VP8/VP9 decode is libvpx behind moq-video's `vpx` feature;
hardware VP8/VP9 decode is in scope here. Windows NVIDIA support needs a demonstrated advantage over
the existing native path. If revisiting software AV1 encoding, measure the
target real-time workload and build cost rather than assuming all presets or
all hardware are equivalent. Optional codec dependencies stay optional.
Expand All @@ -31,4 +32,4 @@ Public API and wire: no changes during this study.

- [NVIDIA formats](/quest/m2/2147-moq-video-10-bit-hevc-and-av1-support-in-the-nvidia-codec.md) - existing AV1 encode and 10-bit scope
- [VAAPI](/quest/m4/video-vaapi.md) - existing Linux codec expansion
- [VP8/VP9](/quest/m1/obs-moq-video/vpx.md) - existing portable decoder scope
- [VP8/VP9 in OBS](/quest/m1/obs-moq-video/vpx-obs.md) - the software decoder reaching the OBS source
4 changes: 4 additions & 0 deletions rs/moq-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ vaapi = ["moq-video?/vaapi", "moq-transcode?/vaapi"]
# checks its bindings in. Only applies when `capture`, `transcode`, or `play`
# pulls in moq-video.
v4l2 = ["moq-video?/v4l2", "moq-transcode?/v4l2"]
# Software VP8 / VP9 decode through libvpx, opt-in like the moq-video feature it
# turns on because libvpx comes from the build host. Only applies when `play`
# pulls in moq-video.
vpx = ["moq-video?/vpx"]
Comment thread
kixelated marked this conversation as resolved.
# Screen capture for `capture --display` on Linux (xdg-desktop-portal + PipeWire)
# and PipeWire cameras for `capture --camera pipewire:<node>`, opt-in like moq-video's own `pipewire` because it links libpipewire-0.3 at
# build time. Only bites when `capture` pulls in moq-video and moq-audio.
Expand Down
8 changes: 6 additions & 2 deletions rs/moq-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1767,8 +1767,12 @@ mod tests {
let Command::Play(play) = &cli.stages[0] else {
panic!("expected play")
};
let err = play.validate().unwrap_err().to_string();
assert!(err.contains(codec), "{err}");
if cfg!(feature = "vpx") {
play.validate().unwrap();
} else {
let err = play.validate().unwrap_err().to_string();
assert!(err.contains(codec), "{err}");
}
}

let cli = Invocation::try_parse_from([
Expand Down
17 changes: 12 additions & 5 deletions rs/moq-cli/src/play/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ impl Args {
use crate::subscribe::VideoCodecArg;

anyhow::ensure!(
!matches!(self.select.video_codec, Some(VideoCodecArg::Vp8 | VideoCodecArg::Vp9)),
"`play` cannot decode vp8 or vp9; pass --video-codec h264, h265, or av1"
cfg!(feature = "vpx") || !matches!(self.select.video_codec, Some(VideoCodecArg::Vp8 | VideoCodecArg::Vp9)),
"`play` was built without the `vpx` feature, so it cannot decode vp8 or vp9; pass --video-codec h264, h265, or av1"
);
// The delay is the speaker's ring depth, so a value it cannot hold is
// refused here rather than after the pipeline has opened a device.
Expand Down Expand Up @@ -90,9 +90,16 @@ mod tests {
parse(&["--video-codec", "h264"]).validate().unwrap();
parse(&["--video-codec", "av1"]).validate().unwrap();

let err = parse(&["--video-codec", "vp9"]).validate().unwrap_err().to_string();
assert!(err.contains("vp8 or vp9"), "{err}");
assert!(parse(&["--video-codec", "vp8"]).validate().is_err());
// VP8 and VP9 decode only through the opt-in libvpx backend.
for codec in ["vp8", "vp9"] {
let result = parse(&["--video-codec", codec]).validate();
if cfg!(feature = "vpx") {
result.unwrap();
} else {
let err = result.unwrap_err().to_string();
assert!(err.contains("vp8 or vp9"), "{err}");
}
}
}

/// The delay is the playout offset and the staleness budget at once, so its
Expand Down
10 changes: 10 additions & 0 deletions rs/moq-video/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ nvidia = ["dep:cudarc", "dep:moq-nvenc", "dep:libloading"]
# Vendored, statically linked software H.264 encode/decode. Enabled by default,
# but separable from native codecs for hardware-only and minimal builds.
openh264 = ["dep:openh264", "dep:openh264-sys2"]
# Software VP8 and VP9 decode through libvpx, on every platform. Off by default
# because libvpx comes from the build host: libvpx-native-sys finds it through
# pkg-config (or `VPX_LIB_DIR` + `VPX_VERSION`) and ships pregenerated bindings,
# so no libclang is needed. Set `VPX_STATIC=1` to link the archive rather than
# libvpx.so, which is what the Nix dev shell does. Decodes 8-bit 4:2:0 only (VP8,
# and VP9 profile 0); anything else is refused rather than converted.
vpx = ["dep:libvpx-native-sys"]
# Intel/AMD VAAPI hardware codecs (Linux): the H.264 encoder and decoder. Off by
# default because moq-vaapi's build script runs bindgen over its vendored libva
# headers, so the build host needs libclang. At runtime libva is dlopen'd: the
Expand Down Expand Up @@ -106,6 +113,9 @@ bytes = { workspace = true }
fast_image_resize = "6"
# Catalog types (VideoConfig / VideoCodec) the decode consumer reads.
hang = { workspace = true }
# libvpx bindings for the `vpx` feature. The library itself comes from the build
# host; see the feature above.
libvpx-native-sys = { version = "5.0.17", optional = true }
moq-mux = { workspace = true }
moq-net = { workspace = true }
# Vendored, statically linked software H.264 fallback (no system dependency).
Expand Down
15 changes: 10 additions & 5 deletions rs/moq-video/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ a working software H.264 fallback but compiles vendored C++; disable defaults
and select native features to omit it. `nvidia` is Linux-only, `dlopen`s the
driver at runtime, and needs no build-time toolkit. `vaapi` is opt-in because
its bindgen needs libclang on the build host, while `v4l2` is opt-in only by
convention, since `moq-v4l` checks its bindings in. `render` is also opt-in so
codec-only consumers do not compile wgpu.
convention, since `moq-v4l` checks its bindings in. `vpx` is opt-in because
libvpx comes from the build host through pkg-config (set `VPX_STATIC=1` to link
the archive, as the Nix dev shell does). `render` is also opt-in so codec-only
consumers do not compile wgpu.

### Vulkan producers on NVIDIA

Expand Down Expand Up @@ -151,7 +153,7 @@ instead of asserting a threshold.
## Decode

`decode::Consumer` (the mirror of `moq_audio::decode::Consumer`) subscribes to an
H.264, H.265, or AV1 track and returns raw `Frame`s. A hardware-decoded frame stays
H.264, H.265, AV1, VP8, or VP9 track and returns raw `Frame`s. A hardware-decoded frame stays
on the GPU: feeding it back to a compatible hardware `encode::Encoder` on the
same device keeps it there (the transcode path), while `into_i420()` downloads
it. An encoder that can't take that surface (openh264, or a different device)
Expand Down Expand Up @@ -179,6 +181,7 @@ Backends are tried hardware-first, like encode:
| H.264 | OpenH264 (feature `openh264`, default) | VideoToolbox | Media Foundation (DXVA) | NVDEC (feature `nvidia`), VAAPI (feature `vaapi`) | MediaCodec (feature `mediacodec`, API 26+) |
| H.265 | none | VideoToolbox | Media Foundation (DXVA) | NVDEC (feature `nvidia`) | MediaCodec (feature `mediacodec`, API 26+) |
| AV1 | none | none | none | NVDEC (feature `nvidia`) | MediaCodec (feature `mediacodec`, when the device provides it) |
| VP8, VP9 | libvpx (feature `vpx`) | none | none | none | none |

On macOS VideoToolbox decodes H.264 and H.265 on hardware, pulling the parameter
sets (SPS/PPS, plus VPS for H.265) out of each keyframe to build the format
Expand All @@ -189,8 +192,10 @@ no software decoder, so it needs the GPU path (on Windows, an HEVC decoder MFT:
the inbox HEVC Video Extensions or a vendor one). On Linux, NVDEC decodes H.264,
H.265, and 8-bit 4:2:0 AV1 to CUDA NV12 frames; AV1 is decode-only and is useful
for AV1 source to H.264/H.265 transcode rungs. VAAPI decodes H.264 to DMA-BUF
surfaces the renderer imports without a download. A non-H.264/H.265/AV1
rendition yields `Error::UnsupportedCodec`.
surfaces the renderer imports without a download. libvpx decodes VP8 and VP9
profile 0 to CPU I420 on every platform; other VP9 profiles (4:4:4, 10-bit) are
refused rather than narrowed to 8-bit 4:2:0. Any other rendition yields
`Error::UnsupportedCodec`.

`decode::Config::output` says where decoded pictures live: `Output::Native`
(the default) hands back whatever the backend decoded into, a GPU surface or
Expand Down
6 changes: 6 additions & 0 deletions rs/moq-video/src/decode/backend/mediacodec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ impl MediaCodec {
Codec::H264 => MIME_H264,
Codec::H265 => MIME_H265,
Codec::Av1 => MIME_AV1,
other => {
return Err(Error::Codec(anyhow::anyhow!(
"MediaCodec {} decode is not wired",
other.label()
)));
}
};

// `GPU_SAMPLED_IMAGE` is what a consumer importing the buffer as a texture
Expand Down
Loading
Loading