A self-contained, local live game-streaming platform — think a miniature Twitch you
can run on one machine with docker compose up. Real accounts and stream keys, gated
RTMP/WebRTC ingest, an adaptive transcode ladder, three playback latency tiers, live chat
rooms, and a clip recommender — all served same-origin from http://localhost:8088.
Screenshots: drop your own into
docs/and reference them here, e.g.— the UI is a neon/HUD esports theme.
┌─────────── Volt API (FastAPI + SQLite) ───────────┐
│ accounts · JWT · stream keys · presence · chat │
└─▲──────────────▲───────────────────────▲──────────┘
publish auth ───────────┘ POST /chat (JWT) heartbeat │ ▲ paths/list
│ │ │ │ (liveness)
OBS / browser ─RTMP/WHIP─▶ mediamtx ──┬─ LL-HLS ─┐ │ │
│ └─ WebRTC ──┤ │ │
transcoder ◀─RTSP─ source (WHEP) │ │ │
└─ 1080/720/480/360 rungs ─▶ mediamtx ─────┤ │ │
└─ ABR master (HLS) ─▶ disk ───────────────┤ │ │
│ │ │ │
browser ◀─ :8088/ ─▶ nginx ◀─ /hls /abr /whep ──┘ :8088/api ───┘──┘
│ ◀─ /mqtt ─▶ mosquitto (chat, subscribe-only)
│ ◀─ /reco ─▶ recommender
| Service | Role |
|---|---|
api/ |
FastAPI + SQLite — accounts, JWT auth, per-user stream keys, publish gating, chat rooms (history/presence/join-leave), channel directory, viewer presence |
mediamtx/ |
Media server — RTMP ingest, Low-Latency HLS, WebRTC (WHIP publish / WHEP playback), RTSP (internal) |
transcoder/ |
ffmpeg orchestrator — per-channel ABR ladder (1080/720/480/360) as LL-HLS rungs + a multivariant ABR master |
rtmp/ |
nginx front door — serves the web app and reverse-proxies HLS / API / chat / reco |
mosquitto/ |
MQTT broker — realtime chat transport; browsers are read-only (ACL), only the API publishes |
reco/ |
Flask clip recommender (runs on synthetic demo models) |
web/ |
The watch/broadcast UI (vanilla JS + hls.js + WebRTC) |
docker compose up -d --build
open http://localhost:8088Instant demo: add the demo profile and a broadcast starts automatically as @demo:
docker compose --profile demo up -d --build # a live channel within secondsSign up, then Go Live. Stream three ways:
- Browser (no OBS): Go Live → Camera + mic or Share screen (WebRTC/WHIP).
- OBS / any RTMP encoder: Server
rtmp://localhost:1935, key<user>?user=<user>&pass=<streamkey>. - CLI test pattern:
./stream.sh <user> --key <streamkey>(seeded demo:./stream.sh demo --key demokey).
The player's quality selector + the ⚡ Real-time toggle expose every tier:
| Tier | Transport | Latency | Switching |
|---|---|---|---|
| ⚡ Real-time | WebRTC (WHEP) | sub-second | fixed (source) |
| Source / 1080 / 720 / 480 / 360 ⚡ | LL-HLS rung | ~1–2s | manual |
| Auto (ABR) | multivariant HLS | ~3–6s | automatic (adapts to bandwidth) |
Browser/WebRTC-published streams auto-route to the Real-time tier; the transcoder reads every source over RTSP and normalizes audio to AAC, so WebRTC (Opus) and RTMP (AAC) sources both flow through the full LL-HLS + ABR ladder.
- Accounts: bcrypt-hashed passwords in SQLite; 7-day JWT sessions.
- Stream keys: per-user secret, resettable. Publishing is gated — mediamtx calls the API to validate the key before accepting a frame (wrong key → rejected at the handshake).
- Chat is spoof-proof: browsers only subscribe over MQTT (broker ACL). Sends go to the API with a JWT and are published server-side with a verified identity.
- Dev secrets (
JWT_SECRET,MQTT_API_PASSWORD,TRANSCODER_SECRET, reco client secret,hlsCDNSecret) live indocker-compose.ymlfor local use — change them before exposing.
| What | URL |
|---|---|
| Watch app | http://localhost:8088/ |
| Auth | POST /api/auth/signup · POST /api/auth/login · GET /api/me |
| Channels (live now) | GET /api/channels · GET /api/channels/<name> |
| Chat | GET /api/chat/<ch>/history · GET /api/chat/<ch>/presence · POST /api/chat/<ch> |
| RTMP ingest | rtmp://localhost:1935/<user>?user=<user>&pass=<key> |
| WebRTC publish / play | http://localhost:8889/<user>/whip · /whep |
| LL-HLS / ABR | http://localhost:8088/hls/<user>/index.m3u8 · /abr/<user>/master.m3u8 |
Each channel is a room with history (last 50 messages replayed on join), presence ("N online" with usernames), and join/leave system messages — all published server-side with JWT-verified identity over MQTT.
Every broadcast is recorded (fMP4, self-deleting after 12h locally) and exposed by the
playback server, proxied at /vod/:
- 📼 Past broadcasts —
GET /api/vods/<channel>lists them; click to replay in the app. - ✂ Clip the last 30s — logged-in viewers hit the Clip button while a stream is live;
the API cuts the clip from the recording, generates a thumbnail, announces it in chat,
and it appears in the shared clips rail (
GET /api/clips). Internal transcode rungs are excluded from recording.
scripts/e2e.sh runs a full end-to-end pass against the running stack — auth,
wrong-key publish rejection, HLS packaging, the transcode ladder, the ABR master,
chat round-trip, presence, VOD recording, and clip creation (19 checks). CI runs it
on every push, alongside lint and image-build jobs.
scripts/bench.py (stdlib-only) load-tests the delivery hot paths and prints
req/s + latency percentiles for the cached segment, static playlist, and micro-cached
API endpoints.
Volt is architecturally faithful but runs on one node. Going from "works locally" to "serves millions concurrently" is mostly a distribution, transcoding, and operations problem. Here are the real challenges and how each is solved in production.
The scale-limiting property of a naive design is that origin work grows with the audience. Volt's code removes that coupling — the four things that let real platforms put a CDN in front and fan out arbitrarily:
| Mechanism | What it does | Effect |
|---|---|---|
Edge segment cache (nginx proxy_cache + proxy_cache_lock) |
Media segments are immutable → cached and coalesced | Each segment is fetched from the origin exactly once, whether 10 or 10M viewers ask |
| API micro-cache (1s + coalescing, anonymous GETs only) | Collapses viewer polling | 1M viewers polling /api/channels cost the API ~1 req/s |
| Stateless API via Redis | Presence, chat history, rate limits live in Redis, not process memory | docker compose up --scale api=N just works (verified with 3 replicas) |
| Sampled presence | Server assigns a heartbeat sampling rate; count = tracked ÷ rate | Viewer counting costs O(1) (≤ ~2k tracked heartbeats/channel) at ANY audience size |
Measured on one M-series laptop (python3 scripts/bench.py, 50 connections):
LL-HLS media segment (edge cache) 13,300 req/s · p50 2.9ms
ABR master playlist (static file) 13,800 req/s · p50 2.8ms
/api/channels (1s micro-cache) 4,300 req/s · p50 2.5ms
The honest 1.5-billion-viewer math: 1.5B concurrent × ~3 Mbps ≈ 4.5 Pbps — that is ~45,000 × 100GbE edge servers, i.e. a planet-scale CDN, not a code change. What the code does control is the origin's side of that equation, and here it's solved: per-segment origin cost is O(edge nodes), control-plane cost is O(1) per second, and the API tier scales horizontally. Every viewer beyond the first hits cache, not origin.
A single nginx can't fan out to millions. The dominant cost and bottleneck is delivery.
- Edge CDN (CloudFront / Fastly / Cloudflare, or your own edge tier) caches HLS
segments close to viewers. Segments are immutable → cache them aggressively; only
playlists are
no-cache. - Origin shielding / mid-tier caching so a viewer spike doesn't stampede the origin.
- Multi-CDN with steering for resilience and cost.
- Local analog already here: an nginx
proxy_cachelayer in front of mediamtx/transcoder is the same shape — add HTTP/2, immutable-segment caching,sendfile, keepalive.
Software libx264 (what the transcoder uses) is CPU-bound — ~4 encodes/channel here.
- Hardware encoding (NVENC / Quick Sync / AMF, or VideoToolbox on Mac) cuts CPU 5–10× and is how platforms transcode at scale. Note: Docker-on-Mac has no GPU passthrough — run the transcoder natively or on a Linux/NVENC box.
- Per-title / content-aware encoding to spend bitrate only where it helps.
- Passthrough the top rung (don't re-encode the source resolution) to save an encode.
- Kubernetes + KEDA: scale transcoder pods on queue depth / live-channel count, with GPU node pools for HW encode. Scale to zero when idle.
- Place a job queue between ingest and transcode so bursts don't drop streams.
- Ingest PoPs so streamers connect to the nearest edge (RTMP/SRT), with SRT for lossy networks. Relay to a regional origin for packaging/transcoding.
- LL-HLS is tuned here (~1–2s); push further with smaller parts + HTTP/2/3.
- WebRTC / "warp"-style delivery for sub-second at scale (hard to fan out — needs SFUs).
- Twitch chat is sharded, IRC-derived, and handles millions of concurrent connections.
- Replace single-broker MQTT with a sharded pub/sub (NATS / Redis Streams / Kafka), Redis for presence/history fan-out, per-room sharding, and rate-limiting/moderation (slow mode, bans, automod) at the edge.
- Move SQLite → Postgres (users, channels), Redis for hot state (presence, sessions, leaderboards), and object storage (S3) for recordings → VOD/clips pipeline.
- mediamtx exposes Prometheus metrics → Grafana dashboards.
- Track the QoE metrics platforms live by: startup time, rebuffer ratio, glass-to-glass latency, ABR switches, concurrent viewers, transcode fps, ingest health.
- Health checks, graceful draining, multi-AZ/region failover, circuit breakers between tiers, and chaos testing. No single point of failure on ingest, transcode, or edge.
TL;DR: the interfaces here — HLS, WebRTC (WHIP/WHEP), JWT auth, MQTT topics, a stream-key gate, an ABR ladder — are the same shapes Twitch uses. Scaling is about putting a CDN in front, hardware-accelerated + autoscaled transcoding behind, sharded chat/state beside, and deep QoE observability around it.
docker compose down -v # stop everything + remove volumes