# LiveKit Setup (Self-Hosted Video Platform) LiveKit is the recommended open-source, self-hosted video platform for Reflector. It replaces Daily.co for deployments that need free, fully self-hosted video rooms with per-participant audio recording. > LiveKit runs alongside Daily.co and Whereby — you choose the platform per room. Existing Daily/Whereby setups are not affected. ## What LiveKit Provides - **Video/audio rooms** — WebRTC-based conferencing via `livekit-server` (Go SFU) - **Per-participant audio recording** — Track Egress writes each participant's audio to S3 as a separate OGG/Opus file (no composite video, no Chrome dependency) - **S3-compatible storage** — works with Garage, MinIO, AWS S3, or any S3-compatible provider via `force_path_style` - **Webhook events** — participant join/leave, egress start/end, room lifecycle - **JWT access tokens** — per-participant tokens with granular permissions ## Architecture ``` ┌─────────────────┐ Participants ────>│ livekit-server │ :7880 (WS signaling) (browser) │ (Go SFU) │ :7881 (TCP RTC) │ │ :44200-44300/udp (ICE) └────────┬────────┘ │ media forwarding ┌────────┴────────┐ │ livekit-egress │ Track Egress │ (per-track OGG) │ writes to S3 └────────┬────────┘ │ ┌────────┴────────┐ │ S3 Storage │ Garage / MinIO / AWS │ (audio tracks) │ └─────────────────┘ ``` Both services share Redis with the existing Reflector stack (same instance, same db). ## Quick Start ### Option 1: Via Setup Script (Recommended) Pass `--livekit` to the setup script. It generates all credentials and config automatically: ```bash # First run — --livekit generates credentials and config files ./scripts/setup-selfhosted.sh --gpu --ollama-gpu --livekit --garage --caddy # Re-runs — LiveKit is auto-detected from existing LIVEKIT_API_KEY in server/.env ./scripts/setup-selfhosted.sh ``` The `--livekit` flag will: 1. Generate `LIVEKIT_API_KEY` and `LIVEKIT_API_SECRET` (random credentials) 2. Set `LIVEKIT_URL`, `LIVEKIT_PUBLIC_URL`, and storage credentials in `server/.env` 3. Generate `livekit.yaml` and `egress.yaml` config files 4. Set `DEFAULT_VIDEO_PLATFORM=livekit` 5. Enable the `livekit` Docker Compose profile 6. Start `livekit-server` and `livekit-egress` containers On subsequent re-runs (without flags), the script detects the existing `LIVEKIT_API_KEY` in `server/.env` and re-enables the profile automatically. ### Option 2: Manual Setup If you prefer manual configuration: 1. **Generate credentials:** ```bash export LK_KEY="reflector_$(openssl rand -hex 8)" export LK_SECRET="$(openssl rand -hex 32)" ``` 2. **Add to `server/.env`:** ```env # LiveKit connection LIVEKIT_URL=ws://livekit-server:7880 LIVEKIT_API_KEY=$LK_KEY LIVEKIT_API_SECRET=$LK_SECRET LIVEKIT_PUBLIC_URL=wss://your-domain:7880 # or ws://your-ip:7880 # LiveKit egress S3 storage (reuse transcript storage or configure separately) LIVEKIT_STORAGE_AWS_BUCKET_NAME=reflector-bucket LIVEKIT_STORAGE_AWS_REGION=us-east-1 LIVEKIT_STORAGE_AWS_ACCESS_KEY_ID=your-key LIVEKIT_STORAGE_AWS_SECRET_ACCESS_KEY=your-secret LIVEKIT_STORAGE_AWS_ENDPOINT_URL=http://garage:3900 # for Garage/MinIO # Set LiveKit as default platform for new rooms DEFAULT_VIDEO_PLATFORM=livekit ``` 3. **Create `livekit.yaml`:** ```yaml port: 7880 rtc: tcp_port: 7881 port_range_start: 44200 port_range_end: 44300 redis: address: redis:6379 keys: your_api_key: your_api_secret webhook: urls: - http://server:1250/v1/livekit/webhook api_key: your_api_key logging: level: info room: empty_timeout: 300 max_participants: 0 ``` 4. **Create `egress.yaml`:** ```yaml api_key: your_api_key api_secret: your_api_secret ws_url: ws://livekit-server:7880 health_port: 7082 log_level: info session_limits: file_output_max_duration: 4h ``` 5. **Start with the livekit profile:** ```bash docker compose -f docker-compose.selfhosted.yml --profile livekit up -d livekit-server livekit-egress ``` ## Environment Variables Reference ### Required | Variable | Description | Example | |----------|-------------|---------| | `LIVEKIT_URL` | Internal WebSocket URL (server -> LiveKit) | `ws://livekit-server:7880` | | `LIVEKIT_API_KEY` | API key for authentication | `reflector_a1b2c3d4e5f6` | | `LIVEKIT_API_SECRET` | API secret for token signing and webhooks | `64-char hex string` | ### Recommended | Variable | Description | Example | |----------|-------------|---------| | `LIVEKIT_PUBLIC_URL` | Public WebSocket URL (browser -> LiveKit). **Must be reachable from participants' browsers**, not a Docker-internal address. Without `--domain`, set to `ws://:7880`. With `--domain`, set to `wss://:7880`. | `wss://reflector.example.com:7880` | | `LIVEKIT_WEBHOOK_SECRET` | Webhook verification secret. Defaults to `LIVEKIT_API_SECRET` if not set. Only needed if you want a separate secret for webhooks. | (same as API secret) | | `DEFAULT_VIDEO_PLATFORM` | Default platform for new rooms | `livekit` | ### Storage (for Track Egress) Track Egress writes per-participant audio files to S3. If not configured, falls back to the transcript storage credentials. | Variable | Description | Example | |----------|-------------|---------| | `LIVEKIT_STORAGE_AWS_BUCKET_NAME` | S3 bucket for egress output | `reflector-bucket` | | `LIVEKIT_STORAGE_AWS_REGION` | S3 region | `us-east-1` | | `LIVEKIT_STORAGE_AWS_ACCESS_KEY_ID` | S3 access key | `GK...` | | `LIVEKIT_STORAGE_AWS_SECRET_ACCESS_KEY` | S3 secret key | `...` | | `LIVEKIT_STORAGE_AWS_ENDPOINT_URL` | S3 endpoint (for Garage/MinIO) | `http://garage:3900` | ## Docker Compose Services Two services are added under the `livekit` profile in `docker-compose.selfhosted.yml`: ### livekit-server | Setting | Value | |---------|-------| | Image | `livekit/livekit-server:v1.10.1` | | Ports | 7880 (signaling), 7881 (TCP RTC), 44200-44300/udp (ICE) | | Config | `./livekit.yaml` mounted at `/etc/livekit.yaml` | | Depends on | Redis | ### livekit-egress | Setting | Value | |---------|-------| | Image | `livekit/egress:v1.10.1` | | Config | `./egress.yaml` mounted at `/etc/egress.yaml` | | Depends on | Redis, livekit-server | No `--cap-add=SYS_ADMIN` is needed because Track Egress does not use Chrome (that's only for Room Composite video recording, which we don't use). ## Port Ranges | Range | Protocol | Service | Notes | |-------|----------|---------|-------| | 7880 | TCP | LiveKit signaling | WebSocket connections from browsers (direct, no Caddy) | | 7881 | TCP | LiveKit RTC over TCP | Fallback when UDP is blocked | | 44200-44300 | UDP | LiveKit ICE | WebRTC media. Avoids collision with Reflector WebRTC (40000-40100) and macOS ephemeral ports (49152-65535) | ### TLS / Caddy Integration When `--caddy` is enabled (HTTPS), the setup script automatically: 1. Adds a `/lk-ws` reverse proxy route to the Caddyfile that proxies `wss://domain/lk-ws` → `ws://livekit-server:7880` 2. Sets `LIVEKIT_PUBLIC_URL` to `wss:///lk-ws` (or `wss:///lk-ws`) This avoids mixed-content blocking (browsers reject `ws://` connections on `https://` pages). Caddy handles TLS termination; LiveKit server itself runs plain WebSocket internally. Without `--caddy`, browsers connect directly to LiveKit on port 7880 via `ws://`. ### Security Note: on_demand TLS When using `--ip` (Caddy with self-signed certs), the Caddyfile uses `tls internal { on_demand }`. This generates certificates dynamically for any hostname/IP on first TLS request. **Risk:** An attacker can trigger certificate generation for arbitrary hostnames by sending TLS requests with spoofed SNI values, causing disk and CPU usage. This is a low-severity resource exhaustion risk, not a data theft risk. **Mitigations:** - For LAN/development use: not a concern (not internet-exposed) - For cloud VMs: restrict port 443 access via firewall to trusted IPs - For production: use `--domain` with a real domain name instead of `--ip` — Caddy uses Let's Encrypt (no `on_demand` needed) | Deployment | `LIVEKIT_PUBLIC_URL` | How it works | |---|---|---| | localhost, no Caddy | `ws://localhost:7880` | Direct connection | | LAN IP, no Caddy | `ws://192.168.1.x:7880` | Direct connection | | IP + Caddy | `wss://192.168.1.x/lk-ws` | Caddy terminates TLS, proxies to LiveKit | | Domain + Caddy | `wss://example.com/lk-ws` | Caddy terminates TLS, proxies to LiveKit | ## Webhook Endpoint LiveKit sends webhook events to `POST /v1/livekit/webhook`. Events handled: | Event | Action | |-------|--------| | `participant_joined` | Logs participant join, updates meeting state | | `participant_left` | Logs participant leave | | `egress_started` | Logs recording start | | `egress_ended` | Logs recording completion with output file info | | `room_started` / `room_finished` | Logs room lifecycle | Webhooks are authenticated via JWT in the `Authorization` header, verified using the API secret. ## Frontend The LiveKit room component uses `@livekit/components-react` with the prebuilt `` UI. It includes: - Recording consent dialog (same as Daily/Whereby) - Email transcript button (feature-gated) - Extensible overlay buttons for custom actions When a user joins a LiveKit room, the backend generates a JWT access token and returns it in the `room_url` query parameter. The frontend parses this and passes it to the LiveKit React SDK. ## Separate Server Deployment For larger deployments (15+ participants, multiple simultaneous rooms), LiveKit can run on a dedicated server: 1. Run `livekit-server` and `livekit-egress` on a separate machine 2. Point `LIVEKIT_URL` to the remote LiveKit server (e.g., `ws://livekit-host:7880`) 3. Set `LIVEKIT_PUBLIC_URL` to the public-facing URL (e.g., `wss://livekit.example.com`) 4. Configure the remote LiveKit's `webhook.urls` to point back to the Reflector server 5. Both need access to the same Redis (or configure LiveKit's own Redis) 6. Both need access to the same S3 storage ## Troubleshooting ### LiveKit server not starting ```bash # Check logs docker compose -f docker-compose.selfhosted.yml logs livekit-server --tail 30 # Verify config cat livekit.yaml # Common issues: # - Redis not reachable (check redis service is running) # - Port 7880 already in use # - Invalid API key format in livekit.yaml ``` ### Participants can't connect ```bash # Check that LIVEKIT_PUBLIC_URL is accessible from the browser # It must be the URL the browser can reach, not the Docker-internal URL # Check firewall allows ports 7880, 7881, and 44200-44300/udp sudo ufw status # or iptables -L # Verify the access token is being generated docker compose -f docker-compose.selfhosted.yml logs server | grep livekit ``` ### Track Egress not writing files ```bash # Check egress logs docker compose -f docker-compose.selfhosted.yml logs livekit-egress --tail 30 # Verify S3 credentials # Egress receives S3 config per-request from the server, so check server/.env: grep LIVEKIT_STORAGE server/.env ```