167 lines
8.5 KiB
Markdown
167 lines
8.5 KiB
Markdown
# broadcast (quinn.cast relay)
|
|
|
|
LLM-controlled remote OBS + SRT contribution relay for running high-bitrate multi-platform streams from terrible hotel WiFi while all the heavy compute and uplink happens on a DigitalOcean droplet.
|
|
|
|
## Architecture (the point of the whole thing)
|
|
|
|
```
|
|
[Hotel laptop / phone]
|
|
ffmpeg or OBS (light settings)
|
|
|
|
|
| SRT (modest bitrate, resilient)
|
|
v
|
|
[DO droplet - strong public bandwidth]
|
|
mediamtx (SRT listener)
|
|
|
|
|
+-- v4l2loopback bridge (ffmpeg) --> /dev/video10
|
|
|
|
|
v
|
|
OBS (headless or containerized, scenes + overlays + browser sources)
|
|
|
|
|
| high-bitrate RTMP (local to mediamtx or direct)
|
|
v
|
|
controller (this Bun app)
|
|
- LLM (Grok-4.3 via xAI tool calling)
|
|
- obs-websocket client
|
|
- dynamic fanout manager (ffmpeg -c copy to N public RTMPs)
|
|
|
|
|
+--> Twitch / YouTube / custom RTMPS etc.
|
|
```
|
|
|
|
You only ever push a few Mbps from the hotel. The droplet does the encoding at broadcast quality and pushes the final high-bitrate streams.
|
|
|
|
## The LLM app + feature structure (the "app" you asked for)
|
|
|
|
`codebase/@features/broadcast/`
|
|
|
|
- `controller/` — the core engine + the self-contained runnable (for droplet / hotel use)
|
|
- `broadcast-controller.ts` — **the core** (BroadcastController) that wires everything and exposes the high-level typed API
|
|
- `obs-client.ts` — pure Bun obs-websocket v5 client (no deps)
|
|
- `fanout-manager.ts` — ffmpeg -c copy children for multi-RTMP
|
|
- `destination-store.ts` — file-backed persistence (simple JSON, atomic, graceful dev)
|
|
- `llm-agent.ts` — Grok-4.3 tool-calling loop (system prompt + full tool set)
|
|
- `index.ts` — thin entry that wires the core + serves the original zero-dep embedded chat UI (Tailwind CDN) + Bun.serve (kept for compatibility / minimal deploys)
|
|
- `frontend-public/` — production Vite + React 19 + @lilith/ui-* SPA (polished extraction of the embedded UI): status dashboard, quick actions, direct scene switcher, destination CRUD manager, LLM chat with tool results, passphrase via query or localStorage. Proxies /api to backend port. `bun run dev` (see its package.json). Runs against backend-api (3034 in dev proxy) or controller.
|
|
- `package.json` + `Dockerfile` (unchanged deploy surface)
|
|
- `backend-api/` — proper Bun/Hono backend-api tier (the public/server-to-server surface)
|
|
- `src/index.ts` — Hono app with full REST direct control + /api/chat + health
|
|
- consumes the controller core via relative imports (no logic duplication)
|
|
- dedicated port (3033), token or passphrase auth, typed responses matching shared/
|
|
- `shared/types.ts` — canonical types (Destination, RelayStatus, Chat*, ApiErrorResponse, etc.)
|
|
- `docs/` + `README.md`
|
|
|
|
Key properties (unchanged from skeleton but now modular and complete):
|
|
- Pure Bun in controller (zero production runtime deps).
|
|
- Single-file server + fully embedded chat UI still works exactly as before for the droplet.
|
|
- Password gate via `?p=...` (change `UI_PASSPHRASE`).
|
|
- Full agent loop + direct obs-websocket v5 + fanout.
|
|
- Destinations persisted (simple JSON file for now; /data/ volume in prod).
|
|
|
|
Run the controller locally (dev / droplet):
|
|
```bash
|
|
XAI_API_KEY=sk-... \
|
|
OBS_WS_URL=ws://127.0.0.1:4455 \
|
|
OBS_WS_PASSWORD=secret \
|
|
UI_PASSPHRASE=dev \
|
|
bun run codebase/@features/broadcast/controller/index.ts
|
|
```
|
|
|
|
Then open http://localhost:8080/?p=dev
|
|
|
|
Run the backend-api (separate process, richer REST surface):
|
|
```bash
|
|
cd codebase/@features/broadcast/backend-api
|
|
bun install # pulls hono
|
|
XAI_API_KEY=... OBS_WS_URL=... OBS_WS_PASSWORD=... UI_PASSPHRASE=... \
|
|
BROADCAST_API_TOKEN=optional-strong-token \
|
|
bun run src/index.ts
|
|
```
|
|
|
|
The backend-api listens on :3033 by default and exposes:
|
|
- GET/POST/DELETE /api/destinations, /api/scenes, /api/broadcast/{start,stop}, /api/text, /api/status, /api/chat etc.
|
|
- Same LLM chat behavior.
|
|
- Intended for later quinn.my / admin integration (SSO/service token will replace the simple gate).
|
|
|
|
## Provisioning the droplet
|
|
|
|
See `scripts/provision-stream-droplet.sh`.
|
|
|
|
Typical flow:
|
|
1. `./scripts/provision-stream-droplet.sh create --name quinn-cast-01 --region nyc2`
|
|
2. SSH the droplet.
|
|
3. Run the `post-boot` script (printed by the provision command or `./scripts/... post-boot`).
|
|
4. Put real secrets in `/opt/stream/.env`.
|
|
5. `cd /opt/stream && docker compose up -d --build`.
|
|
|
|
The post-boot already sets up mediamtx + a bridge example + an OBS container (you may want to customize the OBS image with your scene collection and the multi-RTMP plugin).
|
|
|
|
## Local contribution from hotel (macOS example)
|
|
|
|
Minimal reliable SRT push (adjust device indexes):
|
|
|
|
```bash
|
|
ffmpeg -f avfoundation -framerate 30 -video_size 1280x720 \
|
|
-i "0:0" \
|
|
-c:v libx264 -preset veryfast -tune zerolatency \
|
|
-b:v 3500k -maxrate 4000k -bufsize 8000k -g 60 \
|
|
-c:a aac -b:a 128k -ar 48000 \
|
|
-f mpegts \
|
|
"srt://YOUR_DROPLET_PUBLIC_IP:8890?streamid=publish:live"
|
|
```
|
|
|
|
For better audio device selection use `ffmpeg -f avfoundation -list_devices true -i ""`.
|
|
|
|
You can also run a full local OBS and use a Custom FFMpeg output that targets the same SRT URL.
|
|
|
|
## Destinations & fanout
|
|
|
|
In the chat you can say:
|
|
|
|
- "add twitch rtmp://live.twitch.tv/app/live_1234_5678"
|
|
- "list destinations"
|
|
- "remove youtube"
|
|
- "start broadcast"
|
|
|
|
The controller will remember them (persisted on the droplet volume) and when you start the broadcast it launches parallel ffmpeg copy processes to all of them from the single high-quality encode coming out of OBS.
|
|
|
|
Configure OBS to stream to a local RTMP that the controller knows about (convention in the code: `rtmp://127.0.0.1:1935/live/produced`).
|
|
|
|
## Security notes (follow these)
|
|
|
|
- Change `UI_PASSPHRASE` and `OBS_WS_PASSWORD` immediately.
|
|
- Put the control UI behind Caddy/nginx + real TLS + domain as soon as you have one.
|
|
- For the SRT listener, use a strong `passphrase` in the mediamtx path config + streamid.
|
|
- The droplet firewall should only open the ports you actually need (SRT UDP, the UI port, 22).
|
|
- Never put real stream keys in git.
|
|
|
|
## Platform integration (now partially complete)
|
|
|
|
We have delivered the first slice:
|
|
|
|
- Proper `@features/broadcast` layout: `backend-api/`, `shared/`, modular `controller/` (core engine).
|
|
- Persistence for destinations (simple JSON file, production-grade load/save with graceful fallback).
|
|
- Backend-api (Hono) + controller both expose complete, typed direct REST + LLM chat surfaces.
|
|
- All logic is production complete, strongly typed, no stubs, full error handling, shared types.
|
|
|
|
Deployment (now live):
|
|
|
|
- `deployments/@domains/quinn.cast/` full surface (deploy.sh, nginx, services.yaml, systemd, bootstrap, data/docker-compose + mediamtx/obs/controller wiring).
|
|
- `codebase/@features/broadcast/infra/` (canonical source for the relay stack: docker-compose with mediamtx+video/audio bridges+seeded OBS+controller, bootstrap.sh, OBS Dockerfile with "Hotel Cam" pre-seeded for V4L2/LowerThird/produced RTMP, etc.). Provision script now auto-deploys this. Local test: `./run dev:broadcast:relay`.
|
|
- Deploy on demand to DO tested: `scripts/provision-stream-droplet.sh create --dry-run` (and full flow), `bash -n` on deploy.sh, local infra compose equiv. Use `--name vip-live-xxx` for a show-specific droplet.
|
|
|
|
For VIP platform live shows (vip.transquinnftw.com/shows/live,list as a VIP feature): add/use the "vip-live" destination (rtmp://live.transquinnftw.com/app/<key-from-vip-shows-system>). The fanout from the DO relay delivers the clean high-bitrate feed to the platform ingest so it powers the VIP shows live player/list. Key managed in the VIP shows backend (future auto via quinn.my/service token).
|
|
- Use `./run deploy:cast --from-local` (after provisioning the droplet via `scripts/provision-stream-droplet.sh`).
|
|
- Targets dedicated DO droplet (high bandwidth for SRT ingest + high-bitrate fanout from DO).
|
|
- See the surface's own deploy.sh header + services.yaml for exact usage, health gates, rollback, and secrets.
|
|
|
|
Next (remaining slices):
|
|
|
|
- Polish `frontend-public/` (Vite/React SPA with the components/hooks already started: SceneSwitcher, StatusDashboard, ChatPanel, DestinationManager, ToolResultDisplay, useBroadcastAuth).
|
|
- Wire destinations/"go live" sessions into quinn.my provider profiles (service token to backend-api).
|
|
- Persist per-performer RTMP templates + scene presets in quinn.my-owned data.
|
|
- Add recording of clean program feed + VOD clipping.
|
|
- This matches the "RTMP relay for simulcast" promise in the performer marketing copy.
|
|
|
|
See docs/RUNBOOK.md for the complete end-to-end steps, OBS setup notes, troubleshooting, and self-verification checklist.
|
|
|
|
We ship working systems.
|