Publish IP cameras into a WebRTC conference room — one participant per camera — so a whole fleet can be watched from an ordinary browser tab, with no NVR, no port forwarding and no VPN.
camera 1 --rtsp--> ┐
camera 2 --rtsp--> ├── wbcam ──wss──> WB Stream / LiveKit ──> browser
camera N --rtsp--> ┘ N participants N tiles
H.264 and H.265 are forwarded without transcoding, so a Raspberry-Pi-class box can carry a dozen cameras. The conference service does the NAT traversal, the fan-out and the UI.
Built for OpenIPC cameras, but nothing in it is OpenIPC-specific — any camera with an RTSP URL works.
Working, and measured rather than assumed. Four OpenIPC cameras across three SoCs (hi3516ev300, hi3516av300, t31) ran live as four conference tiles, decoded from the subscriber side with zero errors. See docs/findings.md for what was tested and what broke.
Treat it as a solid proof of concept: the media path is proven, the operational edges (long soaks, large fleets) are not yet.
go install github.com/OpenIPC/wbcam/cmd/wbcam@latest
# 1. Ask the camera what it actually emits. Do this first, always.
wbcam probe -url rtsp://192.168.1.10:554/stream=1 -user root -pass secret
# 2. Configure.
curl -O https://raw.githubusercontent.com/OpenIPC/wbcam/master/config.example.yaml
mv config.example.yaml cams.yaml && $EDITOR cams.yaml
# 3. Run.
wbcam run -config cams.yaml
# 4. Watch: open the room in a browser, and the local status page.
open http://127.0.0.1:8099/livekit — your own LiveKit server. No third party,
no rate limits, no account. Recommended if you already self-host.
wbstream — the WB Stream service. Cameras join anonymously as guests;
you need an account only to create the room.
A note on being a good neighbour: the wbstream backend registers one guest
account per camera through the same flow the web client uses. That service
rate-limits guest registration and will return 429 if you start a fleet at
once — start_stagger and the token cache exist to keep wbcam inside sensible
bounds. Don't point hundreds of cameras at someone else's free service. If you
need scale, self-host LiveKit.
Your video leaves your network either way. With wbstream it passes through
third-party infrastructure. Decide accordingly.
wbcam probe will tell you if something is wrong, but the short version for
OpenIPC/majestic:
curl -u root:pass -X POST -H 'Content-Type: application/json' \
-d '{"video1":{"enabled":true,"codec":"h264","profile":"base","size":"704x576","fps":15,"bitrate":1024,"gopSize":"1"}}' \
http://CAMERA/api/v1/config| setting | value | why |
|---|---|---|
| stream | sub-stream | a tile in an N-up grid is 200-400 px wide; 1080p into it is 10x waste |
| profile | base | Main is not a profile LiveKit can negotiate honestly |
| gopSize | 1 (second) | sub-second time-to-first-picture and loss recovery |
| B-frames | off | RTSP delivers decode order, RTP carries presentation order |
| rcMode | cbr | we cannot adapt bitrate, so VBR spikes just cause loss |
The enum is base, not baseline — the API rejects the latter.
| command | what it does |
|---|---|
wbcam run |
the gateway |
wbcam probe |
what a camera emits, judged against what the SFU can carry |
wbcam watch |
subscribe and capture what the SFU actually delivers, to a file |
wbcam listen |
dump room data packets, metadata and participant events |
wbcam room-create |
create a WB Stream room (needs an account token) |
wbcam console-preview |
render the control tile to a PNG |
probe and watch are the two that earn their keep when something looks
wrong: one shows what you sent, the other what arrived.
Set control.enabled: true and wbcam joins as an extra participant that
publishes a console tile and takes commands as room reactions:
| reaction | action |
|---|---|
| 👏 | refresh status |
| ❤️ | start all cameras |
| 😢 | stop all cameras |
| 😂 | move the cursor |
| 👎 | toggle the selected camera |
Reactions rather than chat because chat never reaches the SFU — see docs/findings.md. Five buttons plus a screen is a TV remote, which is enough to pick a camera and toggle it.
Authorisation is by participant identity, resolved automatically from the room's owner. It fails closed: with nothing authorised, nothing is obeyed. Guests cannot publish data at all, so a stranger holding your room link cannot reach the control channel even before the allowlist is consulted.
H.264 profile. LiveKit negotiates only 42e01f and 640032. A Main-profile
camera still negotiates — pion offers 42e01f unconditionally — and is then
forwarded under an SDP that misdescribes it. Some browsers decode it, some
paint black. wbcam probe grades this; fix it on the camera.
No bitrate adaptation. Passthrough means no encoder, so nothing can respond to congestion. Overshoot is packet loss, and every tile breaks at once. Size the uplink for the worst case and pin CBR.
Rooms die with their owner. On WB Stream a non-permanent room is reaped when the host leaves; camera guests cannot hold it open.
Guest registration is rate limited. Cold-starting four cameras at 400 ms
apart earned a 429. The token cache in state_dir means a restart re-uses
identities and avoids it entirely — don't delete that directory.
go build ./cmd/wbcam
go test -race ./...The RTSP suite stands up a real in-process RTSP server serving fragmented H.264, so ingest, reassembly, PTS handling and parameter-set seeding are all covered without hardware.
Apache-2.0. See LICENSE and NOTICE.
The WB Stream authentication flow was derived from olcrtc, along with several hard-won details of publishing video through pion and the LiveKit SDK.