Skip to content

Virtual camera

Present any video in a stream as a camera that Zoom, Google Meet, OBS or a browser can select. Linux only.

VirtualCameraSink turns a video output into a camera that every other application on the machine can pick, like a webcam you plugged in. Put an effect, an overlay or a model’s annotations on your video, and use the result in a video call or a streaming tool.

stream.py
from tatolab.stream import CameraSource, StreamBuilder, VirtualCameraSink, stream
from nodes.vignette import Vignette
@stream
def call_camera(stream_builder: StreamBuilder) -> None:
"""The camera with a vignette, as a camera other apps can use."""
camera = stream_builder.add(CameraSource)
effect = stream_builder.add(Vignette, config={"amount": 0.7})
virtual_camera = stream_builder.add(VirtualCameraSink, config={"name": "Tatolab Camera"})
stream_builder.connect(camera.output("video"), effect.input("video"))
stream_builder.connect(effect.output("vignetted_video"), virtual_camera.input("video"))

Vignette is the node from GPU effects. Run the stream, then open your video app’s camera settings and choose Tatolab Camera. The camera exists while its node runs: it appears when the stream starts and disappears when it stops, like a USB camera being plugged in and pulled out.

Add as many VirtualCameraSink nodes as you like; each one is a separate camera. The camera-virtual-camera example presents the plain camera and an inverted copy side by side.

Linux has two routes for presenting a camera, and the door setting picks one:

door How it works Who can see it
"auto" (default) Uses v4l2loopback if it’s set up, and PipeWire otherwise Depends on which it picked
"v4l2loopback" Creates a standard video device, /dev/videoN Every application that uses cameras
"pipewire" Registers a camera with PipeWire. Needs no kernel module and no root. Applications that use PipeWire’s camera support

The v4l2loopback route works with everything, including Zoom, browsers and OBS, so it’s the one to set up if you can. The log says which route each camera took.

v4l2loopback is a Linux kernel module. Install it, then give Tatolab permission to create cameras with it:

Terminal window
sudo apt install v4l2loopback-dkms # Debian and Ubuntu
tatolab enable-virtual-camera

tatolab enable-virtual-camera makes one privileged change, behind your desktop’s password prompt (or sudo in a terminal without a desktop): it loads the module at boot, with no devices of its own, and lets the logged-in user create cameras through it. You only do this once per machine. To see exactly what it would change without changing anything, run tatolab enable-virtual-camera --print.

Until it’s set up, door="auto" takes the PipeWire route, and the log names tatolab enable-virtual-camera as the way to switch to v4l2loopback.

The name setting is what applications show in their camera list. Leave it out and the camera is called StreamLib Camera plus a short ID that’s unique to the node and stream, and stays the same across runs. Names longer than 31 bytes are cut short, so keep them brief.

The runtime gives agents a recipe for this, show_channel_on_virtual_camera: it adds a VirtualCameraSink to a running stream and connects any video output to it. Ask your agent “put the detector’s output on a virtual camera” and it can do it without restarting the stream. See Agents and MCP.

  • The camera doesn’t appear in my app. Check the stream’s log for which route the camera took. If it’s PipeWire and your app only lists /dev/video* devices, set up v4l2loopback as above. Some apps only check for cameras when they start, so restart the app.
  • door="v4l2loopback" is refused. The permission isn’t set up yet. Run tatolab enable-virtual-camera, and install v4l2loopback-dkms first if it asks for the module.
  • The picture is mirrored in my video call. That’s usually the app mirroring your self-view; other people see it the right way round.