Compare commits

...

3 Commits

6 changed files with 52 additions and 22 deletions

View File

@@ -246,7 +246,7 @@ Checklist:
publishes the plugin zip is the obvious first pipeline publishes the plugin zip is the obvious first pipeline
- risk: the runner's Docker socket is root on the host for anyone who can push a workflow - risk: the runner's Docker socket is root on the host for anyone who can push a workflow
### Phase 10 — Gitea login via Authentik (OIDC) (later) ### Phase 10 — Gitea login via Authentik (OIDC) (compose done; Authentik + Gitea UI steps pending)
Same pattern as Jellyfin: Authentik is the identity, Gitea trusts it. Same pattern as Jellyfin: Authentik is the identity, Gitea trusts it.
- [ ] Authentik → Applications → Create with provider → OAuth2/OpenID: name `gitea`, redirect - [ ] Authentik → Applications → Create with provider → OAuth2/OpenID: name `gitea`, redirect
`https://git.HOST/user/oauth2/authentik/callback`, authorization flow implicit-consent, scopes `https://git.HOST/user/oauth2/authentik/callback`, authorization flow implicit-consent, scopes
@@ -255,12 +255,10 @@ Same pattern as Jellyfin: Authentik is the identity, Gitea trusts it.
provider OpenID Connect, name `authentik` (must match the callback path above), provider OpenID Connect, name `authentik` (must match the callback path above),
client ID/secret, discovery URL `https://auth.HOST/application/o/<slug>/.well-known/openid-configuration`, client ID/secret, discovery URL `https://auth.HOST/application/o/<slug>/.well-known/openid-configuration`,
"Skip local 2FA", claim mapping: group claim `groups` → Gitea admin group `admins` "Skip local 2FA", claim mapping: group claim `groups` → Gitea admin group `admins`
- [ ] Gitea app.ini / env: `GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION=true`, - [x] Gitea env: `DISABLE_REGISTRATION=false` + `ALLOW_ONLY_EXTERNAL_REGISTRATION=true` (local self-signup stays
`GITEA__oauth2_client__ACCOUNT_LINKING=auto`, `GITEA__oauth2_client__USERNAME=nickname` so first off), `ENABLE_AUTO_REGISTRATION`, `ACCOUNT_LINKING=auto`, `USERNAME=nickname`, `UPDATE_AVATAR`
Discord login creates the Gitea user with no form (registration stays closed for local accounts) - [ ] optional: hide the password form on the login page (custom template) so Discord is the only visible way in
- [ ] optional: `GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=true`, hide the password form on the - [ ] Authentik group `git-users` (from a Discord role) bound on the `Gitea` application (+ `admins`); only they can log in
login page (`GITEA__oauth2_client__OPENID_CONNECT_SCOPES`, custom template) so Discord is the only way in
- [ ] policy on the Authentik application: who may reach Gitea at all (e.g. `admins` + a `devs` group)
- [ ] test with a non-admin Discord user; confirm admin mapping via `admins` group - [ ] test with a non-admin Discord user; confirm admin mapping via `admins` group
### Phase 11 — MediaMTX auth through Authentik (no stream keys anywhere) ✅ live ### Phase 11 — MediaMTX auth through Authentik (no stream keys anywhere) ✅ live
@@ -287,7 +285,7 @@ Discord roles); the publish path must start with the user's own name.
- [ ] dock + README; remove streamers from configure.py; delete `streamers.json` - [ ] dock + README; remove streamers from configure.py; delete `streamers.json`
- [ ] test: OBS with an app password; Go Live with no key; a non-streamer gets denied - [ ] test: OBS with an app password; Go Live with no key; a non-streamer gets denied
### Phase 12 — frame-accurate timecode for multicam via an overscan band (built; deploy pending) ### Phase 12 — frame-accurate timecode for multicam via an overscan band ✅ live, verified (OBS: strip, −3 ms drift, 0 gaps)
Target: clips from several streamers align to the frame (16.7 ms @60) on a multicam timeline; the game Target: clips from several streamers align to the frame (16.7 ms @60) on a multicam timeline; the game
image is untouched and finals carry nothing. Reference clock = the server; streamer PC clocks are never trusted. image is untouched and finals carry nothing. Reference clock = the server; streamer PC clocks are never trusted.

View File

@@ -204,6 +204,24 @@ already existed run `./createGiteaDb.sh` once.
Runner caveat: it holds the Docker socket, so anyone who can push a workflow can run as root on the box. Runner caveat: it holds the Docker socket, so anyone who can push a workflow can run as root on the box.
Keep repo write access to trusted people. Keep repo write access to trusted people.
**Login via Authentik (Discord)** — same pattern as Jellyfin, done once:
1. Authentik → Applications → Create with provider → name `Gitea`, slug `gitea`; **OAuth2/OpenID Provider**,
authorization flow implicit-consent, client type Confidential, redirect URI (strict)
`https://git.HOST/user/oauth2/authentik/callback`, signing key = the default certificate, scopes
`openid email profile` (the default profile mapping carries `preferred_username` and `groups`). Copy client ID + secret.
2. Gitea → Site Administration → Identity & Access → Authentication Sources → Add: type **OAuth2**, name
**`authentik`** (must match the callback path), provider **OpenID Connect**, client ID/secret, OpenID Connect
Auto Discovery URL `https://auth.HOST/application/o/gitea/.well-known/openid-configuration`, tick "Skip local
2FA", claim name for groups `groups`, administrator group claim value `admins`. Save.
3. compose (already set): registration only via external sources, auto-registration + account linking on,
username from `preferred_username`. A first Discord login creates the Gitea user with no form.
4. Access = Authentik group **`git-users`**: Directory → Groups → create it (add a Discord role to it in the
roles→groups table); Applications → `Gitea` → Policy / Group / User Bindings → bind `git-users` (and `admins`).
Non-members are refused by Authentik before Gitea sees them.
5. Test in a private window: https://git.HOST/user/login → "Sign in with authentik" → Discord → logged in.
Members of `admins` become Gitea site admins on login. The local admin account stays as a fallback.
Optional later: hide the password form (custom template) so Discord is the only visible way in.
## 5f. Live portal (`live.HOST`) ## 5f. Live portal (`live.HOST`)
React + dockview page (`live/`) behind Authentik forward-auth: every active stream is a dockable window, React + dockview page (`live/`) behind Authentik forward-auth: every active stream is a dockable window,
@@ -289,9 +307,10 @@ MediaMTX asks dumpd (`POST /mtx-auth` on 127.0.0.1:8081, reachable only from Doc
connection. Reads/playback are allowed (viewers are gated by Caddy + Authentik). Publishing needs a token connection. Reads/playback are allowed (viewers are gated by Caddy + Authentik). Publishing needs a token
minted by `GET /me/stream-token` behind Authentik for a member of `streamers`/`admins`/`media-admins`; dumpd minted by `GET /me/stream-token` behind Authentik for a member of `streamers`/`admins`/`media-admins`; dumpd
checks the HMAC signature (`/etc/vanguard/stream-token.secret`, generated into `.env` as `STREAM_TOKEN_SECRET`), checks the HMAC signature (`/etc/vanguard/stream-token.secret`, generated into `.env` as `STREAM_TOKEN_SECRET`),
the expiry, and that the path starts with that user's name. MediaMTX hot-reloads paths/recording settings the expiry, and that the path starts with that user's name. MediaMTX does not pick up `mediamtx.yml` changes
but **not** the auth section: after changing auth, `docker compose restart mediamtx` between streams. live on this setup: after any change to it, `docker compose restart mediamtx` between streams. Day-to-day
`configure.py` reloads Caddy in place and restarts only copyparty. operations (streamers, tokens, titles) never touch that file. `configure.py` reloads Caddy in place and
restarts only copyparty.
## Timecode for multicam editing (frame accurate) ## Timecode for multicam editing (frame accurate)

View File

@@ -25,7 +25,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
| Who | Activity | Where | Login | | 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 | 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 | live |
| 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** | | 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 | — | | 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 | 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 | — |
@@ -47,8 +47,8 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
|---|---|---| |---|---|---|
| MediaMTX | RTMP/SRT ingest, records fMP4; publish auth delegated to dumpd (tokens via `/mtx-auth`) | live, token auth verified | | 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 | live | | copyparty | browse/scrub/download/finals upload | behind Authentik, group permissions (`editors`, `media-admins`), Discord-only, no WebDAV | live |
| dumpd + dock | dump, stream tokens, MediaMTX auth hook, `/time`; OBS dock with timecode-band setup | identity from Authentik headers | live; band/timecode **pending deploy** | | dumpd + dock | dump, stream tokens, MediaMTX auth hook, `/time`; OBS dock with timecode-band setup | identity from Authentik headers | live |
| 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 | **built, pending deploy** | | 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 | **live, verified** (OBS: frame-accurate, 0 gaps) |
| Syncthing | clip intake, UI at `/sync/` | UI behind Authentik + `media-admins` | live | | Syncthing | clip intake, UI at `/sync/` | UI behind Authentik + `media-admins` | live |
| normalize sweep | editing standard, one copy per clip | live | | normalize sweep | editing standard, one copy per clip | live |
| nftables | host firewall (22, 80/443, 1935, 2222, 3306, 8890/udp, 22000) | live | | nftables | host firewall (22, 80/443, 1935, 2222, 3306, 8890/udp, 22000) | live |
@@ -57,7 +57,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
| Jellyfin | watch page, SSO via Authentik, direct play only | 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 | | 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 | | 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 | | Gitea | git hosting on MariaDB, Actions enabled; holds this repo | live, local admin; Discord login via Authentik OIDC **compose ready, UI steps pending** |
| act_runner | CI jobs in Docker | **not registered** | | act_runner | CI jobs in Docker | **not registered** |
| discord-bot | Python runtime, idles until `bot/app/main.py` exists | live, no code yet | | 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** | | `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** |
@@ -65,11 +65,11 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
## Unfinished, in rough priority ## Unfinished, in rough priority
0. **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. 0. `docker compose restart mediamtx` at a quiet moment so recording filenames gain microseconds (the arrival-time fallback); everything else in phase 12 is live.
1. 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. 1. 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.
2. Confirm plugin 0.1.3 under Jellyfin → Plugins and the Gitea runner registered. 2. Confirm plugin 0.1.3 under Jellyfin → Plugins and the Gitea runner registered.
3. Portal viewer during a real stream: corner chip `webrtc`; Windows Go Live with WarDogs audio (first Windows streamer). 3. Portal viewer during a real stream: corner chip `webrtc`; Windows Go Live with WarDogs audio (first Windows streamer).
4. Gitea login via Authentik (plan phase 10). 4. Gitea login via Authentik: deploy the compose change, then the Authentik provider + Gitea auth source (README 5e).
5. Housekeeping: pin `AUTHENTIK_TAG`, rotate the Discord client secret seen in a screenshot, nightly Postgres dump, Caddyfile formatting warning. 5. Housekeeping: pin `AUTHENTIK_TAG`, rotate the Discord client secret seen in a screenshot, nightly Postgres dump, Caddyfile formatting warning.
6. Medal filename pattern in the plugin (need a sample filename). 6. Medal filename pattern in the plugin (need a sample filename).
7. Home archive puller on the home box. 7. Home archive puller on the home box.
@@ -86,7 +86,8 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
- 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. - 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. - 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. - 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 does not reload `mediamtx.yml` live here (bind-mounted file): any change to it needs `docker compose restart mediamtx` between streams. Nothing day-to-day touches that file.
- **Timecode verified live (2026-10-31)**: OBS + NVENC with the band → `source: strip`, drift −3 ms over 80 s, 0 gaps, 1920×1092 (band present); arrival time was 2.3 s late vs the first frame, which the strip corrected. Go Live from Linux Chromium (software encode) → strip decoded fine but 239 gaps and −85 ms: frame dropping, as expected on that path.
- 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 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. - 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. - 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.

View File

@@ -13,6 +13,7 @@ ROLE_TO_GROUP = { # <- Discord role id : A
# "222222222222222222": "streamers", # "222222222222222222": "streamers",
# "333333333333333333": "editors", # "333333333333333333": "editors",
# "444444444444444444": "admins", # "444444444444444444": "admins",
"555555555555555555": "git-users",
} }
from authentik.core.models import Group from authentik.core.models import Group

View File

@@ -364,7 +364,8 @@ srtAddress: :8890
# ---- auth ------------------------------------------------------------- # ---- auth -------------------------------------------------------------
# Publishing is authorised by dumpd (/mtx-auth) with short-lived tokens minted for Discord-authenticated # Publishing is authorised by dumpd (/mtx-auth) with short-lived tokens minted for Discord-authenticated
# members of the streamers group. Reads are allowed; viewers are gated by Caddy + Authentik. No passwords. # members of the streamers group. Reads are allowed; viewers are gated by Caddy + Authentik. No passwords.
# NOTE: MediaMTX hot-reloads paths/recording settings but NOT the auth section — restart after auth changes. # NOTE: on this setup (bind-mounted file) MediaMTX does not reload this file live: restart after any change here.
# Day-to-day operations (streamers, tokens, titles) never touch this file.
authMethod: http authMethod: http
authHTTPAddress: http://{auth_gw}:8081/mtx-auth authHTTPAddress: http://{auth_gw}:8081/mtx-auth
authHTTPExclude: authHTTPExclude:
@@ -475,8 +476,9 @@ def apply(domain, admin_pw, streamers, editors, auth_host="", watch_host="", dis
run(dc + ["up", "-d", "--build"]) # recreates only services whose compose definition changed run(dc + ["up", "-d", "--build"]) # recreates only services whose compose definition changed
run(dc + ["restart", "copyparty"]) # copyparty re-reads its config on start run(dc + ["restart", "copyparty"]) # copyparty re-reads its config on start
run(dc + ["exec", "caddy", "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]) # zero-downtime run(dc + ["exec", "caddy", "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]) # zero-downtime
print(" mediamtx: hot-reloads most of mediamtx.yml, but NOT auth changes. If you changed auth settings, run") print(" mediamtx: config changes are NOT picked up live on this setup (bind-mounted file). If mediamtx.yml changed,")
print(" docker compose restart mediamtx between streams (cuts a live recording).") print(" run docker compose restart mediamtx between streams (cuts a live recording). Adding/removing")
print(" streamers or tokens never touches mediamtx.yml, so day-to-day nothing restarts.")
def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""): def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
print(f"\n== domain : {domain or '(not set)'}") print(f"\n== domain : {domain or '(not set)'}")

View File

@@ -219,7 +219,16 @@ services:
GITEA__server__SSH_DOMAIN: ${GIT_HOST:-git.example.com} GITEA__server__SSH_DOMAIN: ${GIT_HOST:-git.example.com}
GITEA__server__SSH_PORT: "2222" # what users put in their remotes GITEA__server__SSH_PORT: "2222" # what users put in their remotes
GITEA__server__SSH_LISTEN_PORT: "22" # inside the container GITEA__server__SSH_LISTEN_PORT: "22" # inside the container
GITEA__service__DISABLE_REGISTRATION: "true" # accounts are created by an admin (or via Authentik OIDC later) # accounts come from Authentik (Discord) via OIDC: external auto-registration on, local self-registration off
GITEA__service__DISABLE_REGISTRATION: "false"
GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true"
GITEA__service__SHOW_REGISTRATION_BUTTON: "false"
GITEA__service__REQUIRE_SIGNIN_VIEW: "false"
GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION: "true"
GITEA__oauth2_client__ACCOUNT_LINKING: auto
GITEA__oauth2_client__USERNAME: nickname # Authentik preferred_username
GITEA__oauth2_client__UPDATE_AVATAR: "true"
GITEA__oauth2_client__OPENID_CONNECT_SCOPES: openid email profile
GITEA__actions__ENABLED: "true" GITEA__actions__ENABLED: "true"
GITEA__actions__DEFAULT_ACTIONS_URL: github GITEA__actions__DEFAULT_ACTIONS_URL: github
GITEA__webhook__ALLOWED_HOST_LIST: private,loopback,external GITEA__webhook__ALLOWED_HOST_LIST: private,loopback,external