9.9 KiB
9.9 KiB
Vanguard stack — what runs, what people do, what's unfinished
Living document. Updated whenever something is deployed or changes behaviour. Last update: 2026-10-31.
Host: ovh1.dumpstervigilantes.rip (OVH SYS-1, Debian 13). Bundle: ~/vanguard-stream on the box.
Source of truth: this repo, techy/Media-Server-Setup on the stack's own Gitea (ssh://git@git.dumpstervigilantes.rip:2222/techy/Media-Server-Setup.git).
Runs on its own
| Cadence | What happens | Done by |
|---|---|---|
| Continuous | OBS streams record into a hidden streams/<user>/<Title>/.live/ folder, invisible to Jellyfin and copyparty until finished |
MediaMTX |
| Continuous | Gamers' clip folders mirror onto the server (video files only, their structure untouched, receive-only) | Syncthing |
| Continuous | New recordings and clips are announced in a Discord channel with the streamer's name and a frame grab | Jellyfin plugin (needs bot token + channel in its config) |
| Every 5 min | Finished files are checked. Streams idle ≥3 min: lossless remux out of <Title>/.live/ to <user>/<Title>__<ts>.mp4, one file per recording (this is when they appear in Jellyfin/copyparty and the Discord notice fires). Non-compliant clips: converted to the editing standard, raw removed, one copy per clip |
normalize sweep (systemd timer) |
| Every 5 min | Discord scheduled events snapshotted into a history so recordings can be matched to their session | Jellyfin plugin |
| Every 30 min + startup | Every non-admin Jellyfin user set to direct-play-only (no hardware encoder on this box) | Jellyfin plugin |
| Daily 04:10 ET | Recordings roll between weekday → week → month → year seasons as the calendar moves; no files change | Jellyfin plugin |
| Daily 05:00 ET | Dumped nights older than 3 days deleted for good | trash purge (systemd timer) |
| On boot | Everything restarts itself after the media mount is up; interrupted recordings stay playable | systemd + Docker |
| Continuous | Active streams are watchable live (WebRTC, HLS fallback) by Discord-server members; optional "is live" Discord post when a stream starts | MediaMTX + live portal (page deployed; MediaMTX port fix pending apply) |
| Not yet running | Pull of files older than 15 days to the home HDD | home/rclone-archive.sh, needs the home box |
| Not yet running | Nightly backup of the Authentik database (Postgres) | planned |
What people do
| Who | Activity | Where | Login |
|---|---|---|---|
| Streamer | Type a title, Go Live, Stop. "Dump tonight" if it was junk (3-day undo) | OBS browser dock https://ovh1…/dock/ |
stream password in dock settings |
| Gamer | Nothing; clips save where they always did | their clipper + Syncthing, paired once | — |
| Editor | Browse by streamer/title, scrub in browser, download originals, upload finals | copyparty https://ovh1…/ |
local account (to move to Discord) |
| Editor | Mount the library as a drive | WebDAV, same URL | same |
| Anyone in the Discord server | Watch recordings and clips by streamer and season | Jellyfin https://watch.…/ |
Discord (Authentik) |
| Anyone in the Discord server | Watch live: all active streams in dockable windows, audio follows focus, per-window mute/volume | Live portal https://live.…/ |
Discord (Authentik, domain-level) — live; viewer side not yet exercised during a stream |
| Streamer (Windows) | Go Live from the browser: pick the WarDogs window (+audio on Windows Chrome 141+; Linux/Mac video only), streams as <user>/HelmetCam-<date>; no OBS needed |
Live portal → Go Live | Discord; key fetched for the logged-in user — verified end to end (publish → .live → sweep → title folder) |
| Devs | Host code, review, CI | Gitea https://git.…/, ssh 2222 |
local account (Discord later) |
| Bot dev | Push code, restart container, read logs; web UI at https://bot.…/ |
Gitea + compose; bot/AGENT.md |
web UI behind Discord (Authentik), once its provider exists |
| Admin | Add/remove streamers & editors, hostnames, bot token | ./configure.py |
ssh |
| Admin | Pair a gamer's device, approve their folder | Syncthing UI https://ovh1…/sync/ |
Syncthing GUI password |
| Admin | Groups, who may log in, app access | Authentik https://auth.…/ |
Discord or akadmin |
| Admin | Install/upgrade the Jellyfin plugin | ./installJellyfinPlugin.sh |
ssh |
| Admin | Register the CI runner | ./registerGiteaRunner.sh |
ssh |
Services and their state
| Service | Role | State |
|---|---|---|
| MediaMTX | RTMP/SRT ingest, records fMP4 | live |
| copyparty | browse/scrub/download/WebDAV/finals upload | live, local accounts |
| dumpd + dock | dump-tonight endpoint, OBS dock page | live, own password file |
| Syncthing | clip intake, UI at /sync/ |
live, Syncthing GUI login |
| normalize sweep | editing standard, one copy per clip | live |
| nftables | host firewall (22, 80/443, 1935, 2222, 3306, 8890/udp, 22000) | live |
| Caddy | TLS + routing for all hostnames | live |
| Authentik (+Postgres, Redis) | Discord login, groups, guild-membership policy | live |
| Jellyfin | watch page, SSO via Authentik, direct play only | live |
| Vanguard Metadata plugin | seasons by tag, Discord names/avatars/events, feed, playback policy | 0.1.3 built (flat <Title>__<ts> names, policy task, feed wording) — install pending; server has 0.1.0 or 0.1.2 |
| MariaDB | databases for the bot and Gitea; public :3306, root local-only | live |
| Gitea | git hosting on MariaDB, Actions enabled; holds this repo | live, local admin |
| act_runner | CI jobs in Docker | not registered |
| discord-bot | Python runtime, idles until bot/app/main.py exists |
live, no code yet |
bot. host |
bot web UI behind Authentik forward-auth; Caddy requires group bot-users (from a Discord role) |
Caddy block live; bot CNAME still points at the wrong IP; roles→groups policy pending |
Live portal (live/, live. host) |
React + dockview multi-stream viewer + browser Go Live (WHIP); MediaMTX HLS/WebRTC/API on loopback; dumpd /me/stream-key |
live; Go Live verified |
Unfinished, in rough priority
- MediaMTX container recreate for
TZ=America/New_York(filenames were UTC-stamped) andmoq: no—docker compose up -d --force-recreate mediamtxafter the compose upload. 1b. Authentikdiscord-roles-to-groupspolicy +bot-usersgroup (botCNAME now correct). 1c. Confirm plugin 0.1.2 installed and runner registered. 1d. Watch a real stream from a second browser: corner chip should saywebrtc; confirm WarDogs audio came through. - Register the runner (
./registerGiteaRunner.sh). botCNAME + Authentik proxy provider — first use of forward-auth; proves the pattern for copyparty.- copyparty behind Authentik (group permissions, WebDAV via app passwords, then drop local accounts). The one change that can lock editors out; keep a local admin until a Discord editor has used the new path.
- Syncthing UI + dump behind Authentik; dump reads the identity header; dock fetches the caller's own stream key.
- Gitea login via Authentik (OIDC source, auto-register,
admins→ Gitea admin). - Housekeeping: pin
AUTHENTIK_TAGin.env, rotate the Discord client secret that was in a screenshot, nightly Postgres dump, Jellyfin login branding (Discord button first), check a non-admin Discord user's Jellyfin view. - Medal filename pattern in the plugin's timestamp parser — need one sample filename.
- Home archive puller on the home box.
- Plugin repository served by Caddy so Jellyfin updates the plugin from its catalog.
- Clip submissions (Medal/Twitch/YouTube links, Discord uploads) — belongs to the bot.
- MediaMTX auth through Authentik (plan phase 11): no stream keys; Go Live uses the Authentik JWT, OBS uses an Authentik app password; "streamer" = group membership from a Discord role.
- SYS-3 when transcoding or sweep speed hurts: Quick Sync, flip the plugin's policy toggle off.
Verified behaviours worth remembering
- Never scp generated configs (
mediamtx.yml,copyparty.conf,Caddyfile,dump-users.txt,.env): a template copy wiped all streamers once. They are gitignored now;*.example.*are reference only. - Caddy runs in a container: anything it serves from disk must be a mount (
/srv/livewas missing → 404). - Go Live quality depends on the browser's encoder: Windows Chrome uses the GPU (fine); Linux Chromium on NVIDIA is software H.264 Baseline (soft at any bitrate, CPU-limited at 1440p60) and cannot be fixed by flags — Linux streamers use OBS. The stats strip shows encoder + profile.
- Several videos inside one non-season folder = ONE Jellyfin episode with "versions". Streams are therefore published flat as
<Title>__<ts>.mp4in the streamer's folder (plugin 0.1.3 parses the title from the name). Four or five helmet cams a night each get their own episode. - The sweep's VFR check used a gawk-only function; Debian's mawk made it pass everything silently. Portable now.
- A deploy block that is only half-run leaves old binaries in
/usr/local/bin;grep -cthe installed script before blaming logic. - MediaMTX stamps recording filenames in its container clock: without
TZit is UTC and the plugin reads names as Eastern (4 h off).TZ=America/New_Yorkis set on the container. - MediaMTX 1.21 refuses to start without
webrtcAdditionalHostsorwebrtcIPsFromInterfaces; configure.py writes the public host in. - Jellyfin honours provider-supplied season numbers on a Shows library (day seasons appeared). Next check: Monday 04:10 ET should fold them into a week season.
- A stream indexed by Jellyfin while still recording has no duration and skips to the next episode on seek — fixed by recording into
.live/so nothing sees a file until it is finished and flat. - Sweep leaves compliant Syncthing files untouched and keeps exactly one copy of converted ones (tested).
- Firewall: Docker-published ports bypass the input chain; MariaDB uses host networking precisely so the rules apply.
- Never
systemctl stop|restart nftables(flushes Docker's NAT). Reload only.