Bedrud uses LiveKit to handle real-time video and audio communication. LiveKit provides the SFU (Selective Forwarding Unit) media server, and Bedrud handles authentication, room management, and admin controls.
Embedded vs. External
Bedrud supports two LiveKit deployment modes:
- Embedded Mode (Default): The backend starts and manages a LiveKit server process internally. No additional infrastructure is required - the backend handles the LiveKit process lifecycle. When server TLS is enabled, TURN/TLS is automatically configured using the server’s certificate.
- External Mode: Bedrud connects to a separate LiveKit server or cluster. This is useful for horizontal scaling or when using a managed LiveKit Cloud instance.
Configuring External Mode
To use an external LiveKit server, set the following keys in config.yaml:
livekit:
host: "wss://livekit.example.com:7880" # Client WebSocket URL (ws:// or wss://)
internalHost: "https://livekit.example.com:7880" # Server-to-server API URL
apiKey: "your-api-key"
apiSecret: "your-api-secret"
external: true # Skip embedded LiveKit startup
skipTLSVerify: false # Set true if LiveKit uses self-signed certsWhen external is true, Bedrud skips starting the embedded LiveKit binary. The internalHost (for server-to-server API calls) and host (for client WebSocket connections) may differ if your LiveKit server is behind a reverse proxy.
The API key and secret must match the external server’s credentials.
Webhook Configuration (External Only)
When using an external LiveKit server, you must configure webhooks so LiveKit can notify Bedrud of participant disconnects and room closures. Without webhooks, the database state will go stale.
Endpoint: https://<your-domain>/api/livekit/webhook
Authentication: Uses LiveKit’s JWT signing — the same apiKey/apiSecret you configured above. No separate secret needed.
LiveKit Cloud: Settings → Webhooks → Create new webhook. Enter the endpoint URL and select your API key.
Self-hosted LiveKit: Add to your LiveKit YAML config (e.g., livekit.yaml):
webhook:
urls: ["https://bedrud.example.com/api/livekit/webhook"]
api_key: "your-api-key"Without a properly configured webhook, participant_disconnected and room_finished events are not delivered to Bedrud. The admin dashboard may show stale participant data, and rooms won’t be cleaned up automatically.
LiveKit Version Compatibility
Bedrud’s embedded LiveKit mode auto-generates a compatible config. However, if you self-host an external LiveKit server, be aware of breaking changes across LiveKit versions:
- LiveKit v1.12+ removed the top-level
tlsfield from its config. If you were usingtls:in your LiveKit YAML, remove it. Configurehttp://forinternalHostandws://forhostin Bedrud’s config. TURN TLS is still supported under theturn:section. - LiveKit v1.11 and earlier support
tls:at the top level. Usehttps://forinternalHostandwss://forhost.
When in doubt, check your LiveKit server version with livekit-server --version and consult the LiveKit release notes.
Embedded Config Generation
When TLS is enabled in embedded mode, Bedrud generates a temporary LiveKit YAML config (/tmp/bedrud-livekit-*.yaml) with:
- TURN enabled,
domainauto-set fromserver.host,udp_port: 3478,tls_port: 5349, and the server’s TLS certificate reused for TURN/TLS node_ipresolved vialivekit.nodeIP→server.host→ outbound IP auto-detectionbind_addressesomitted (LiveKit binds all interfaces by default)
The temp file is cleaned up when the LiveKit process exits. To bypass auto-generation with a static config, set livekit.configPath or LIVEKIT_CONFIG_PATH.
How It Works
1. Room Creation
When a user creates a room in Bedrud, the server does not create a LiveKit room immediately. LiveKit rooms are created on demand when the first participant joins.
2. Join Tokens
When a user joins a meeting:
-
The frontend sends a request to
/api/room/join. -
The backend verifies the user has permission to join that room.
-
The backend uses its API Key and Secret to generate a signed JWT (Join Token).
-
The token contains:
- The room name.
- The user’s identity (display name).
- Permissions - for example, whether the user can publish audio or share their screen.
-
The frontend receives this token and connects directly to the LiveKit media port (default
7880).
3. Room Controls (Admin)
The backend uses the LiveKit Go SDK to perform administrative actions:
- Kick: Disconnects a participant.
- Mute: Force-mutes a participant’s microphone.
- Permissions: Changes what a participant can do in real-time.
Network Architecture
- API Port (8090/443): Handles HTTP requests and WebSocket signaling for call setup.
- Media Port (7880): Handles video and audio data using WebRTC protocols. ICE/TCP fallback uses port 7881 when UDP is blocked.
- TURN Port (3478 UDP / 5349 TLS): Relays media for clients behind restrictive NATs or firewalls. See TURN Server Guide.
For firewall and port requirements, see WebRTC Connectivity.
Troubleshooting
Startup & Config Crashes
| Symptom | Cause | Fix |
|---|---|---|
Container crash-loops, logs could not resolve external IP | use_external_ip: true without explicit node_ip in Docker | Set node_ip: <lan-ip> under rtc: and use_external_ip: false |
LiveKit exits with TURN domain required on v1.12+ | turn.tls_port is set but no domain or TLS cert/key | Add domain: under turn:, provide cert_file/key_file, or remove tls_port for UDP-only TURN |
field tls not found on startup | LiveKit v1.12+ removed top-level tls: config field | Remove tls: block from LiveKit YAML; use http:// / ws:// in Bedrud config |
LIVEKIT_CONFIG env var not picked up | Entrypoint doesn’t parse env var (pre-v1.7 or custom entrypoint) | LiveKit reads LIVEKIT_CONFIG natively since v1.7 — verify version; pass via --config-body only if needed |
Docker: --config-body via sh -c fails with flag provided but not defined: -c | Image entrypoint is /livekit-server directly — command gets appended, not wrapped by shell | Don’t use a shell wrapper, LIVEKIT_CONFIG env var is supported natively by the binary |
Docker Compose: $LIVEKIT_CONFIG expands to empty string | Compose substitutes $VAR from host environment, not container | Use $$LIVEKIT_CONFIG to escape docker-compose variable substitution |
YAML parsing error from LIVEKIT_CONFIG | Incorrect YAML indentation or syntax | Validate: docker run --rm -e LIVEKIT_CONFIG livekit-server --config-body "$LIVEKIT_CONFIG" |
Connectivity
| Symptom | Cause | Fix |
|---|---|---|
| Admin dashboard shows “LiveKit disconnected” | Bedrud can’t reach LiveKit HTTP API | Verify internalHost in config; run curl http://<internalHost>/ from Bedrud host; check firewall |
| Token generated but client connection times out | LiveKit WebSocket unreachable from browser | Check host in livekit: config (must be reachable by clients); verify DNS/firewall; test with wscat |
| Embedded LiveKit not starting | Missing binary or permission | Ensure internal/livekit/bin/livekit-server exists (even empty file for build); check Bedrud server logs |
| Port 7880 already in use | Another process on same port | Change livekit.port or use different Docker port mapping |
| Redis connection fails in LiveKit logs | Redis unreachable or wrong address | Verify redis.address: in LiveKit YAML; check container network connectivity |
curl http://127.0.0.1:7880 returns connection refused | LiveKit crashed during startup | Check docker logs / journalctl; look for RTC/TURN validation errors near the bottom of the log |
| Token expired before client connected | Short JWT validity window | Request a fresh token via POST /api/room/join before each connection attempt |
Media & TURN
| Symptom | Cause | Fix |
|---|---|---|
| Participants join but no audio/video | UDP port range blocked or wrong node_ip | Open UDP 50000-60000; verify node_ip is the externally reachable address |
| Clients behind NAT can’t connect | TURN not configured or ports blocked | Enable TURN; open UDP 3478 (and TCP 5349 for TLS); verify TURN domain resolves |
Could not resolve external IP on startup (non-Docker) | No STUN internet access or DNS failure | Set explicit node_ip and use_external_ip: false |
| Self-signed cert errors with external LiveKit | Bedrud’s skipTLSVerify is false | Set skipTLSVerify: true in Bedrud’s livekit: config |
| Clients connect via relay unnecessarily | node_ip is a private IP behind NAT | Set node_ip to the public IP or use use_external_ip: true with STUN access |
| TURN relay not used by clients | Direct WebRTC path is working (expected) | Check chrome://webrtc-internals — srflx candidates = direct path, no TURN needed |
Webhook & State
| Symptom | Cause | Fix |
|---|---|---|
| Database shows stale active participants after disconnect | Webhook not configured for external LiveKit | Add webhook: block to LiveKit YAML with URL https://<domain>/api/livekit/webhook |
| Participants never marked inactive | Firewall blocking webhook delivery from LiveKit to Bedrud | Check LiveKit logs for webhook delivery errors; ensure port 443/8090 is reachable from LiveKit |
| Room not cleaned up after all leave | empty_timeout / departure_timeout too high | Reduce values in LiveKit YAML room: section |
Recording (Egress)
🚧 Recording is a planned feature. This guide describes functionality that will be available in a future release.
Bedrud uses LiveKit’s RoomCompositeEgress API to record rooms as MP4 files.
Prerequisites
-
Redis — LiveKit egress requires Redis for coordinating egress workers. Without Redis,
StartRoomCompositeEgressreturns permission errors. -
Egress S3 storage (external mode only) — For embedded mode, the file lives on the LiveKit file server and Bedrud downloads it directly. For external LiveKit, you must configure an
egress:section in your LiveKit YAML so recordings are stored to durable S3-compatible storage. Otherwise the producedFileURLis a local path unreachable from Bedrud.Example LiveKit YAML:
egress: s3: access_key: "your-s3-access-key" secret_key: "your-s3-secret-key" endpoint: "http://minio:9000" # S3-compatible endpoint bucket: "bedrud-recordings" region: "us-east-1" force_path_style: true # Required for MinIO/RustFS/etc.
Recording Lifecycle
- Start: Handler calls
StartRoomCompositeEgresswith a room-scoped JWT (requiresRoomRecord: trueandRoom: <roomName>). - Stop: Handler calls
StopEgress, transitions recording toprocessing. - Process: Background job (
process_recording) downloads the file from LiveKit’s file server or S3 and stores it viaChatUploadStore. - Complete: Webhook dispatched (
recording.completed) to registered webhooks.
Egress Client: Created at startup via lkutil.NewEgressClient(). If unavailable, recording is disabled but the server starts normally.
Egress API Auth: The JWT used to call StartRoomCompositeEgress / StopEgress must include both RoomRecord: true and Room: <roomName>. LiveKit’s Egress service requires room-scoped permission — bare RoomRecord without a room name returns twirp error unauthenticated: permissions denied.
Download Auth: Recording file URLs get a short-lived (5 min) LiveKit JWT scoped to the specific room for download.
Webhook Event: When a recording completes, a recording.completed event is dispatched to all webhooks subscribed to that event.
See the Recordings API for endpoint details.
Troubleshooting: Recording
| Symptom | Cause | Fix |
|---|---|---|
twirp error unauthenticated: permissions denied when starting recording | Egress JWT missing Room field in grant | Update egressAuthContext to include Room: roomName in the VideoGrant |
twirp error unauthenticated: permissions denied even with valid API key | Redis not configured for LiveKit, or egress workers unavailable | Add redis: section to LiveKit YAML; ensure Redis is healthy |
Recording starts but FileURL is a local path (e.g., /tmp/...) | No egress: S3 config — LiveKit writes to worker temp dir | Add egress.s3: block to LiveKit YAML pointing to S3-compatible storage |
process_recording job fails to download | External LK’s file URL points to inaccessible local path | Configure egress S3 so LiveKit produces S3 URLs; OR ensure Bedrud can reach LK’s file server port |
See also the TURN Server Guide for TURN-specific troubleshooting, WebRTC Connectivity for STUN/ICE/firewall debugging, and Installation Troubleshooting for port/perm/setup issues.
See also
- TURN Server Guide - TURN architecture, configuration, TLS, and troubleshooting
- WebRTC Connectivity - full STUN/ICE/TURN/SFU connectivity stack
- Architecture Overview - full system architecture