A camera is a room: live camera feeds over WebRTC
People who build apps for security teams, building sites, shops and care homes keep asking us the same thing: can the people in this app see the cameras we already have? Live video from an IP camera or an NVR, in a browser or on a phone, with the same access control as the rest of the app. This is how we built it, and why it looks the way it does.
Moving the video is not the hard part
Getting pixels from a camera to a screen is a solved problem, several times over. Cameras speak RTSP. Browsers do not, so something has to relay the video in a form a browser can play. There are good open-source relays for exactly that.
The hard part is who may watch. A camera feed is some of the most sensitive data an app can carry, and every camera product ends up needing the same answers: which people can see which camera, how that access is granted, how it is taken away, and what happens to the footage when a customer leaves. Build those answers a second time, for video only, and you get two access systems that slowly drift apart.
A camera is a room
Ollacore already had a model for "a group of people allowed to see something": the room. Rooms have members, members have roles, and a room-scoped session token says who is connecting and to which room. So a camera is simply a room:
- The room id is the stream's address.
- The device sending the video is the room's owner, and only an owner may publish.
- The people who may watch are its members; the read-only
viewerrole is enough. - Taking access away is removing a member. Retiring a camera is deleting the room, and the erasure that already covers chat covers the camera too.
POST /v1/server/rooms → {"id": "25964f12-…"} the camera
POST /v1/server/rooms/{id}/members {"principal_id": "gate-cam-1", "role": "owner"}
POST /v1/server/rooms/{id}/members {"principal_id": "guard-anna", "role": "viewer"}
POST /v1/server/session-tokens one per device, when it connects That is the whole integration on your server: the same four calls you make for a chat room. There are no camera tables and no camera endpoints to learn.
One detail made this safe rather than merely convenient. Room ids are UUIDs that the server generates; a customer cannot choose one. So a stream address names exactly one room of exactly one customer, and no naming trick can point one customer's viewer at another customer's camera.
WebRTC only, for now
The video itself travels over WebRTC, using the two standard HTTP handshakes for
it: WHIP to send a stream and WHEP to watch one. Each is a single
request — an SDP offer in, an answer out — carrying the session token in an ordinary
Authorization header over HTTPS.
That choice buys a lot:
- The delay of a video call, not the several seconds of a segmented stream. For a door or a gate, seconds matter.
- Every modern browser plays it with no plugin and no player library, and the mobile WebRTC stacks our customers already use for calls play it too.
- Strict networks work. A viewer whose network blocks UDP relays through the same TURN servers our calls use; their credentials already come with the session token.
- One new port. Signalling rides the existing HTTPS endpoint; media needs a single UDP port.
It also rules some things out, deliberately. SRT, which copes well with lossy mobile links, carries its credential in a stream id of at most 512 bytes; our session tokens are signed JWTs of about 650 characters, so they do not fit. RTMP, still what most encoders and many cameras push, carries its credential in the stream URL, where a 650-character token is fragile across encoders and camera settings pages. HLS scales to huge audiences through a CDN, at the cost of seconds of delay. Each of those needs short-lived stream keys, or a different latency trade, and we would rather add them properly than bolt them on. For now: WebRTC in, WebRTC out.
A media server that asks before it serves
camera ──RTSP──▶ your gateway ──WHIP──▶ ┌──────────────┐ ──WHEP──▶ browser / app
(LAN) (MediaMTX) │ media server │
└──────┬───────┘
│ "may this token publish / read
│ this room?" (every connection)
▼
┌──────────────┐
│ Ollacore API │ token signature, room,
└──────────────┘ live membership, role The video runs through MediaMTX, an MIT-licensed media server, as a small sidecar next to the API. It relays; it does not decide. For every publish and every view it calls back into the Ollacore API, which checks:
- the token's signature, and that it names this room;
- that the person is still a member, read live from the database rather than from the token;
- for a publish, that both the token and the live membership say
owner; - that the room is not end-to-end encrypted.
That last rule deserves a sentence. A relay that handles the video can see it, so letting an end-to-end encrypted room carry a camera would quietly break the room's promise. Those rooms are refused.
The sidecar is configured to be as small as it can be: of everything MediaMTX can serve, only WebRTC is switched on. Its RTSP, RTMP, HLS and SRT servers, its control API, metrics and playback are all off, and it runs read-only, with a memory cap, and with no path it will accept other than a room id.
Getting real cameras in
Most cameras sit on a private network behind a router, speaking RTSP with a password. Our answer is to not ask for that password at all. You run a forwarder on your side — MediaMTX again works well, on the NVR or a small gateway — that pulls each camera on your network and forwards it to us over WHIP. The camera's credentials never leave your building; we only ever see the stream you chose to send.
Because nothing is transcoded, what the camera sends is what viewers receive. That keeps the relay cheap and adds no delay, but it means the camera should send H.264, which every browser decodes. Most cameras can be switched from H.265 in their settings. And a browser can be a camera as well: a webcam or a phone camera publishes with a few lines of WHIP. The camera docs have both recipes.
What it does not do yet
We would rather you read these here than discover them later:
- Access is checked when a connection starts. Removing a viewer stops them reconnecting, but does not cut a stream they are already watching. Short token lifetimes keep that window small.
- No recording. Ollacore relays live video only; your NVR stays the recorder.
- Twenty viewers per camera, and one publisher.
- No camera online/offline events yet.
Trying it
Camera feeds are in preview: built and tested end to end on our test deployment, and not yet
switched on at api.ollacore.com. The camera feeds docs
cover the whole integration. If you have cameras you want in your app, write to
[email protected] with roughly how many cameras and
viewers you expect.