Servers reachable via both a LAN IP and a public IP could only serve voice on one of them: config.yaml accepts a single voice.node_ip and OwnCord regenerates data/livekit.yaml on every start, discarding manual edits. LiveKit has no multi-IP list, but it does support advertising internal host candidates alongside the external mapping. - New voice.advertise_internal_ip (OWNCORD_VOICE_ADVERTISE_INTERNAL_IP): emits rtc.advertise_internal_ip: true so LAN clients get a reachable candidate while remote clients keep using node_ip. - livekit.yaml escape hatch: if the file exists without the auto-generated marker header, OwnCord leaves it untouched, giving operators access to every LiveKit option (ips.includes, interfaces, stun_servers, ...). The generated header documents how to take ownership. Closes #111 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwtnpHAoSFr1ZibQgQkNQK
8.4 KiB
LiveKit Setup Guide
LiveKit is an open-source SFU (Selective Forwarding Unit) that handles real-time voice and video. OwnCord uses it instead of rolling its own WebRTC stack -- LiveKit handles all the hard parts (DTLS, ICE, codec negotiation, simulcast) while OwnCord manages permissions, state, and room lifecycle.
There are two ways to run LiveKit alongside OwnCord:
| Method | Best for | LiveKit managed by |
|---|---|---|
| Docker Compose | Linux servers | Docker (separate container) |
| Companion process | Windows / bare-metal Linux | OwnCord (auto-start) |
Docker
When running OwnCord via docker compose, LiveKit runs as a separate container on the same internal network. OwnCord reaches it at ws://livekit:7880 via Docker's internal DNS — no port forwarding needed between containers.
Setup
-
Edit
.env(inServer/) — setLIVEKIT_API_KEYandLIVEKIT_API_SECRET:LIVEKIT_API_KEY=my-unique-key LIVEKIT_API_SECRET=my-secret-at-least-32-characters-long -
Edit
livekit.yaml(copy fromlivekit.yaml.example) — use the same key/secret and set your public IP:port: 7880 rtc: tcp_port: 7881 port_range_start: 50000 port_range_end: 60000 node_ip: "YOUR_SERVER_PUBLIC_IP" # required for remote clients keys: my-unique-key: my-secret-at-least-32-characters-long logging: level: info -
Leave
voice.livekit_binaryunset in yourconfig.yaml. Thevoice.livekit_urlshould bews://livekit:7880(Docker DNS). -
Open firewall ports on your host:
Port Protocol Purpose 7880TCP LiveKit signaling 7881TCP TCP fallback for WebRTC 50000-60000UDP WebRTC media
node_ipis required for remote clients. Without it, LiveKit advertises internal Docker IP addresses as ICE candidates, which are unreachable from the internet. If your cloud VM has a metadata service (AWS, GCP, DigitalOcean) you can useuse_external_ip: trueinstead.
Companion Process (Windows / bare-metal Linux)
1. Get the LiveKit Binary
Download livekit-server for your platform from one of:
- GitHub releases: https://github.com/livekit/livekit/releases
- Grab the
livekit-server_*_windows_amd64.zipasset
- Grab the
- LiveKit website: https://livekit.io/ (Docs > Self Hosting)
Extract the binary somewhere permanent (e.g. C:\livekit\livekit-server.exe).
2. Server Configuration
LiveKit settings live in the voice: section of config.yaml:
voice:
livekit_api_key: "devkey"
livekit_api_secret: "owncord-dev-secret-key-min-32chars"
livekit_url: "ws://localhost:7880"
livekit_binary: "C:/livekit/livekit-server.exe"
quality: "medium"
| Field | Purpose | Default |
|---|---|---|
livekit_api_key |
Shared API key between OwnCord and LiveKit | "devkey" |
livekit_api_secret |
Shared secret for JWT signing (min 32 chars) | "owncord-dev-secret-key-min-32chars" |
livekit_url |
LiveKit WebSocket URL | ws://localhost:7880 |
livekit_binary |
Path to livekit-server binary. Empty = assume externally managed |
"" (disabled) |
node_ip |
Public IP for WebRTC ICE candidates (remote users behind NAT) | "" (auto-detect) |
advertise_internal_ip |
Also advertise LAN IPs — enable on dual-homed servers (LAN + public IP) so local clients can connect | false |
quality |
Default voice quality preset | "medium" |
Environment variable overrides use the OWNCORD_ prefix: OWNCORD_VOICE_LIVEKIT_API_KEY, OWNCORD_VOICE_LIVEKIT_API_SECRET, etc.
Warning
: The server logs a warning at startup if you use the default dev key/secret. Always change these for production.
3. Ports and Firewall
| Port | Protocol | Purpose |
|---|---|---|
| 7880 | TCP (HTTP/WS) | LiveKit signaling (WebSocket + REST API) |
| 7881 | TCP | LiveKit internal RTC (TURN/TCP fallback) |
| 50000-60000 | UDP | Media transport (RTP audio/video) |
For LAN-only setups, ensure these ports are open on Windows Firewall. For remote access, forward these through your router or use Tailscale.
4. How the Companion Process Works
When livekit_binary is set, OwnCord manages LiveKit as a companion process:
- Config generation: OwnCord auto-generates
data/livekit.yamlwith the API key/secret, port 7880, and UDP range 50000-60000. To manage the file yourself (customrtcoptions, multiple interfaces, ...), delete the header line containing the auto-generated marker — OwnCord then leaves the file untouched on future starts. Yourkeys:entry must still matchvoice.livekit_api_key/voice.livekit_api_secret. - Process launch:
livekit-server --config data/livekit.yaml - Crash recovery: Exponential backoff restart (3s -> 6s -> 12s ... up to 60s), gives up after 10 consecutive rapid failures
- Health checks:
GET http://localhost:7880/verifies LiveKit is responding - Graceful shutdown: Stops the process when OwnCord shuts down (5s timeout before kill)
If livekit_binary is empty, OwnCord assumes LiveKit is managed externally (e.g. Docker, systemd, or manual start).
5. Token Flow
How a client joins voice:
Client OwnCord Server LiveKit Server
| | |
|-- voice_join (channel_id)-->| |
| |-- check CONNECT_VOICE |
| |-- persist to voice_states |
| |-- GenerateToken() |
|<-- voice_token ------------| |
| { token, url, | |
| direct_url } | |
| | |
|-- connect with JWT --------|-------------------------->|
|<--- media streams ----------|--------------------------|
Token details:
- Room name:
"channel-{channelID}" - Identity:
"user-{userID}" - TTL: 24 hours (refresh at 23h)
canPublishis derived from theSPEAK_VOICEpermissioncanSubscribeis always true- Client can request refresh via
voice_token_refresh(rate limited to 1/60s)
Client connection paths:
- Proxy path (
/livekit): Client connects through OwnCord's HTTPS server. Avoids mixed-content issues. - Direct URL (
ws://localhost:7880): Used when the client is on localhost.
6. Webhook Integration
LiveKit sends webhooks to POST /api/v1/livekit/webhook. The endpoint verifies the JWT and handles participant_left to clean up ghost voice states when a user disconnects from LiveKit without sending a voice_leave message.
7. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "voice not configured" error | LiveKit client failed to initialize | Check livekit_api_key and livekit_api_secret are set and secret is >= 32 chars |
| "failed to generate voice token" | API key/secret mismatch | Ensure config.yaml key/secret match what LiveKit is using |
| Voice connects but no audio | Firewall blocking UDP 50000-60000 | Open UDP port range in Windows Firewall |
"backend unavailable" from /livekit proxy |
LiveKit not running on port 7880 | Check livekit_binary path or start LiveKit manually |
| "too many rapid failures, giving up" in logs | LiveKit binary crashes on startup | Run livekit-server --config data/livekit.yaml manually to see errors |
| Mixed content / insecure WS error | Client using direct URL over HTTPS page | Client should use the /livekit proxy path |
| Voice works via public IP but not on the LAN (dual-homed server) | LiveKit only advertises the public node_ip |
Set voice.advertise_internal_ip: true so LAN host candidates are advertised too |
GET /api/v1/livekit/health returns degraded |
LiveKit server not reachable | Verify LiveKit is running: curl http://localhost:7880 |
8. Production Checklist
- Change
livekit_api_keyfrom"devkey"to a random string - Change
livekit_api_secretto a random 32+ character string - Open firewall ports: 7880/TCP, 50000-60000/UDP
- If using ACME/manual TLS, ensure LiveKit proxy at
/livekitis working - Test voice by joining a voice channel from two clients
- Check
/api/v1/livekit/healthreturns{"status": "ok"}