webrtc-livekit
Context: $ARGUMENTS
LiveKit is an Apache-2.0 WebRTC SFU written in Go — a single-binary deployment with a vast SDK matrix. Docs home: https://docs.livekit.io/home/. Repo: https://github.com/livekit/livekit.
Quick start
- Install server binary for your platform: → Step 1 (
livekit.py install-server) - Install the CLI (
lk/livekit-cli): → Step 1 (livekit.py install-cli) - Start a dev server: → Step 2 (
livekit.py start) - Mint a JWT access token: → Step 3 (
livekit.py mint-token) - Record or stream-out a room: → Step 5 (
livekit.py egress-start) - Ingest RTMP/WHIP/SRT into a room: → Step 6 (
livekit.py ingress-start)
Data model
Room (per-meeting)
├── Participant (per-user)
│ ├── TrackPublication (the publisher's declaration)
│ │ └── Track (camera / mic / screen-share / data)
│ └── Permissions (from JWT grants)
└── Metadata (free-form, room-wide or per-participant)
Tokens are JWT (HS256). Each token carries a video grant describing which room and which permissions.
Step 1 — Install the binaries
# Server
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py install-server --dest ~/bin
# CLI ("lk", formerly "livekit-cli")
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py install-cli --dest ~/bin
The script downloads the correct release from github.com/livekit/livekit/releases (server) and github.com/livekit/livekit-cli/releases (CLI) for your uname -sm, extracts it, and places the binary in --dest. No sudo required if --dest is writable. Homebrew and the official install.sh exist upstream — pick whichever fits your OS.
Step 2 — Start a dev server
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py start --dev
--dev launches with a built-in dev API key / secret (devkey / secret) and enabled self-signed TURN on 443. For production, write a YAML config:
# livekit.yaml (abridged — see references/deployment.md for the full set)
port: 7880
bind_addresses: ['']
rtc:
port_range_start: 50000
port_range_end: 60000
tcp_port: 7881
use_external_ip: true
keys:
devkey: secret
redis:
address: redis:6379
Run: livekit-server --config livekit.yaml.
Step 3 — Mint access tokens (JWT HS256, pure Python)
LiveKit tokens are JWTs with an iss = API key, sub = participant identity, video grant, and standard iat / exp. The helper mints them with stdlib only (hmac + hashlib + base64 + json):
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py mint-token \
--api-key devkey --api-secret secret \
--room my-room --identity alice \
--name 'Alice' --ttl 3600 \
--grants roomJoin=true,canPublish=true,canSubscribe=true,canPublishData=true
Token schema (see references/grants.md for every flag):
{
"iss": "devkey",
"sub": "alice",
"name": "Alice",
"iat": 1710000000,
"exp": 1710003600,
"nbf": 1710000000,
"video": {
"room": "my-room",
"roomJoin": true,
"canPublish": true,
"canSubscribe": true,
"canPublishData": true,
"hidden": false,
"recorder": false
}
}
Step 4 — Use the CLI
lk room list --api-key KEY --api-secret SECRET --url ws://localhost:7880
lk room join --url ws://localhost:7880 --api-key KEY --api-secret SECRET --identity alice my-room
lk load-test --url ws://localhost:7880 --api-key KEY --api-secret SECRET --room test --publishers 5 --subscribers 20
The helper wraps these (passes through to lk):
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py room-list
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py room-join --room my-room --identity alice
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py load-test --config load-test.yaml
Step 5 — Egress (record / stream out)
livekit-egress is a separate binary (github.com/livekit/egress) that connects to a Room and records or re-streams it. Outputs: MP4 file, HLS manifest, or RTMP push.
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py egress-start \
--url ws://localhost:7880 --api-key KEY --api-secret SECRET \
--room my-room --type room-composite --layout grid \
--output mp4 --dest ./recording.mp4
Egress types:
room-composite— lay out all participants into one MP4/HLS stream.track-composite— specific set of tracks (audio + screen share, etc.).track— single track, no composite (raw stream).web— load an arbitrary URL (like a Grafana dashboard) and record it.participant— one participant's outgoing tracks.
Egress runs best as a separate service in its own container (uses headless Chromium); docs: https://docs.livekit.io/home/egress/overview/.
Step 6 — Ingress (RTMP / WHIP / SRT / URL into a Room)
livekit-ingress (github.com/livekit/ingress) accepts RTMP / WHIP / SRT / URL pulls and injects them as Participants.
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py ingress-start \
--url ws://localhost:7880 --api-key KEY --api-secret SECRET \
--room my-room --type rtmp --name 'OBS Feed' --identity obs-1
Returns an ingest URL and stream key. Ingress must run as a sidecar too.
SDK matrix
Official SDKs (all link from https://docs.livekit.io/home/client/):
- JS/TS —
livekit-client(browser + Node) - Python —
livekiton PyPI - Go —
github.com/livekit/server-sdk-go - Rust —
livekiton crates.io - Swift —
LiveKitfor iOS/macOS - Kotlin — Android
- Flutter —
livekit_client - Unity — WebRTC-based
- React / React Native —
@livekit/components-react/livekit-react-native - Node.js server SDK —
livekit-server-sdk(for minting tokens + webhooks) - Ruby, PHP — community + first-party server SDKs
LiveKit Agents (realtime AI)
https://docs.livekit.io/agents/ — framework for realtime voice/video agents that join Rooms. Pair with OpenAI Realtime, Deepgram, ElevenLabs etc. Python + Node.js.
Gotchas
- JWT algorithm is HS256. Do NOT use RS256 or the server rejects it. The helper enforces HS256.
expis mandatory. Missingexp→ 401 at room join.- Identity must be unique per room. Re-joining with the same identity kicks the previous participant.
canPublish:falsetokens still count against room-size limits — hidden observers needhidden:trueto not appear in participant lists.recorder:truegrant is whatlivekit-egressuses — mark your egress participant with it so the server doesn't include it in room composites.- TCP fallback port (
rtc.tcp_port) must be reachable on your public IP; otherwise peers behind strict firewalls fail to connect. - Use a shared Redis for multi-node — without it, participants on different nodes cannot see each other.
- CLI was renamed from
livekit-clitolkin late 2024. The long name still works via symlink in most builds. - The dev key
devkey:secretis insecure. For any deployment that's reachable from the internet, generate fresh keys —lk create-api-key. - Egress containers want 2+ vCPU and 2 GB RAM each — composite recording drives headless Chromium + ffmpeg. Underprovisioning causes frame drops.
- Ingress RTMP stream keys leak the room name if you're not careful — use per-stream keys and rotate.
use_external_ip:truereads cloud metadata endpoints on AWS/GCP/Azure. On-prem: setnode_ipmanually.- Simulcast is ON by default for browser publishers. Turn it off via
VideoPresetson the client for single-layer encoders.
Examples
Example 1 — Dev server + token + join
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py install-server --dest ~/bin
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py start --dev # in one terminal
# in another
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py mint-token \
--api-key devkey --api-secret secret \
--room test --identity alice --ttl 3600 \
--grants roomJoin=true,canPublish=true,canSubscribe=true
# Paste the token into the Meet demo at https://meet.livekit.io
Example 2 — Record a room to MP4
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py egress-start \
--url ws://localhost:7880 --api-key devkey --api-secret secret \
--room test --type room-composite --output mp4 --dest ./out.mp4
Example 3 — RTMP ingress from OBS
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py ingress-start \
--url ws://localhost:7880 --api-key devkey --api-secret secret \
--room test --type rtmp --identity obs-cam
Response contains url + streamKey — paste into OBS Stream settings.
Example 4 — 50-participant load test
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py load-test \
--url ws://localhost:7880 --api-key devkey --api-secret secret \
--room bench --publishers 10 --subscribers 40 --duration 60s
Example 5 — Short-lived recorder token (no publish)
uv run ${CLAUDE_SKILL_DIR}/scripts/livekit.py mint-token \
--api-key devkey --api-secret secret \
--room test --identity rec-bot --ttl 600 \
--grants roomJoin=true,canSubscribe=true,hidden=true,recorder=true
Troubleshooting
Client gets 401 at room.connect
Cause: token signature invalid, expired, wrong API key, or missing roomJoin grant.
Solution: re-mint with mint-token, verify API secret matches server config.
ICE succeeds but no media arrives
Cause: rtc.port_range_start..end UDP range blocked at firewall; no TCP fallback port.
Solution: open the UDP range publicly (50000-60000 by default), plus rtc.tcp_port.
"No room service available"
Cause: talking to the wrong port (7880 is HTTP + WS), or Redis is required but unreachable.
Solution: check docker logs / server stdout for Redis errors.
Egress produces 0-byte MP4
Cause: Chromium failed to launch (missing libatk, libxss, libgbm on slim containers).
Solution: use the official egress image livekit/egress:latest which bundles deps.
Participant appears twice after reconnect
Cause: old session not cleaned up; client using same identity. Solution: server will auto-kick the older session; ensure the client uses a fresh identity or reuse the existing token.
Reference docs
- Read
references/grants.mdfor the full VideoGrant / RoomAdmin grant catalog with per-flag permissions. - Read
references/deployment.mdfor the self-hosted stack (livekit-server, Redis, TURN via bundled Pion or coturn, egress + ingress sidecars, sample YAML).