Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
PipeWire Virtual Camera for Rust
A small, focused Rust library (pipewire-vircam) for building PipeWire virtual
cameras from Rust: register a camera node, and fill its frames from your own
code. No v4l2loopback, no ffmpeg, no Python — just the official pipewire
Rust crate and a callback that hands you a fillable frame view whenever a
consumer connects.
This repo is a library + a working demo that proves it end to end:
pipewire-vircam(the crate) — the library. Configure a camera name and any set of (size, fps, format) modes; it registers the PipeWire node and calls your callback with a fillable, self-describing frame view each frame while a consumer is connected. The app's own code produces the pixels.redcam(a demo binary) — built onpipewire-vircam, fills every frame with solid red at 1920×1080 @ 30 fps by default. It is the runnable example and the target of the self-evaluation harness below, so "it works" is a machine-checked fact, not a claim.
Supports the uncompressed raw formats (RGBA/BGRA/BGRx/RGBx/BGR/RGB and I420/NV12/NV21/YUY2/UYVY/GREY) and multiple modes per camera.
No v4l2loopback, no ffmpeg, no Python — just the official pipewire Rust
crate and a small harness that proves, with no human eyeballing, that a real
PipeWire consumer actually receives red 1080p frames at the right rate.
src/ the `pipewire-vircam` crate (the primary deliverable)
lib.rs public API (Camera, Config, Mode, Format, Frame, …)
camera.rs node + stream + driver timer + callbacks
pod.rs SPA POD construction (EnumFormat / buffers / meta)
error.rs the error type
bin/redcam.rs the solid-red demo binary (harness target)
reference/ standalone C (not part of the crate) — built to repo root
redcam.c the same red camera in C (reference producer, `redcam-c`)
redcam-test.c the independent capture consumer + oracle (`redcam-test`)
ci.sh CI quality gate (fmt, clippy, package, test, arborist)
e2e.sh E2E harness (needs live PipeWire session)
Makefile `make`, `make test`, `make e2e`, `make clean`
The red-cam trio
Three files share the redcam stem and form one connected concept — the
same solid-red camera, demonstrated and verified in three ways:
src/bin/redcam.rs— the demo: the primary, built frompipewire-vircam. This is the harness target.reference/redcam.c— the reference implementation: the same red camera written standalone in C, no crate. It is a second, independent implementation of the concept (and a fallback you can point the harness at withRED_BIN=redcam-c). It is not kept in lockstep with the crate — it offers only the packed-raw formats.reference/redcam-test.c— the oracle: an independent C consumer that captures from any of the above and asserts the pixels, size, fps, and (optionally) sequence. It shares no code with any producer, so it is the honest check that frames really arrive red.
The crate (src/) is the deliverable; the two reference/ files are
standalone C that deliberately stays outside it. All three are named after
the thing they each are a form of: redcam.
Requirements
- A running PipeWire + WirePlumber session (this is a session-managed
node, not a raw core export).
wpctl statusshould work. - Rust (stable ≥ 1.80) +
cargo, andpkg-config libpipewire-0.3(PipeWire ≥ 1.x, SPA ≥ 0.2) for the C oracle. - A C compiler (gcc/clang) for the
redcam-testoracle andredcam-creference. - For the integration check only: GStreamer with the
pipewireplugin (gst-plugins-bad+gstreamer-plugins-pipewire), andImageMagick(identify/convert) to verify the captured PNG.
Build
Builds the Rust redcam (via cargo build --release) and the C
redcam-test oracle. make redcam-c also builds the C reference producer.
Run the camera
# or: make redcam && ./target/release/redcam
On startup it prints the node id and, as a consumer connects, the negotiated format, e.g.
node id: 90
stream state: "paused"
negotiated: format=11 1920x1080@30/1 stride=7680
stream state: "streaming"
It registers a node visible in wpctl status (under Video) and pw-cli ls
as:
node.name = "redcam"
MediaClass "Video/Source"
MediaName "Red Virtual Camera"
Leave it running and any PipeWire-aware consumer can select it.
Options
redcam [--name NAME] [--mode WxH@FPS]...
--mode is repeatable. The default is a single 1920x1080@30 mode offering
every supported format. (Formats are per-mode in Config; parsed --mode
values get the full supported set.)
Using the pipewire-vircam crate
The minimal example lives in examples/mycam.rs (built
by cargo build --examples). In brief:
use ;
// `Camera::new` creates the node and does NOT block. `.run(fill)` installs
// the driver timer and blocks until SIGINT/SIGTERM.
let cam = new?;
cam.run?;
Camera::newcreates the node and does not block..on_state(...)/.on_negotiated(...)are optional builders;.run(fill)blocks until SIGINT/SIGTERM.- The
fillclosure takes(&mut Frame, &Negotiated)and is called on the main-loop thread, at the negotiated fps, only while a consumer is connected and streaming.Frameis self-describing, so your code adapts to whatever the consumer negotiated. StatereportsDisconnected { error }/Paused { node_id }/Streaming { node_id };Negotiatedcarriesformat,width,height,fps_num/denom(plusfps()),stride,node_id.
How to consume redcam
Pick the node and open an input stream on it. Three concrete ways:
-
This repo's oracle (the self-evaluation):
NODE_ID=Captures 30 frames through a real PipeWire input stream and asserts every pixel is red, size is 1920×1080, and rate ≈ 30 fps.
-
GStreamer (a real third-party app — no ffmpeg):
target-object=redcamselects the node by name. You can also capture a few frames to a file:... num-buffers=10 ! ... ! filesink location=/tmp/red.png. -
Any PipeWire-aware app (e.g. OBS, a camera selector in your media stack) will see "Red Virtual Camera" as a capture device.
Self-evaluation (make e2e)
make e2e runs e2e.sh, which builds the Rust camera and the C oracle,
then for three sequences (two identical full 1080p sequences, plus a
multi-size/multi-fps sequence) starts the camera and checks:
- Registration — the node is in
pw-cliwithMediaClass "Video/Source"andMediaName "Red Virtual Camera". - Session manager — the node is visible in
wpctl status(Video tree) (full sequences). - Core assertion —
redcam-test(the independent C oracle) captures 30 frames per check and proves every pixel is red, the size is exact, and fps is within [24, 40] of the requested rate — for each of the 12 formats (full sequences) and for six size/fps/format combinations (1920×1080@30, 1280×720@60, 640×480@15). - Real-app integration — a GStreamer
pipewiresrc → videoconvert → pngencpipeline captures a frame from the node; the PNG is verified to be 1920×1080 and red (via ImageMagick) (full sequences). - Clean teardown — killing redcam removes the node; no error lines in the redcam log.
Exit code is 0 only if all 47 checks pass on all sequences. Every long-running process (redcam, consumers, gst pipelines) is backgrounded and always killed. It needs a live PipeWire/WirePlumber session.
make test runs ci.sh, the lean quality gate: cargo fmt, clippy,
package, test (unit tests + timing benchmarks) and an arborist complexity
check. No live session needed.
To test the C reference producer instead of the Rust one:
RED_BIN=redcam-c make e2e (after make redcam-c).
What the oracle asserts
| Check | Proves |
|---|---|
size_ok |
negotiated size is exactly the requested size (default 1920×1080) |
red_ok |
every pixel of every frame is solid red for the negotiated format |
fps |
frames arrive at ≈the requested fps (timer is driving, not a one-shot) |
frames |
N distinct frames were received |
seq_ok |
(best-effort) per-frame sequence advanced — only when the Header meta is negotiated |
red_ok is a full-pixel memcmp against a precomputed red row for the exact
negotiated format (RGB→FF0000, BGR→0000FF, RGBA→FF0000FF,
BGRA→0000FFFF), so it is exact, not a sampled/averaged check.
Why no ffmpeg / v4l2loopback?
The point is a PipeWire-native virtual camera: it registers as a normal
Video/Source node in the PipeWire/WirePlumber graph, so any
PipeWire-aware consumer (GStreamer pipewiresrc, your media stack, etc.) can
select it. v4l2loopback would fake a /dev/video device; here the camera
lives in the session, which is the more general and PipeWire-idiomatic
approach.
Design notes
- Official
pipewire0.10 crate, safe API throughout except three narrowsyscalls (pw_stream_connect,pw_stream_update_params, whose&mut [&Pod]arguments can't be built from owned PODs in this crate version, andpw_stream_is_lazy, which has no safe wrapper). pw_streamAPI (as in upstreamvideo-src.c/video-play.c), not a raw SPA node export — it handles buffer allocation and negotiation plumbing.- Fixed spec, all uncompressed formats. The demo advertises 1920×1080@30 with every format the crate can fill byte-exactly (the packed RGB family and I420/NV12/NV21/YUY2/UYVY/GREY), so we never negotiate a format we can't produce exactly. For YUV, "red" is filled as the BT.709 limited-range equivalent (Y=63, Cb=104, Cr=240); MJPG and 10/16-bit formats are excluded (they need an encoder or aren't raw).
EnumFormatvalues are plain, not choices. The size is a plainRectangle(notCHOICE_RANGE) and the framerate a plainFraction. Apps like OBS parse the size with a plain-rectangle parse and silently drop entries whose size/framerate are choice values — that was the cause of OBS showing empty format/resolution/framerate dropdowns.- Source is the DRIVER; a 1 ms timer produces at most one frame per negotiated period (software pacing, so fps survives renegotiation without re-arming). The consumer is passive.
- Consumer needs
PW_STREAM_FLAG_AUTOCONNECT+INACTIVE+ a follow-uppw_stream_set_active(true), and must reply withParamBuffersinparam_changed— this is what makes WirePlumber link the consumer to the producer (WirePlumber's session manager links the graph offnode.autoconnect). SignalSourcemust be kept alive. The crate's signal/timer sources unregister on drop;Cameraholds the SIGINT/SIGTERM sources for the loop's duration so the process exits (and the node is removed) on signal.
Development
&&
See the "Key decisions" / design sections above for how it works; the
source files are self-documenting, and the E2E harness (e2e.sh) is the
source of truth for what "works."
License
Copyright 2026 Federico Simoncelli. Licensed under the Apache License,
Version 2.0 (see the LICENSE file).