vcam feeds a video into a v4l2loopback
device so that ordinary applications — Chrome, Firefox, Zoom, Teams, Discord,
OBS, Cheese, OpenCV — see it as a normal webcam. It can also expose the source's
audio track as a virtual microphone, so a call sees a matching camera and
microphone pair.
It is written in Rust and GStreamer. A small always-on daemon (vcamd) holds
the loopback device open and writes frames to it. A command line client (vcam)
and a GTK4 control panel (vcam-gui) talk to the daemon over a Unix socket, so
they never own the capture pipeline and can come and go freely.
- Why vcam?
- Features
- Requirements
- Install
- Quick start
- Command line
- GUI
- Virtual microphone
- Configuration
- How it works
- Application compatibility
- Testing
- Roadmap
- FAQ
- Contributing
- License
Virtual cameras are usually an afterthought of a larger application (OBS) or a
raw kernel interface with no producer. vcam is a dedicated, headless,
compositor-independent producer:
| vcam | OBS Virtual Camera | v4l2loopback alone | |
|---|---|---|---|
| Loop a local file without a GUI app running | ✅ | ❌ (OBS must run) | |
| Works on a headless or minimal session | ✅ | ❌ | ✅ |
| Stable caps and a black placeholder while idle | ✅ | ❌ | |
| Hot-swap source, pause, resume, seek | ✅ | ❌ | |
| Safe output changes while apps are streaming | ✅ (deferred) | ❌ | ❌ |
| Virtual microphone that follows playback | ✅ | ❌ | ❌ |
| Per-application output profiles | ✅ | ❌ | ❌ |
| Watchdog with automatic recovery | ✅ | ❌ | ❌ |
| Unprivileged user daemon | ✅ | ✅ | ✅ |
- Stable output — the device always advertises the configured resolution and
frame rate (default
1280x720 @ 30,NV12, withYUYVavailable for Chromium/Electron apps). - Idle placeholder — the daemon keeps the device open with a black frame, so applications that enumerate cameras at startup never race the producer.
- Looping, hot swapping the source, pause / resume / seek, and error recovery back to the placeholder.
- Output shaping — letterbox, fill or stretch; mirror; rotation 0/90/180/270.
- Diagnostics —
vcam doctorchecks the module, device, permissions and the required GStreamer elements. - Self-healing — a watchdog rebuilds the output pipeline if it stalls or leaks memory, and restarts the daemon as a last resort so systemd brings it back cleanly.
- Camera users —
vcam consumerslists the applications holding the device. Changing the output format while an application is streaming is deferred until the camera is free, which is what makes it safe to do at all. - Chromium/Electron compatibility — Discord, Chrome, Brave, Slack and friends
routinely fail to list a loopback device that only advertises
NV12. The daemon watches for those applications and advertisesYUYVwhile any of them is running, then restores the configured pixel format.vcam compat offdisables the shim. - Virtual microphone — the source's audio track is played into a
PulseAudio/PipeWire null sink and exposed as a remapped source called
Virtual Microphone. It follows playback exactly: pause, seek, looping and
stop all apply to sound and video together, and the short pipeline run used
for paused seeks is muted. Toggle it with
vcam audio on|offor from the GUI's Audio tab. - Access diagnosis — when the device cannot be opened,
vcamdandvcam doctorsay why: novideogroup membership, a session that started beforeusermod -aG video(invisible toid, and it stops every application in that session from seeing the camera), or a wrong device mode. The udev rule carriesuaccessso the active session user gets access without a re-login. - Per-application profiles —
[[profiles]]entries pick an output format automatically based on who is using the camera. - No root daemon — the module is loaded at boot; the daemon runs as your
user in the
videogroup.
- Ubuntu 24.04 / Debian 13 (or newer), 64-bit.
v4l2loopbackkernel module.- GStreamer 1.24 with base, good, bad and libav plugins.
- A PulseAudio or PipeWire server with
pactlfor the virtual microphone (pulseaudio-utils, orpipewire-pulsepluspulseaudio-utils). The camera works without one; the microphone is then reported as unavailable. - For the GUI: GTK 4.14 and libadwaita 1.5.
- For the
videogroup access rule: your user in thevideogroup.
Download the .deb from the latest release:
sudo apt install ./vcam_0.3.0_amd64.deb
sudo modprobe v4l2loopback # or reboot, modules-load.d loads it
sudo usermod -aG video "$USER" # then log out and back inThe package installs /usr/lib/modprobe.d/v4l2loopback.conf,
/usr/lib/modules-load.d/v4l2loopback.conf and the udev rule that grants the
video group access to /dev/video10. The rule also carries
TAG+="uaccess", so the user of the active desktop session gets access through
systemd-logind without a re-login.
Logging out matters. A group change only reaches sessions that start afterwards. Applications in the session that was already open when you ran
usermodkeep the old credentials, cannot open the device, and quietly dropVirtual Camerafrom their camera list.vcam doctordetects exactly this case and prints the fix.
sudo apt install build-essential pkg-config \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libgstreamer-plugins-bad1.0-dev libgtk-4-dev libadwaita-1-dev
git clone https://github.com/ArchDityaa/vcam.git
cd vcam
cargo build --release
sudo install -m0755 target/release/vcamd target/release/vcam target/release/vcam-gui /usr/local/bin/Or build the package yourself:
packaging/build-deb.sh
sudo apt install ./target/deb/vcam_0.3.0_amd64.debsudo modprobe v4l2loopback \
video_nr=10 card_label="Virtual Camera" exclusive_caps=1 max_buffers=4exclusive_caps=1 is important: the device advertises an output format until
a writer starts streaming, then switches to capture. This is what stops
Chrome and Zoom from caching an empty capability set.
To load it at boot, copy the two files from packaging/ into
/etc/modprobe.d/ and /etc/modules-load.d/.
vcam doctor # verify the setup
vcamd & # or: systemctl --user start vcamd
vcam start --source movie.mp4 # start feeding the cameraThen pick Virtual Camera in any application's camera settings and
Virtual Microphone in its microphone settings. The microphone exists while
vcamd runs and stays silent while nothing is playing.
vcam start [--source FILE] [--no-loop] start playing (uses config source)
vcam stop stop and show the black placeholder
vcam pause | resume pause / resume playback
vcam seek 01:30 seek to a position (SS, MM:SS, HH:MM:SS)
vcam source FILE hot swap the source while running
vcam loop on|off toggle looping
vcam output [--width N] [--height N] [--fps N]
[--pixel-format NV12|YUYV]
[--fit letterbox|fill|stretch]
[--mirror true|false] [--rotate 0|90|180|270]
[--force] apply even while apps use the camera
vcam consumers list applications using the camera
vcam profile list list configured profiles
vcam profile use NAME [--force] apply a profile now (pins it)
vcam profile auto on|off toggle automatic profile selection
vcam compat on|off toggle the Chromium/Electron YUYV shim
vcam audio on|off|status toggle or inspect the virtual microphone
vcam status print daemon status
vcam doctor run diagnostics
vcam reload reload the configuration file
vcam shutdown stop the daemon
All commands accept --socket PATH to talk to a daemon on a different socket.
Run vcam-gui (or launch Virtual Camera from your application menu). It
shows a live preview and lets you choose a file, play/pause/stop, seek, toggle
looping, change the output format, see who is using the camera, and run the
doctor. The Audio tab turns the virtual microphone on or off and shows the
node names and the sound server it is using. The GUI is an IPC client: you can
use it alongside the CLI and both stay in sync.
The source's audio track can be exposed as a normal microphone. It is enabled by
default: vcamd creates a PulseAudio/PipeWire null sink and a remapped source
called Virtual Microphone (vcam_mic), then plays the audio through it in
step with the video. Pick Virtual Microphone in the microphone settings of
the application that should receive it.
vcam audio status # backend, sink and source names
vcam audio off # remove the sink and source
vcam audio on # create them againAudio follows playback: pause, seek, looping and stop apply to sound and video together, and the short pipeline run used for paused-seek previews is muted. While nothing is playing the microphone stays connected and silent, so applications can select it before playback starts and keep it selected across sources. Sources without an audio track are silent too.
The microphone is best-effort. If pactl or a sound server is unavailable,
playback continues, vcam status keeps working, and the Audio tab explains
the error. The [audio] table in Configuration changes the
node names and whether the companion starts automatically; see
Troubleshooting
when the source does not show up.
The daemon reads ~/.config/vcam/config.toml. Write a commented default with:
vcamd --write-default-configKey settings:
[device]
video_nr = 10
label = "Virtual Camera"
[output]
width = 1280
height = 720
fps = 30
pixel_format = "NV12" # or "YUYV"
fit = "letterbox" # letterbox | fill | stretch
mirror = false
rotate = 0
[source]
path = "/home/you/movie.mp4"
loop_video = true
autostart = false
# Virtual microphone: the source's audio track is played into a null sink and
# exposed as a remapped source. Applications see `label`. On by default; the
# camera keeps working when no sound server is reachable.
[audio]
enabled = true
sink_name = "vcam_sink"
source_name = "vcam_mic"
label = "Virtual Microphone"
sample_rate = 48000
channels = 2
[daemon]
log_level = "info"
auto_profiles = false # apply matching [[profiles]] automatically
[compatibility]
# Advertise YUYV while a Chromium/Electron application (Discord, Chrome,
# Brave, Slack, ...) is running: those applications frequently fail to list a
# loopback device that only offers NV12. The configured pixel format is
# restored when they exit, and `vcam compat off` disables the shim.
chromium_yuyv = true
[watchdog]
enabled = true
max_rss_mb = 1024 # rebuild the pipeline above this memory use
stall_timeout_ms = 2000 # rebuild after this long without progress
max_push_failures = 30 # rebuild after this many failed frame pushes
max_recoveries = 5 # then exit so systemd restarts the daemon
# Optional per-application output profiles. The first entry whose `match`
# pattern matches a process holding the camera wins. Only the keys you set are
# overridden; everything else falls back to [output]. With the compatibility
# shim above on, Discord and friends already get YUYV without a profile; use a
# profile when you want more than the pixel format to change.
[[profiles]]
name = "discord"
match = ["discord", "chrome", "chromium"]
pixel_format = "YUYV"
[[profiles]]
name = "obs"
match = ["obs"]
pixel_format = "NV12"Note — keys such as
auto_profileslive inside a table, which is why they are under[daemon]. Only keys written before the first[table]header are top-level.
A relative source.path in a config file is resolved against the directory
that contains the config file. Paths passed on the command line
(vcam start --source movie.mp4) are resolved against the shell's working
directory before they are sent to the daemon.
v4l2loopback lets a single producer write the device while several applications
read it, but renegotiating its caps (resolution, frame rate, pixel format) while
an application is streaming can wedge the device. vcam therefore defers
such changes until every application has released the camera; vcam status and
the GUI show the pending change. Changes that do not touch the device caps
(scaling, mirroring, rotation) are applied immediately. Use --force to
override, accepting the risk.
Deferred changes are also how profiles behave: when a matching
application starts, the profile is selected, and the format switches as soon as
the camera is free. Turning a profile on manually with vcam profile use pins
it and turns automatic selection off until you run vcam profile auto on.
flowchart LR
file[Video file] --> dec[decodebin3]
dec --> conv[videoconvert<br/>videoscale<br/>videorate]
conv --> src[appsrc]
src --> queue[queue]
queue --> sink[v4l2sink]
sink --> dev[/dev/video10/]
dev --> apps[Chrome · Firefox · Zoom<br/>Teams · Discord · OBS]
dec --> aconv[audioconvert<br/>audioresample]
aconv --> asink[pulsesink]
asink --> nullsink[(vcam_sink)]
nullsink --> mic[vcam_mic<br/>remapped source]
mic --> callapps[Zoom · Teams · Discord]
cli[vcam CLI] -. Unix socket .-> ctl[control server]
gui[vcam-gui] -. Unix socket .-> ctl
ctl --> src
subgraph vcamd [vcamd]
dec
conv
src
queue
sink
aconv
asink
ctl
end
The daemon starts the output pipeline as soon as it owns the device and keeps it running with a black placeholder. Playback attaches a decoder pipeline that produces frames in the exact caps the output expects. When audio is enabled the same decoder grows a second branch that plays into a PulseAudio/PipeWire null sink; the microphone applications see is a remapped source on that sink's monitor. The GUI never opens the device; the daemon publishes a small preview frame that the GUI reads instead.
See docs/architecture.md for the threading model, the IPC protocol, the preview format, the consumer scan, profiles and the watchdog.
| Application | Notes |
|---|---|
| Chrome / Chromium | Works; the compatibility shim advertises YUYV for it, and the camera list may need a page reload after the daemon starts. |
| Firefox | Works. |
| Zoom | Works. |
| Microsoft Teams | Works. |
| Discord | Works. The daemon advertises YUYV automatically while Discord runs; the GUI no longer opens the device. |
| OBS | Add as a Video Capture Device; select Virtual Camera. |
| Cheese | Works. |
| OpenCV | cv2.VideoCapture("/dev/video10") or by index. |
See docs/troubleshooting.md if a camera does not appear.
cargo test --workspace # unit tests
tests/integration.sh # end-to-end against /dev/video10
tests/audio.sh # virtual microphone against the sound server
DURATION=1800 tests/stability.sh # 30-minute 1080p30 soak testThe integration, audio and stability scripts start a private daemon on a
temporary socket (the audio test also uses private module names), so they do not
disturb a vcamd you are already running — but they do need the device itself,
so stop that daemon first. They require the module to be loaded and the device
node to exist; the audio test additionally needs pactl, parec, python3 and
a reachable PulseAudio/PipeWire server, and skips itself when any of those are
missing.
-
vcamddaemon, CLI and GTK4 GUI - Idle placeholder, looping, hot swap, seek
- Daemon-fed preview
- Consumer detection and deferred output changes
- Per-application profiles
- Watchdog and self-healing
- Virtual microphone companion (PulseAudio/PipeWire null sink)
-
.deb, systemd, udev, man pages - Flatpak package
- Playlist / multi-source queue
- PipeWire / portal source
- Integration tests on a self-hosted runner with
v4l2loopback
Does it work without a desktop session?
Yes. vcamd only needs a user systemd session (or a plain terminal) and the
video group.
Why is the virtual microphone silent?
It only carries the source's own audio track. While nothing is playing, and for
sources without audio, it stays connected but silent — that is deliberate, so
applications can select it before playback starts and keep it selected across
sources. Check vcam audio status; it reports the node names and any error.
Do I need PipeWire for the microphone?
No. The companion uses pactl with a null sink plus a remapped source, which
works the same on PipeWire's PulseAudio compatibility server and on a plain
PulseAudio server. If no sound server is reachable, the camera keeps working and
the microphone reports that it is unavailable.
Why does the preview keep working while Discord uses the camera? The preview is rendered by the daemon and published as a small file. A second process cannot open the loopback device while another reader is streaming it.
Why is my output change not applied?
An application is still holding the camera. Run vcam consumers; the change is
applied automatically once they release it. --force applies it immediately.
Can I use it with OBS? Yes, as a Video Capture Device. Close OBS's own virtual camera if it also creates a loopback device, to avoid two producers.
Is it safe to run as root? It is not needed. The daemon is designed to run as your user; the systemd unit hardens it further.
Contributions are welcome. Read CONTRIBUTING.md and the Code of Conduct. For security issues, follow SECURITY.md.
MIT. See LICENSE.


