---
title: "Virtual camera"
description: "Present any video in a stream as a camera that Zoom, Google Meet, OBS or a browser can select. Linux only."
type: "docs-page"
url: "https://tatolab.com/docs/build/virtual-camera"
site: "Tatolab"
section: "Build"
---

# 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.

> **Note: Linux only**
>
> `VirtualCameraSink` works on Linux. On macOS, a stream that adds one is refused when it loads, with a message saying so.

## A camera with an effect

```python title="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](https://tatolab.com/docs/build/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](https://github.com/tatolab/streamlib/tree/main/examples/camera-virtual-camera) example presents the plain camera and an inverted copy side by side.

## Choose a route: v4l2loopback or PipeWire

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.

## Set up v4l2loopback (once)

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

```sh
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.

## Names

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.

## Ask an agent to do it

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](https://tatolab.com/docs/share/agents).

## Troubleshooting

- **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.
