Advanced Usage
- Broadcasting a Live Stream to Many Viewers via SFU
- Running as a Linux Service
- Two-way Audio Communication
- DataChannels
- Two-way DataChannel Messaging
- Gamepad Input
- Stream AI or Any Custom Feed to a Virtual Camera
- WHEP with webrtc-player
- Using the WebRTC Camera in Home Assistant
- Jetson: Unthrottling the VIC and NVENC Clocks
- Useful Commands
Broadcasting a Live Stream to Many Viewers via SFU
An SFU lets one device stream to many viewers without sending a separate stream to each viewer. The SFU receives the stream from the device and forwards it to the viewers.
Two SFU backends are supported:
- LiveKit: self-hosted or hosted.
- Cloudflare Realtime: managed by Cloudflare.
Both have a free endpoint below for testing.
LiveKit
Free Testing Server
| URL | API Key |
|---|---|
wss://api.mazupo.com | APIWnQTs4tmUZvA |
⚠️ Shared testing server: Limited to 100 concurrent connections, 5,000 minutes, and 50 GB of transfer per month across all users. For a dedicated environment, see SPONSORS.md.
1. Run on the device
/path/to/pi-webrtc --camera=libcamera:0 \
--fps=60 \
--width=1920 \
--height=1080 \
--uid=your-display-name \
--use-livekit \
--livekit-url=wss://api.mazupo.com \
--livekit-key=APIWnQTs4tmUZvA \
--livekit-room=the-room-nameThe device connects to LiveKit using --livekit-url, --livekit-key, --livekit-room, and --uid.
--livekit-urlspecifies the SFU address. Usewss://for TLS orws://for non-TLS connections.--livekit-keyis the LiveKit API key.--livekit-roomspecifies the room to publish to.--uidis the device's identity in the room.--livekit-secret* lets the device generate a LiveKit access token locally, so no separate token server is required.
Anyone who joins the same room can watch the stream.
With --enable-ipc, DataChannel messages are also broadcast to all participants in the room.
2. Join the room
- See the example in client-sdk-js
- Try it on the Web demo: https://app.mazupo.com/room
Cloudflare Realtime
Free Testing Server
| URL | Device API Key | Viewer API Key |
|---|---|---|
https://api.mazupo.com | 81f899b8fab5692b0faa76c3372b7ae6 | ec0478c67e729b6f429eda1e97829af0 |
⚠️ Shared demo environment: The free Cloudflare quota is shared by all users and stops when the monthly limit is reached. Use a unique --uid to avoid conflicts with other users. For a dedicated environment, see SPONSORS.md.
1. Run on the device
/path/to/pi-webrtc --camera=libcamera:0 \
--fps=60 \
--width=1920 \
--height=1080 \
--uid=your-display-name \
--use-cloudflare \
--api-url=https://api.mazupo.com \
--api-key=81f899b8fab5692b0faa76c3372b7ae6The device connects to Cloudflare Realtime through the Mazupo API. Cloudflare generates a new sessionId each time the device reconnects, and the backend maps it to the device's uid.
--api-urlspecifies the backend address.--api-keyauthenticates the device with the backend.--uididentifies the device.
Anyone who knows the device's uid can find its stream.
DataChannel/IPC traffic is not supported yet. But --enable-ipc still applies to the other signaling services running alongside it.
2. Watch streams
- See the example in client-sdk-js
- Try it on the Web demo: https://app.mazupo.com/cloudflare. Enter the URL and the Viewer API Key under Settings → Network, add a device with the same
uid, then select your device. The viewer only needs the device'suid.
Running as a Linux Service
1. Set up pulseaudio as a system-wide daemon
Skip this step if you run with the --no-audio flag.
[reference]
- Install it:
sudo apt install pulseaudio - Create
/etc/systemd/system/pulseaudio.service:[Unit] Description= Pulseaudio Daemon After=rtkit-daemon.service systemd-udevd.service dbus.service [Service] Type=simple ExecStart=/usr/bin/pulseaudio --system --disallow-exit --disallow-module-loading Restart=always RestartSec=10 [Install] WantedBy=multi-user.target - Stop the client from autospawning its own copy:
echo 'autospawn = no' | sudo tee -a /etc/pulse/client.conf > /dev/null - Grant access, enable, and reboot:
sudo adduser root pulse-access sudo systemctl enable pulseaudio.service sudo reboot
2. Run pi-webrtc on boot
-
Create
/etc/systemd/system/pi-webrtc.service, adjustingWorkingDirectoryandExecStart:[Unit] Description= The p2p camera via webrtc. After=network-online.target pulseaudio.service [Service] Type=simple WorkingDirectory=/path/to ExecStart=/path/to/pi-webrtc --camera=libcamera:0 --fps=60 --width=1280 --height=720 --uid=your-uid --hw-accel --use-mqtt --mqtt-host=example.s1.eu.hivemq.cloud --mqtt-port=8883 --mqtt-username=hakunamatata --mqtt-password=wonderful Restart=always RestartSec=10 [Install] WantedBy=multi-user.targetTip
A long
ExecStartis a good reason to use a config file instead:ExecStart=/path/to/pi-webrtc --config=/path/to/config.yml -
Enable and start it:
sudo systemctl daemon-reload sudo systemctl enable pi-webrtc.service sudo systemctl start pi-webrtc.service
Two-way Audio Communication
Remove the --no-audio flag to enable two-way audio. If PulseAudio is available, run pulseaudio in the background. On systems without PulseAudio, use --force-alsa.
Two-way audio works for peer-to-peer connections. Audio sent through an SFU is one-way, from the device to the viewers. Remote audio plays on the default output device. With --force-alsa, make sure the ALSA default device points to your speaker.
The device needs a microphone and speaker. USB audio devices are the easiest option. For GPIO/I2S devices, see:
- Microphone — wiring and testing an I2S MEMS mic
- Speaker — wiring a MAX98357 I2S amp
DataChannels
pi-webrtc uses DataChannels for commands, data transfer, and optional IPC:
| Label | Ordered | Reliability | Carries |
|---|---|---|---|
command | yes | reliable | Commands and small responses, such as recording status. |
stream | no | reliable, unordered | Large data such as snapshots and file transfers. |
_lossy | no | unreliable | IPC messages where old data can be dropped. |
_reliable | yes | reliable | IPC messages that must be delivered. |
Which channels a peer gets depends on the signaling it arrived through:
| Label | MQTT | WHEP | LiveKit | Cloudflare |
|---|---|---|---|---|
command | ✅ | ❌ | ❌ | ❌ |
stream | ✅ | ❌ | ❌ | ❌ |
_lossy | ✅ | ❌ | ✅ | ❌ |
_reliable | ✅ | ❌ | ✅ | ❌ |
_lossy and _reliable also need --enable-ipc. WHEP and Cloudflare peers carry no DataChannel traffic at all, and SFU peers never get the built-in command and stream channels, so snapshots, recording, camera control, and file transfers are only available over MQTT. More than one signaling can be enabled at a time, so running --use-mqtt alongside an SFU keeps those commands available while the SFU carries the viewers.
Large transfers use the stream channel so they do not block commands. Multiple transfers can run at the same time.
Two-way DataChannel Messaging
DataChannels allow the browser and device to exchange AI events, sensor data, and remote control commands. Supported with --use-mqtt and --use-livekit.
With --enable-ipc, pi-webrtc provides two additional channels:
| Channel | Purpose |
|---|---|
| Lossy | Messages where old data can be dropped, such as real-time sensor data |
| Reliable | Messages that must be delivered, such as commands |
The client can choose the channel for each message. client-sdk-js defaults to the reliable channel.
Note
With --use-livekit, messages are broadcast to all participants in the room.
Usage:
-
Start
pi-webrtcwith--enable-ipc:/path/to/pi-webrtc --camera=libcamera:0 --fps=60 ... --enable-ipc -
Run the unix_socket_client.py example on the device:
python ./examples/unix_socket_client.pyIt logs everything sent and received through
pi-webrtc. -
On the client side, use
onMessage(),sendText(), orsendData()to receive and send messages.
- See examples in client-sdk-js
- Try it on Web demo: http://app.mazupo.com/interaction
Gamepad Input
--enable-gamepad lets a browser gamepad control a process on the device through an IPC endpoint named gamepad. It requires --enable-ipc.
The gamepad input is forwarded over a lossy DataChannel, so the device always receives the latest input available.
Button mapping
The browser uses the W3C standard gamepad mapping when available. Buttons pack into one big-endian uint32, buttons[N].pressed as bit N.
| Bit | Button | Bit | Button | Bit | Button |
|---|---|---|---|---|---|
| 0 | A | 6 | LT | 12 | D-pad up |
| 1 | B | 7 | RT | 13 | D-pad down |
| 2 | X | 8 | Back/View | 14 | D-pad left |
| 3 | Y | 9 | Start/Menu | 15 | D-pad right |
| 4 | LB | 10 | L3 | 16 | Guide |
| 5 | RB | 11 | R3 |
Reading Gamepad Input
The gamepad_socket.py example reads gamepad input from the device's gamepad IPC endpoint. The gamepad socket path can be changed with --gamepad-socket-path.
-
Install the protobuf tools:
pip install protobuf sudo apt install protobuf-compiler -
Generate the Python bindings:
protoc -I external/protocol/protos --python_out=examples input.proto common.proto -
Start
pi-webrtcwith IPC and gamepad support:/path/to/pi-webrtc --camera=libcamera:0 --fps=60 ... --enable-ipc --enable-gamepad -
Run the example and connect a gamepad in the browser:
python ./examples/gamepad_socket.py
- See the example. The sender polls the browser's Gamepad API.
- Try it on Web demo: http://app.mazupo.com/gamepad
Stream AI or Any Custom Feed to a Virtual Camera
To enhance images, run AI recognition, or preprocess frames before streaming, process the camera frames and write the result to a V4L2 loopback device. pi-webrtc can then open it as a normal V4L2 camera.
Tip
On Jetson, the sponsor build runs detection and tracking in-process on the GPU instead, with no loopback device and no copy through the CPU.
-
Install the packages:
sudo apt install v4l2loopback-dkms libopencv-dev python3-opencv python3-picamera2 ffmpeg -
Create a virtual device at
/dev/video8:sudo modprobe v4l2loopback devices=1 video_nr=8 card_label=ProcessedCam max_buffers=4 exclusive_caps=1 -
Create a Python virtual env:
python -m venv --system-site-packages ~/venv -
Activate it and install the packages:
source ~/venv/bin/activate pip install --upgrade pip pip install wheel pip install rpi-libcamera picamera2 opencv-python -
Run the virtual camera, using Libcamera to output YUV420 (I420) to the virtual device. See the virtual_cam.py example:
python virtual_cam.py --width 1280 --height 720 --camera-id 0 --virtual-device /dev/video8 -
Run
pi-webrtcagainst the virtual device with the matching format:/path/to/pi-webrtc --camera=v4l2:8 --fps=30 --width=1280 --height=720 --v4l2-format=i420 ...
Tip
Need the same camera source in several pi-webrtc instances?
Create multiple virtual cameras from one processed source and stream each independently.
See yolo_cam.py for writing to multiple loopback devices.
WHEP with webrtc-player
Eyevinn/webrtc-player plays a WHEP URL in the browser.
-
Run the program:
/path/to/pi-webrtc --camera=libcamera:0 \ --uid=home-pi-5 \ --fps=60 \ --width=1920 \ --height=1080 \ --use-whep \ --whep-port=8080 \ --no-audio -
Open the demo player, keep the adapter on WHEP, and play
http://<device-ip>:8080, e.g.http://192.168.4.35:8080.
The player page is served over https while the device answers over http. Chrome allows this for private addresses such as 192.168.x.x once you Allow its prompt to access devices on your local network; other browsers may block the request as mixed content.
Using the WebRTC Camera in Home Assistant
1. Prepare the environment
Follow the official Home Assistant installation guide.
2. Install HACS
HACS lets you install community integrations like WebRTC Camera. Follow the official HACS installation guide.
3. Install WebRTC Camera via HACS
Go to Home Assistant → HACS → Integrations → search for WebRTC Camera, then restart
Home Assistant.
4. Add the integration
Go to Settings → Devices & Services → Add Integration.
5. Run pi-webrtc with WHEP signaling
/path/to/pi-webrtc --camera=libcamera:0 \
--uid=home-pi-4b \
--fps=30 \
--width=1280 \
--height=720 \
--use-whep \
--whep-port=8080The stream is exposed on port 8080, e.g. http://192.168.4.35:8080.
6. Add the card to a dashboard
Go to Dashboard → Edit Dashboard → Add Card → WebRTC Camera.
Enter the URL in the configuration and save:
type: custom:webrtc-camera
url: webrtc:http://192.168.4.35:8080Jetson: Unthrottling the VIC and NVENC Clocks
On Jetson, the VIC and NVENC engines can run at low clock speeds under the default tegra_wmark governor. This can add significant latency to camera copies and hardware encoding.
jetson_clocks and nvpmodel MAXN do not increase the operating frequency of these multimedia engines.
For example, on an Orin NX at 1080p60, setting both governors to performance reduced device-side latency from about 36.5 ms to 24 ms in our test.
- Check the current state:
for d in /sys/class/devfreq/*vic* /sys/class/devfreq/*nvenc*; do
echo "$d: $(cat $d/governor) $(cat $d/cur_freq) / $(cat $d/max_freq)"
done- Apply it for the current boot:
echo performance | sudo tee /sys/class/devfreq/15340000.vic/governor
echo performance | sudo tee /sys/class/devfreq/154c0000.nvenc/governorMaking it persistent
The governor resets on every boot. Create /etc/systemd/system/tegra-mm-perf.service:
[Unit]
Description=Pin Tegra VIC/NVENC devfreq governors to performance
After=nvargus-daemon.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'for d in /sys/class/devfreq/*vic* /sys/class/devfreq/*nvenc*; do echo performance > "$d/governor"; done'
[Install]
WantedBy=multi-user.targetThen enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now tegra-mm-perf.serviceThe wildcard keeps the service working across Jetson modules with different device addresses.
Note
performance holds each engine at maximum while they are powered, which increases power consumption and heat. If that is too aggressive, you can instead raise the minimum frequency (echo 614400000 | sudo tee /sys/class/devfreq/15340000.vic/min_freq), or try the nvhost_podgov governor, which reacts to bursty per-frame work better than tegra_wmark.
Useful Commands
| Command | Description |
|---|---|
v4l2-ctl --list-devices | Show available V4L2 devices. |
v4l2-ctl -d /dev/video0 --list-formats-ext | Show supported formats — for cameras and codecs alike. |
sudo fdisk -l | List partition tables, to help set up a USB disk. |
vcgencmd get_camera | Check whether the camera is detected. |
sudo tegrastats --interval 500 | Jetson: show per-engine utilisation and clocks, e.g. VIC 36%@115. |
To install the latest Mosquitto packages, follow the official Readme.txt for the Eclipse Mosquitto Debian repository.
Recording
Recording is controlled by two independent options. --record-type decides what is captured and --record-mode decides when:
Architecture
pi-webrtc is one process that pulls frames from a camera once and fans them out to three consumers: the WebRTC encoder, the recorder, and — in the sponsor bu...

