Simple Twitch Relay Server with Fallback Disconnection Screen
  • TypeScript 80.5%
  • Vue 14%
  • CSS 3.5%
  • Dockerfile 1.4%
  • HTML 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jannik Reimers cb1fb4cdae
All checks were successful
CI / build-and-check (push) Successful in 16s
CI / docker-publish (push) Successful in 33s
fix README.md
2026-07-23 23:43:00 +02:00
.forgejo/workflows Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
.vscode Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
public Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
server Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
shared Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
src Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
.dockerignore Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
.env.example Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
.gitignore Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
docker-compose.yml Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
Dockerfile Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
env.d.ts Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
eslint.config.ts Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
index.html Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
package-lock.json Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
package.json Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
README.md fix README.md 2026-07-23 23:43:00 +02:00
tsconfig.app.json Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
tsconfig.json Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
tsconfig.node.json Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00
vite.config.ts Twitch relay: OBS RTMP ingest, Twitch relay/fallback, Vue dashboard 2026-07-23 23:39:18 +02:00

Twitch Relay

A self-hosted Twitch relay with a dashboard: OBS pushes RTMP into this server, which relays the stream on to Twitch over a single persistent connection. If OBS ever disconnects while armed, the server automatically switches Twitch over to a synthesized "Stream Disconnected" placeholder scene, and switches back the instant OBS reconnects — Twitch never sees the channel go dark or the connection drop, just the picture changing. The dashboard shows live status, a realtime preview of whatever's currently going out, and a single Start/Stop control.

Designed to run on a Tailscale tailnet with no dashboard login — the tailnet membership is the auth.

How it fits together

OBS  --RTMP-->  this server  --RTMP-->  Twitch
                     |
                     v
              Vue dashboard (status + live preview + Start/Stop)
  • Start arms the relay: nothing is pushed to Twitch until you click it.
  • While armed, OBS connected → your real stream goes out; OBS disconnected → the placeholder goes out instead, automatically, after a short debounce.
  • Stop disarms the relay and stops pushing anything to Twitch.

Prerequisites

  • A Twitch stream key (Creator Dashboard → Settings → Stream). Keep it secret — it's the only credential this project needs.
  • Docker and Docker Compose, for the recommended setup.
  • A network path from wherever OBS runs to this server's RTMP port (1935 by default) — typically Tailscale, since there's no dashboard login and RTMP itself isn't encrypted.

Quick start (Docker)

  1. Clone this repo onto the machine that will run the relay (it needs to be reachable from wherever OBS runs).
  2. Copy the env file and fill in your stream key:
    cp .env.example .env
    
    Open .env and set TWITCH_STREAM_KEY. Everything else in there has a working default — see Environment variables below if you want to change ports or other behavior.
  3. Build and start it:
    docker compose up --build -d
    
  4. Open the dashboard at http://<host>:8080 (use the Tailscale hostname or IP for <host>). You should see it reachable but not yet armed — nothing has been pushed to Twitch yet.
  5. Point OBS at the ingest (see OBS setup below) and start streaming in OBS. The dashboard's "OBS" indicator should flip to connected, and the live preview should show your feed.
  6. Click Start on the dashboard. Twitch should go live within a couple seconds.
  7. To confirm the failover works: stop streaming in OBS (or just kill its network connection) — within a few seconds the dashboard should flip to "fallback" and Twitch should show the "Stream Disconnected" placeholder instead of cutting out. Reconnecting OBS should switch it back automatically.
  8. Click Stop when you're done — this stops pushing anything to Twitch until you arm again.

To update to a newer version later: git pull, then docker compose up --build -d again.

To watch logs: docker compose logs -f. The dashboard also has its own /api/logs (rendered in the UI) with the last ~200 lines from the relay/fallback/broadcaster ffmpeg processes, which is usually more useful than the container logs for anything ffmpeg-related.

OBS setup

In OBS's Stream settings, choose Custom... as the service and fill in:

OBS field Value
Server rtmp://<tailnet-hostname-or-ip>:1935/live
Stream Key whatever RTMP_STREAM_KEY is set to (default: stream)

Both values are also shown directly on the dashboard (composed from the page's own hostname), with a copy button next to each — use those rather than typing them by hand if you're not sure what <tailnet-hostname-or-ip> should be.

These are two separate OBS fields — don't paste the whole ingest URL into the Stream Key field or concatenate them.

Set OBS's own output (resolution/framerate/bitrate) to whatever you actually want on Twitch: the real relay path is a pure -c copy remux with no transcoding, so OBS's settings are the broadcast quality. (The synthesized fallback placeholder is currently tuned for 1080p60 — see ENCODE_ARGS in server/ffmpeg/commands.ts if your OBS output differs and you want the fallback to visually match.)

Environment variables

See .env.example for the full list, each with an explanatory comment. The only one you must set is TWITCH_STREAM_KEY — everything else has a sane default:

Variable Default Purpose
TWITCH_STREAM_KEY (none) Required to arm. Never sent to the frontend in full.
RTMP_PORT 1935 Port OBS publishes into.
RTMP_APP / RTMP_STREAM_KEY live / stream Form the local ingest URL's app/key path.
HTTP_PORT 8080 Dashboard + REST API + WebSocket, all on one port.
FALLBACK_VIDEO_PATH (none) Optional custom video to loop instead of the synthesized scene.
RECONNECT_DEBOUNCE_MS 3000 How long OBS must stay disconnected before switching to fallback.
OBS_IDLE_TIMEOUT_MS 10000 How long a silent (no-FIN) OBS connection is tolerated before it's treated as dead.
CRASH_LOOP_MAX / CRASH_LOOP_WINDOW_MS 5 / 60000 Auto-disarm guard if ffmpeg keeps crashing.

Changing any of these requires restarting the container (docker compose up --build -d again; no rebuild needed unless you changed code, just a restart) — they're read once at process start.

Local development

npm install
npm run dev:all   # frontend (Vite, :5173, proxies /api and /ws) + backend (:8080) together

Open http://localhost:5173. Requires ffmpeg on your PATH locally if you want to actually exercise arming/relaying outside Docker — without it, the dashboard still loads but arming will fail.

Other scripts:

  • npm run build — type-check, then build both frontend (dist/) and backend (dist-server/).
  • npm run type-checkvue-tsc for the frontend + tsc --noEmit for the backend.
  • npm run lint — ESLint, autofixing what it can.
  • npm start — run the already-built backend (dist-server/server/index.js), serving the built frontend statically. Used by the Docker image; rarely needed directly in dev.

Troubleshooting

  • Dashboard loads but "Start" is disabled / arming fails immediatelyTWITCH_STREAM_KEY isn't set. Check .env (Docker) or your shell environment (local dev), then restart.
  • OBS won't connect — double check the Server/Stream Key split above (a common mistake is swapping or concatenating them), that 1935/tcp is actually reachable from OBS's machine (e.g. both are on the tailnet), and that nothing else on the host is already bound to that port.
  • Dashboard shows OBS connected but nothing happens on Twitch — make sure you actually clicked Start; connecting OBS alone doesn't push anything to Twitch until the relay is armed.
  • It fell back to the placeholder and won't come back — check /api/logs in the dashboard first. A crash-looping ffmpeg auto-disarms after a few failures (CRASH_LOOP_MAX) rather than retrying forever — a bad/expired TWITCH_STREAM_KEY is a common cause, since Twitch rejects the connection immediately.
  • docker compose up fails to bind ports — something else on the host already owns 1935 or 8080; change RTMP_PORT/HTTP_PORT in .env, or stop the other process.

Deployment notes

  • No dashboard auth by design — the trust boundary is Tailscale network membership, not a login. Don't expose the dashboard port on a network you don't trust; see the commented-out Tailscale sidecar block in docker-compose.yml if you want the container itself on the tailnet rather than relying on the host already being on it.
  • CI is Forgejo Actions (.forgejo/workflows/ci.yml) — lints, type-checks, builds, and (on main) publishes a Docker image. Needs instance-specific runner labels and registry credentials filled in (vars.REGISTRY, secrets.REGISTRY_USER/REGISTRY_TOKEN, and a runs-on: label matching a runner configured on your Forgejo instance).