Shared low-latency radio. One stream, many listeners.
Everyone hears the same thing at the same time. Submit a YouTube link, it gets queued, and when it plays, everyone listening hears it together.
┌─────────────────────────────────────────────────────────────────────────┐
│ chanson.live │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ User submits YouTube URL │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ yt-dlp │───▶│ ffmpeg │───▶│ mediasoup │ │
│ │ (download) │ │ (mp3→opus) │ │ (WebRTC SFU) │ │
│ └─────────────┘ └──────────────┘ └─────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Listeners │ │
│ │ (synchronized) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
- Request: User submits a YouTube URL via the web interface
- Download:
yt-dlpfetches the audio (cached for future plays) - Transcode:
ffmpegconverts to Opus and streams via RTP - Distribute: mediasoup (WebRTC SFU) fans out to all connected clients
- Listen: Everyone hears the same audio at the same time
| Component | Purpose |
|---|---|
| Bun | Runtime, HTTP server, WebSocket, SQLite |
| mediasoup | WebRTC Selective Forwarding Unit |
| yt-dlp | YouTube audio extraction |
| ffmpeg | Audio transcoding (MP3 → Opus over RTP) |
# Prerequisites: ffmpeg, yt-dlp
bun install
bun run dev
# Open http://localhost:3000docker compose build
docker compose upUses network_mode: host for UDP port range (required for WebRTC).
YouTube may block downloads from VPS IP ranges. The external provider architecture solves this by running the downloader on a separate machine (e.g., your home network) while the VPS handles only streaming.
┌────────────────────┐ WebSocket ┌────────────────────┐
│ VPS (Server) │◀──────────────────────────▶│ Home (Provider) │
│ │ │ │
│ • Serves clients │ ◀─ track requests ── │ • Runs yt-dlp │
│ • WebRTC stream │ ── audio upload ──▶ │ • Sends audio │
│ • Queue/history │ │ • Avoids blocks │
└────────────────────┘ └────────────────────┘
PROVIDER_MODE=external
PROVIDER_TOKEN=your_secure_token
ADMIN_TOKEN=your_secure_tokenBROADCASTER_URL=https://your-vps-domain
PROVIDER_TOKEN=your_secure_token
PROVIDER_DOWNLOAD_DIR=./provider-downloads
AUDIO_QUALITY=5bun run providerThe provider connects to the VPS via WebSocket, receives download requests, fetches audio via yt-dlp, and uploads the file back over the same connection.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP server port |
PROVIDER_MODE |
local |
local or external |
PROVIDER_TOKEN |
n/a | Shared secret for provider auth |
ADMIN_TOKEN |
n/a | Admin UI auth (defaults to PROVIDER_TOKEN) |
MEDIASOUP_ANNOUNCED_IP |
auto | Public IP for WebRTC (required for production) |
MEDIASOUP_LISTEN_IP |
0.0.0.0 |
Bind address |
RTC_MIN_PORT |
10000 |
WebRTC UDP port range start |
RTC_MAX_PORT |
20000 |
WebRTC UDP port range end |
STUN_URLS |
Google STUN | ICE server URLs |
AUDIO_QUALITY |
5 |
yt-dlp quality (1-10, lower = better) |
CACHE_MAX_BYTES |
5368709120 |
Max cache size (5GB) |
DOWNLOAD_DIR |
./downloads |
Audio cache directory |
/adminusesADMIN_TOKEN(falls back toPROVIDER_TOKENif unset).- Blacklisted tracks cannot be queued or played in fallback rotation.
Unlike per-user streaming, chanson.live uses a single mediasoup producer that all clients subscribe to. One ffmpeg process streams to one producer, and mediasoup efficiently fans out to hundreds of consumers.
Downloaded audio is cached locally. When cache exceeds CACHE_MAX_BYTES, the oldest files are pruned, but files currently in queue or playing are protected from deletion.
When the queue is empty, the player shuffles through cached tracks automatically, keeping the radio alive.
The server monitors the RTP score every 5 seconds. If ffmpeg stops sending audio (score drops to 0), the producer is closed automatically to prevent zombie streams.
External providers upload audio as raw binary chunks over WebSocket (not base64), minimizing overhead for large files.
# E2E test with a specific video
bun run test:e2e "https://www.youtube.com/watch?v=..."
# Test page
open http://localhost:3000/testMEDIASOUP_ANNOUNCED_IPmust be your VPS public IP for clients behind NAT to connect- STUN helps NAT discovery but doesn't relay media; some restrictive networks may need TURN
- SQLite runs in WAL mode for better concurrent read performance