Files
Media-Server-Setup/STATUS.md

11 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 (evening). 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: timecode band decoded (server clock) or arrival time used, lossless remux out of <Title>/.live/ to <user>/<Title>__<ts>.mp4 with a frame-accurate tmcd track + sidecar, 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 One-time: dock → Timecode band → "Add band + clock source" (OBS) or leave the Go Live checkbox on — gives editors frame-accurate timecode OBS dock / Go Live — pending deploy
Streamer Type a title (live preview + naming rules), Go Live, Stop. "Dump tonight" if it was junk (3-day undo). Manual-setup accordion with endpoints and a hidden token OBS browser dock https://ovh1…/dock/ Discord login in the dock; dock mints a 12 h token per Go Live — no passwords, verified live
Gamer Nothing; clips save where they always did their clipper + Syncthing, paired once —
Editor Multicam: drop clips on a timeline, sync by the embedded timecode track (frame-accurate when the band was on), cross-check with voice audio; export at the game resolution to drop the band Resolve / Premiere —
Editor Browse by streamer/title, scrub in browser, download originals (zip/tar for folders), upload finals copyparty https://ovh1…/ Discord (Authentik); rights by group editors / media-admins
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, shows a real 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 Hostnames, bot token/guild, webhook; streamers/editors are Discord roles → Authentik groups ./configure.py ssh
Admin Pair a gamer's device, approve their folder Syncthing UI https://ovh1…/sync/ Discord + group media-admins, then Syncthing's 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; publish auth delegated to dumpd (tokens via /mtx-auth) live, token auth verified
copyparty browse/scrub/download/finals upload behind Authentik, group permissions (editors, media-admins), Discord-only, no WebDAV
dumpd + dock dump, stream tokens, MediaMTX auth hook, /time; OBS dock with timecode-band setup identity from Authentik headers
Timecode bin/vgstrip.py decoder, tmcd + sidecar in the sweep, /dock/clock/ strip page, Go Live band composite, portal crop server clock is the reference
Syncthing clip intake, UI at /sync/ UI behind Authentik + media-admins
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

  1. Deploy phase 12 (timecode): vgstrip + sweep + dumpd /time + configure (Caddy /time routes, MediaMTX %f) + dock/clock + portal. Then a streamer adds the band and we check a recording's tmcd and sidecar.
  2. Authentik: discord-roles-to-groups policy so streamers/editors/media-admins/bot-users follow Discord roles (bot-users group needed for the bot host). Set the provider's Invalidation flow to default-provider-invalidation-flow so every logout forces a full re-login.
  3. Confirm plugin 0.1.3 under Jellyfin → Plugins and the Gitea runner registered.
  4. Portal viewer during a real stream: corner chip webrtc; Windows Go Live with WarDogs audio (first Windows streamer).
  5. Gitea login via Authentik (plan phase 10).
  6. Housekeeping: pin AUTHENTIK_TAG, rotate the Discord client secret seen in a screenshot, nightly Postgres dump, Caddyfile formatting warning.
  7. Medal filename pattern in the plugin (need a sample filename).
  8. Home archive puller on the home box.
  9. Plugin repository served by Caddy so Jellyfin updates from its catalog.
  10. Clip submissions (belongs to the bot). Bot code from the other dev.
  11. Authentik blueprint from .env (Discord source, groups, policies, provider) for reproducible installs.
  12. SYS-3 when transcoding or sweep speed hurts.

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/live was 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>.mp4 in 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.
  • Streams are not re-encoded for VFR/gaps by default (dropped frames are legitimate; a 3 h re-encode is hours of CPU); players hold the last frame across a gap. The timecode sidecar lists gaps. Gap filling is available (FILL_GAPS_MS in normalize-sweep.service, README "Timecode") — turn on when the box has a hardware encoder. Clips always get CFR.
  • 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 -c the installed script before blaming logic.
  • MediaMTX hot-reloads paths/recording settings but NOT authMethod/auth settings: those need a container restart (between streams).
  • MediaMTX stamps recording filenames in its container clock: without TZ it is UTC and the plugin reads names as Eastern (4 h off). TZ=America/New_York is set on the container.
  • MediaMTX 1.21 refuses to start without webrtcAdditionalHosts or webrtcIPsFromInterfaces; 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.