Self-hosted web dashboard that manages srt_compositor processes and pushes them to Twitch RTMP.
OBS/vMix ──► SRT ──► srt_compositor ──► RTMP ──► Twitch
│
background.mp4 (loops while SRT is down)
Sign in with Twitch → stream key is fetched automatically → configure and go live.
Each stream gets a dedicated SRT listener port (UDP). Point your encoder at srt://<host>:<port>?mode=caller. The compositor:
- Shows your SRT feed when connected
- Switches to a looping background MP4 when the SRT feed drops
- Switches back automatically on reconnect
- Pushes the result to Twitch via RTMP
The web dashboard lets you create/manage streams, upload background videos, configure encoding settings, and start/stop compositing — all without touching the command line.
Works on any architecture — arm64 (Raspberry Pi 4/5), amd64, etc. Both the C compositor and the better-sqlite3 native addon compile from source inside the build, so the image is always native to whatever machine runs docker build. No --platform flag needed.
Go to dev.twitch.tv/console/apps → Register Your Application and add:
http://<your-host>:3000/api/auth/callback/twitch
as an OAuth Redirect URL.
cp .env.example .env
# fill in TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET, NEXTAUTH_SECRET, NEXTAUTH_URLThe path variables (COMPOSITOR_BINARY, UPLOADS_DIR, DATA_DIR) are pre-filled with the correct container paths — leave them as-is.
docker compose build
docker compose up -d
docker compose logs -fApp is at http://<host>:3000. The SQLite database is created automatically on first start at ./data/reestreamer.db.
git pull
docker compose build
docker compose up -ddocker buildx build --platform linux/arm64 -t ree:latest --load .| Port | Protocol | Purpose |
|---|---|---|
| 3000 | TCP | Web UI |
| 6000–6099 | UDP | SRT listener pool (one per active stream) |
SRT ports must be reachable from your encoder. Open them in your firewall/router.
| Host path | Container path | Purpose |
|---|---|---|
./data/ |
/app/data |
SQLite database |
./uploads/ |
/app/uploads |
Uploaded background videos |
- Linux (any architecture)
- Node.js 22+ and pnpm
- FFmpeg dev libraries with SRT support:
sudo apt install build-essential pkg-config \
libavformat-dev libavcodec-dev libavutil-dev \
libswscale-dev libswresample-devVerify SRT support: ffmpeg -protocols 2>/dev/null | grep srt
cd compositor
gcc -Wall -Wextra -O2 -std=c11 -D_GNU_SOURCE \
$(pkg-config --cflags libavformat libavcodec libavutil libswscale libswresample) \
-o srt_compositor srt_compositor.c \
$(pkg-config --libs libavformat libavcodec libavutil libswscale libswresample) \
-lpthread -lmGo to dev.twitch.tv/console/apps → Register Your Application:
| Field | Value |
|---|---|
| Name | anything |
| OAuth Redirect URLs | http://localhost:3000/api/auth/callback/twitch (dev) |
| Category | Broadcasting Suite |
Copy the Client ID and generate a Client Secret.
cp .env.example apps/web/.env.localEdit apps/web/.env.local:
TWITCH_CLIENT_ID=<your client id>
TWITCH_CLIENT_SECRET=<your client secret>
NEXTAUTH_SECRET=<run: openssl rand -base64 32>
NEXTAUTH_URL=http://localhost:3000
COMPOSITOR_BINARY=/absolute/path/to/compositor/srt_compositor
UPLOADS_DIR=/absolute/path/to/uploads
DATA_DIR=/absolute/path/to/datapnpm install
better-sqlite3compiles a native addon — requiresbuild-essential/python3.
./start-dev.shOpen http://localhost:3000.
cd apps/web
pnpm build
pnpm startcd apps/web
pnpm buildIn apps/web/.env.local:
NEXTAUTH_URL=https://your-domain.comAlso add https://your-domain.com/api/auth/callback/twitch to your Twitch app's OAuth redirect URLs.
Create /etc/systemd/system/ree.service:
[Unit]
Description=ree compositor dashboard
After=network.target
[Service]
Type=simple
User=compositor
WorkingDirectory=/home/compositor/apps/web
ExecStart=/usr/bin/node .next/standalone/server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/home/compositor/apps/web/.env.local
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now ree
sudo systemctl status ree
# Live logs:
sudo journalctl -u ree -fCaddy handles HTTPS automatically via Let's Encrypt.
sudo apt install caddy/etc/caddy/Caddyfile:
your-domain.com {
reverse_proxy localhost:3000
}
sudo systemctl reload caddysudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com/etc/nginx/sites-available/ree:
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}sudo ln -s /etc/nginx/sites-available/ree /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxHAProxy is a good choice if you're already using it for other services or need fine-grained TCP control.
sudo apt install haproxy certbot
# Obtain cert first (haproxy needs a combined PEM)
sudo certbot certonly --standalone -d your-domain.com
sudo cat /etc/letsencrypt/live/your-domain.com/fullchain.pem \
/etc/letsencrypt/live/your-domain.com/privkey.pem \
> /etc/haproxy/certs/your-domain.com.pem/etc/haproxy/haproxy.cfg — append:
frontend ree_https
bind *:443 ssl crt /etc/haproxy/certs/your-domain.com.pem
bind *:80
redirect scheme https if !{ ssl_fc }
default_backend ree_app
backend ree_app
server ree 127.0.0.1:3000 check
sudo systemctl reload haproxyRenewing certs: add a deploy hook to regenerate the combined PEM and reload haproxy.
lighttpd is lightweight and well-suited for low-resource machines like a Raspberry Pi.
sudo apt install lighttpdEnable the required modules:
sudo lighttpd-enable-mod proxy
sudo lighttpd-enable-mod setenvCreate /etc/lighttpd/conf-enabled/90-ree.conf:
$HTTP["host"] == "your-domain.com" {
$SERVER["socket"] == ":443" {
ssl.engine = "enable"
ssl.pemfile = "/etc/letsencrypt/live/your-domain.com/combined.pem"
ssl.ca-file = "/etc/letsencrypt/live/your-domain.com/chain.pem"
}
proxy.server = ( "" => (
( "host" => "127.0.0.1", "port" => 3000 )
))
proxy.header = (
"map-urlpath" => ( "/" => "/" ),
"https-remap" => "enable",
"upgrade" => "enable"
)
setenv.add-request-header = (
"X-Forwarded-Proto" => "https",
"X-Real-IP" => "%{REMOTE_ADDR}e"
)
}
# HTTP → HTTPS redirect
$HTTP["scheme"] == "http" {
$HTTP["host"] == "your-domain.com" {
url.redirect = ( "" => "https://your-domain.com${url.path}${qsa}" )
}
}
Combined PEM for lighttpd: lighttpd expects the cert + key in a single file:
sudo cat /etc/letsencrypt/live/your-domain.com/fullchain.pem \ /etc/letsencrypt/live/your-domain.com/privkey.pem \ > /etc/letsencrypt/live/your-domain.com/combined.pem
WebSocket support: The
"upgrade" => "enable"line inproxy.headerrequires lighttpd 1.4.46+. Check withlighttpd -v. Debian Bookworm ships 1.4.69+, so this should work out of the box.
sudo lighttpd -t -f /etc/lighttpd/lighttpd.conf # test config
sudo systemctl restart lighttpdCloudflare sits in front of your reverse proxy and gives you DDoS protection, free TLS, and a CDN — but requires a couple of settings to work correctly with ree.
| Name | Type | Content | Proxy |
|---|---|---|---|
ree.domain.com |
A | your server IP | Proxied (orange cloud) |
srt.domain.com |
A | your server IP | DNS only (grey cloud) |
SRT must bypass Cloudflare. Cloudflare's proxy is HTTP-only; it cannot forward UDP. Point your encoders at srt.domain.com (or the raw IP) instead of ree.domain.com.
| Setting | Value |
|---|---|
| Mode | Full (strict) |
| Always Use HTTPS | On |
| Minimum TLS Version | TLS 1.2 |
Full (strict) means Cloudflare validates your origin cert. Use either a free Cloudflare Origin CA certificate (15-year validity, no renewal needed) or a Let's Encrypt cert on the origin.
In Cloudflare dashboard → SSL/TLS → Origin Server → Create Certificate. Download the cert and key, then:
Caddy — Caddy handles this automatically when proxied through Cloudflare with Full (strict); no changes needed if you already have a real cert.
nginx:
ssl_certificate /etc/ssl/cloudflare-origin.pem;
ssl_certificate_key /etc/ssl/cloudflare-origin.key;HAProxy — combine into a single PEM:
cat cloudflare-origin.pem cloudflare-origin.key > /etc/haproxy/certs/ree.pem
sudo systemctl reload haproxy| Setting | Value |
|---|---|
| WebSockets | On |
| gRPC | Off (not used) |
WebSockets must be on for tRPC subscriptions and hot-reload in dev.
| Setting | Value |
|---|---|
| Cache Level | Standard |
| Browser Cache TTL | Respect Existing Headers |
Next.js sets its own Cache-Control headers; Cloudflare will honour them. No page rules needed.
Prevents anyone from hitting your server directly, bypassing Cloudflare.
# Allow only Cloudflare IP ranges + your own access
sudo ufw allow from 173.245.48.0/20 to any port 443 proto tcp
sudo ufw allow from 103.21.244.0/22 to any port 443 proto tcp
sudo ufw allow from 103.22.200.0/22 to any port 443 proto tcp
sudo ufw allow from 103.31.4.0/22 to any port 443 proto tcp
sudo ufw allow from 141.101.64.0/18 to any port 443 proto tcp
sudo ufw allow from 108.162.192.0/18 to any port 443 proto tcp
sudo ufw allow from 190.93.240.0/20 to any port 443 proto tcp
sudo ufw allow from 188.114.96.0/20 to any port 443 proto tcp
sudo ufw allow from 197.234.240.0/22 to any port 443 proto tcp
sudo ufw allow from 198.41.128.0/17 to any port 443 proto tcp
sudo ufw allow from 162.158.0.0/15 to any port 443 proto tcp
sudo ufw allow from 104.16.0.0/13 to any port 443 proto tcp
sudo ufw allow from 104.24.0.0/14 to any port 443 proto tcp
sudo ufw allow from 172.64.0.0/13 to any port 443 proto tcp
sudo ufw allow from 131.0.72.0/22 to any port 443 proto tcp
sudo ufw deny 443/tcp # block everything elseKeep the current Cloudflare IP list at cloudflare.com/ips.
# Web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# SRT listener ports (UDP) — must be reachable from your encoder directly (not via Cloudflare)
sudo ufw allow 6000:6099/udpIf you're behind a router, forward the same UDP port range to your server.
Twitch OAuth requires a publicly reachable callback URL — localhost won't work unless you expose it. Pick any of the options below.
# Install (Debian/Ubuntu)
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
# Start a tunnel to port 3000
cloudflared tunnel --url http://localhost:3000Cloudflared prints a random https://*.trycloudflare.com URL. Use that as your base.
# Install: https://ngrok.com/download
ngrok http 3000Ngrok prints a https://<random>.ngrok-free.app URL.
# On your local machine — forwards VPS port 3000 → your local 3000
ssh -R 3000:localhost:3000 user@your-vps-ipThen either access via http://your-vps-ip:3000 or put nginx in front for HTTPS.
1. Add the callback URL to your Twitch app (dev.twitch.tv/console/apps):
https://<your-tunnel-url>/api/auth/callback/twitch
2. Update apps/web/.env.local:
NEXTAUTH_URL=https://<your-tunnel-url>3. Restart the dev server — ./start-dev.sh
Tip: cloudflared and ngrok give a new random URL each run. For a stable URL during development, use ngrok's paid plan, a reserved Cloudflare tunnel, or a VPS.
Sign in with Twitch OAuth. The app automatically:
- Creates your user account
- Fetches your Twitch stream key via the API (
channel:read:stream_keyscope) - Pre-populates stream key on all your streams
Re-signing in refreshes the stream key if Twitch ever rotates it.
apps/web/ Next.js 15 (App Router)
├── app/ Pages and API routes
├── components/ shadcn/ui components
└── lib/
├── auth.ts next-auth v4, Twitch OAuth, JWT sessions
├── db/ Drizzle ORM + SQLite (better-sqlite3)
│ ├── schema.ts users, sessions, streams, uploads
│ └── index.ts DB init + inline migrations
├── trpc/ tRPC v11 routers (streams, uploads)
└── stream-manager/ Node.js process manager for compositor
compositor/
└── srt_compositor.c C binary — SRT → background compositor → RTMP
srt_compositor --config <config.json>
Config JSON fields: srt_url, bg_file, stream_id, out_width, out_height, out_fps, video_bitrate, audio_bitrate, sample_rate, bg_unmute_delay.
Events emitted on stderr as JSON: started, bg_opened, srt_connected, srt_dropped, srt_active, output_ready, running, stats, stopped, done, error.
Default: ports 6000–6099 (100 concurrent streams). Configure via SRT_PORT_MIN / SRT_PORT_MAX.
SQLite at $DATA_DIR/reestreamer.db. Schema is created/migrated automatically on startup — no migration CLI needed.
| Layer | Technology |
|---|---|
| Frontend | Next.js 15, React 19, Tailwind CSS, shadcn/ui |
| API | tRPC v11, TanStack Query v5 |
| Auth | next-auth v4, Twitch OAuth (JWT sessions) |
| Database | SQLite + Drizzle ORM (better-sqlite3) |
| Runtime | Node.js 22 |
| Compositor | C (FFmpeg libav*, libsrt) |