Voice
Voice rooms run on the LiveKit sidecar, and audio flows directly between each browser and LiveKit. Voice needs three things:
- A LiveKit server. The compose file runs one, and Stoop mints the
key pair the two share on first boot; there is nothing to set. With
STOOP_LIVEKIT_URLempty, voice is off and joining fails with “voice is not configured”. To use a LiveKit you run elsewhere, or LiveKit Cloud, setSTOOP_LIVEKIT_URLand that server’s pair inSTOOP_LIVEKIT_API_KEY/STOOP_LIVEKIT_API_SECRET. - Media ports reachable, or a TURN relay.
Browsers must reach
7881/tcpand50000-50100/udpon the machine. To move them, setSTOOP_LIVEKIT_TCP_PORTandSTOOP_LIVEKIT_UDP_PORTSin.env; nothing else needs editing. LiveKit discovers the public address to advertise (use_external_ip: true); on a LAN-only install, setNODE_IPin.envto the machine’s LAN address instead. - HTTPS, see Reaching your server.
TURN, when media ports can’t be reached
Section titled “TURN, when media ports can’t be reached”When browsers can’t reach the media ports, WebRTC falls back to a TURN relay if one is offered. The relay has to be reachable itself, so it can’t sit behind the same tunnel. Set either or both under Server admin → Hosting → Voice relay; browsers try every server they are given.
- Cloudflare’s TURN relay. In the Cloudflare dashboard create a TURN
key (Realtime → TURN) and paste its id and token under “Cloudflare’s
TURN relay” (or
STOOP_CLOUDFLARE_TURN_KEY_IDandSTOOP_CLOUDFLARE_TURN_API_TOKENin.env). Nothing to forward, and it works behind CGNAT. Voice audio is ~50 kbps per stream, so a friend group stays well inside the free tier. If Cloudflare’s API is unreachable, joins go ahead without the relay rather than failing. - A TURN relay I run myself. Tick it and fill in the fields, or in
.env:STOOP_TURN_URLS(comma-separated, e.g.turn:turn.example.com:3478?transport=udp,turns:turn.example.com:5349),STOOP_TURN_USERNAME,STOOP_TURN_CREDENTIAL, andSTOOP_STUN_URLS(coturn answers STUN on the same port:stun:turn.example.com:3478).
LiveKit’s own TURN (turn: or rtc.turn_servers in livekit.yaml) also
works when Stoop supplies nothing, but it is still a port forwarded to the
machine; see the
LiveKit self-hosting docs.
Video and screen share: what it costs
Section titled “Video and screen share: what it costs”If voice works, video works: cameras and screen shares use the same ports and the same relay. What changes is bandwidth. Every viewer gets their own copy from the server, so a 1080p screen share watched by five people is roughly 10–12 Mbps up from your server, and a camera in the spotlight is 3–4 Mbps per viewer. A 40 Mbps uplink runs out past a few video viewers. Behind Cloudflare Tunnel that traffic goes through the relay and counts against the TURN allowance. Screen sharing isn’t available from phone browsers; cameras are.
Troubleshooting voice
Section titled “Troubleshooting voice”| Symptom | Cause |
|---|---|
| Joining fails with “voice is not configured” | STOOP_LIVEKIT_URL not set |
| Joining fails with an error mentioning the microphone; or you join but the mic button is stuck muted | Not a secure origin — you need HTTPS off localhost |
| Joining fails after ~15 s with “Couldn’t establish an audio connection” | Media ports unreachable from that network: not forwarded, wrong NODE_IP, or an HTTP-only tunnel with no TURN. docker compose logs livekit shows “removing participant without connection” with the ICE candidates it tried |
| Everyone shows as connected, nobody hears anyone | The same, but the media path broke after the join (a network change); leave and rejoin, then check the row above |
| A participant lingers after their tab closed | Their WebSocket to Stoop hadn’t dropped yet; it clears when it does (seconds) |