From 0dff4eeee3523ad178d985153bae21116d4f51e3 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 6 Apr 2026 03:46:13 -0400 Subject: [PATCH] Update all docs to reflect current working configuration - README: update architecture diagram for RTMP+FFmpeg pipeline, add FFmpeg install step, fix path descriptions - obs-setup: switch from WHIP to RTMP output, add FFmpeg prerequisite, fix script log messages (MediaMTX starts on load not streaming start), add Python setup note, update troubleshooting for game-opus path and audio - npm-setup: remove Custom Locations GUI instructions (must be empty - all locations defined in Advanced tab only), update verify steps to game-opus paths, add troubleshooting for WHEP 400/401 causes - authentik-setup: add section 6 covering both manual account creation and self-service enrollment via invite link; clarify User Write stage group field is what triggers auto-add (not the invitation form) Co-Authored-By: Claude Sonnet 4.6 --- README.md | 34 ++++++++----- docs/authentik-setup.md | 40 +++++++++++++-- docs/npm-setup.md | 64 +++++++++++++----------- docs/obs-setup.md | 106 ++++++++++++++++++++++++---------------- 4 files changed, 158 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index ee86ed8..07781ab 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,21 @@ Authentik authentication. ## How it works ``` -OBS Studio (NVENC, WHIP out) - -> MediaMTX (localhost) ---> WHEP / HLS / API - -> Frontend HTTP server (localhost:8080) +OBS Studio (NVENC H.264, RTMP out) + -> MediaMTX (localhost, RTMP ingest) + -> FFmpeg (AAC -> Opus transcode, internal RTSP) + -> game-opus path (H.264 + Opus) + -> WHEP / HLS for viewers + -> Frontend HTTP server (localhost:48080) -> NPM (TLS, Authentik forward auth, reverse proxy) -> Friend's browser ``` +OBS outputs RTMP to MediaMTX on localhost. MediaMTX spawns FFmpeg via +`runOnReady` to transcode audio from AAC to Opus (required for WebRTC) and +re-publish to the `game-opus` path. Viewers connect to `game-opus` via WHEP +(WebRTC) or HLS. + Everything on the gaming PC (MediaMTX, HTTP server, Windows Firewall rule for the WebRTC UDP port) is spawned and torn down by an OBS Python script - `obs-script/game_stream.py`. You just click **Start Streaming** in OBS and the @@ -28,7 +36,7 @@ whole pipeline comes up; click **Stop Streaming** and it all goes away. | Path | Purpose | |-------------------------------|-------------------------------------------------------------| -| `config/mediamtx.yml` | MediaMTX configuration (WHIP in, WHEP/HLS out, locked-down) | +| `config/mediamtx.yml` | MediaMTX configuration (RTMP in, WHEP/HLS out, FFmpeg transcode) | | `config/npm-advanced.conf` | Authentik forward-auth snippet for the NPM Advanced tab | | `obs-script/game_stream.py` | OBS script: lifecycle, HTTP server, firewall toggle | | `frontend/index.html` | Viewer page | @@ -36,27 +44,29 @@ whole pipeline comes up; click **Stop Streaming** and it all goes away. | `frontend/js/app.js` | Status polling and DOM glue | | `frontend/css/style.css` | Dark theme | | `scripts/install.ps1` | Downloads MediaMTX, creates the Windows Firewall rule | -| `docs/authentik-setup.md` | Authentik proxy provider + group configuration | +| `docs/authentik-setup.md` | Authentik proxy provider, group, and invite configuration | | `docs/npm-setup.md` | NPM proxy host + stream (UDP) configuration | -| `docs/obs-setup.md` | OBS encoder + WHIP output settings | +| `docs/obs-setup.md` | OBS encoder + RTMP output settings | ## Setup at a glance 1. **Clone** this repo onto the Windows gaming PC. -2. **Install MediaMTX and the firewall rule:** open an elevated PowerShell in +2. **Install FFmpeg:** `winget install Gyan.FFmpeg` in an elevated PowerShell, + then open a new terminal to verify: `ffmpeg -version`. +3. **Install MediaMTX and the firewall rule:** open an elevated PowerShell in the repo root and run `.\scripts\install.ps1`. -3. **Configure Authentik** - see `docs/authentik-setup.md`. -4. **Configure NPM** - see `docs/npm-setup.md`. -5. **Configure OBS** - see `docs/obs-setup.md`, then add +4. **Configure Authentik** - see `docs/authentik-setup.md`. +5. **Configure NPM** - see `docs/npm-setup.md`. +6. **Configure OBS** - see `docs/obs-setup.md`, then add `obs-script/game_stream.py` via Tools -> Scripts. -6. **Click Start Streaming in OBS.** Friends can now open +7. **Click Start Streaming in OBS.** Friends can now open `https://stream.hetherman.cloud`, log in with Authentik, and watch. ## Security posture - TLS terminates at NPM with Let's Encrypt. - Every request is gated by Authentik forward auth before it reaches the - frontend, WHEP signaling, HLS, or the MediaMTX API. + frontend, WHEP signaling, or HLS. - MediaMTX only accepts publishers from `127.0.0.1` - nobody on the public internet can hijack the stream. - The UDP port used for WebRTC media is opened on the Windows Firewall only diff --git a/docs/authentik-setup.md b/docs/authentik-setup.md index 21de816..7599acd 100644 --- a/docs/authentik-setup.md +++ b/docs/authentik-setup.md @@ -11,8 +11,6 @@ have admin access. 1. Authentik admin -> **Directory -> Groups -> Create** 2. Name: `stream-viewers` 3. Save. -4. Add each friend's user account to this group. **Do not** add yourself / - your admin account - your access comes from your existing admin group. ## 2. Create a Proxy Provider @@ -68,7 +66,43 @@ curl -I https://auth.hetherman.cloud/outpost.goauthentik.io/ping A 200 or 204 means the outpost is up. -## 6. Verify end-to-end +## 6. Adding friends + +You have two options: + +### Option A: Create accounts manually (recommended for small groups) + +1. **Directory -> Users -> Create** + - Set username, name, email + - Set a temporary password + - Enable "User must change password on next login" +2. **Directory -> Groups -> stream-viewers -> Users tab -> Add** the new user. +3. Send your friend the URL (`https://stream.hetherman.cloud`) and their + temporary credentials. They set their own password on first login. + +### Option B: Self-service enrollment via invite link + +This requires a working enrollment flow with an Invitation stage. The key +requirement is that the **User Write stage** in the enrollment flow must have +`stream-viewers` set in the **Groups** field - this is what auto-adds new +users to the group. The invitation form itself has no group field. + +1. **Flows & Stages -> Stages** - find or create a User Write stage and set + **Groups** to `stream-viewers`. +2. **Flows & Stages -> Flows** - create an enrollment flow with these stages + in order: Invitation stage -> prompt (email/password) -> User Write -> Login. +3. **Flows & Stages -> Invitations -> Create**: + - **Flow**: your enrollment flow + - **Single use**: on (one invite per person) + - Set an expiry date +4. Copy the invite link (shown after saving) and send it to your friend. They + click it, fill in username/password, and are redirected to the stream. + +> **Important:** if friends completed enrollment but were not added to +> `stream-viewers` (because the User Write stage was not configured), add them +> manually: **Directory -> Groups -> stream-viewers -> Users tab -> Add**. + +## 7. Verify end-to-end After finishing `docs/npm-setup.md`: diff --git a/docs/npm-setup.md b/docs/npm-setup.md index 99a7aa3..ec6ead2 100644 --- a/docs/npm-setup.md +++ b/docs/npm-setup.md @@ -8,7 +8,7 @@ Configures NPM to: **Stream** (L4 UDP proxy). Replace `` with the LAN IP of the Windows gaming PC -(e.g., `192.168.50.10`). +(e.g., `192.168.50.254`). ## 1. DNS @@ -22,7 +22,7 @@ Make sure your router forwards these to NPM (not to the PC directly): | Proto | External port | Internal target | |-------|--------------|-------------------| | TCP | 443 | NPM host, 443 | -| UDP | 48189 | NPM host, 48189 | +| UDP | 48189 | NPM host, 48189 | (TCP 443 is probably already forwarded for your other services; UDP 48189 is the new one for this app.) @@ -38,20 +38,15 @@ In NPM, **Hosts -> Proxy Hosts -> Add Proxy Host**. | Domain Names | `stream.hetherman.cloud` | | Scheme | `http` | | Forward Hostname | `` | -| Forward Port | `48080` | +| Forward Port | `48080` | | Cache Assets | off | | Block Common Exploits | on | -| Websockets Support | **on** (WebRTC signaling works without this, but it costs nothing) | +| Websockets Support | **on** | -**Custom locations tab:** add three entries so WHEP, HLS, and the MediaMTX -API are reverse-proxied to the right MediaMTX ports (and inherit the same -forward-auth gating). - -| Location | Scheme | Forward Hostname | Forward Port | -|----------|--------|------------------|--------------| -| `/whep` | `http` | `` | `48889` | -| `/hls` | `http` | `` | `48888` | -| `/v3` | `http` | `` | `19997` | +**Custom Locations tab:** leave this **empty**. Do not add any custom location +entries here. The `/whep/`, `/hls/`, and `/v3/` locations are defined in the +Advanced tab config below with trailing-slash path stripping. Adding them in +the GUI creates duplicate nginx location blocks that cause routing failures. **SSL tab:** @@ -61,8 +56,12 @@ forward-auth gating). - HSTS Enabled: optional **Advanced tab:** paste the entire contents of -[`config/npm-advanced.conf`](../config/npm-advanced.conf). This installs the -Authentik forward-auth subrequest and the sign-in redirect. +[`config/npm-advanced.conf`](../config/npm-advanced.conf). This installs: +- Authentik forward-auth subrequest on all requests +- `/whep/` -> MediaMTX WHEP port 48889 (prefix stripped) +- `/hls/` -> MediaMTX HLS port 48888 (prefix stripped) +- `/v3/` -> MediaMTX API port 19997 (prefix stripped, auth bypassed) +- `/outpost.goauthentik.io` -> Authentik internal outpost Save the proxy host. Wait for the Let's Encrypt certificate to be issued. @@ -72,9 +71,9 @@ In NPM, **Hosts -> Streams -> Add Stream**. | Field | Value | |-------------------|---------------| -| Incoming Port | `48189` | +| Incoming Port | `48189` | | Forward Host | `` | -| Forward Port | `48189` | +| Forward Port | `48189` | | TCP | **off** | | UDP | **on** | @@ -88,14 +87,23 @@ on the gaming PC. This is the path WebRTC media takes after ICE negotiation. `auth.hetherman.cloud` to log in. Log in as a `stream-viewers` member - you should land back at the stream page (video container + "Stream offline" overlay, assuming you haven't started OBS yet). -2. **Certificate:** the padlock icon should show the Let's Encrypt cert you - requested. -3. **/whep, /hls, /v3:** once you start streaming in OBS, open DevTools on - the stream page and confirm requests to `/whep/game/whep`, - `/hls/game/index.m3u8`, and `/v3/paths/get/game` all return 200 (and not - 401/302). -4. **UDP stream:** with OBS streaming, tail the NPM container logs - you - should see entries from the stream module for UDP connections on 48189. - Alternatively, from the NPM host run - `tcpdump -n -i any udp port 48189` and confirm packets flow while a - viewer is connected. +2. **Certificate:** the padlock icon should show the Let's Encrypt cert. +3. **With OBS streaming**, open DevTools on the stream page and confirm: + - `POST /whep/game-opus/whep` returns 201 + - `GET /hls/game-opus/index.m3u8` returns 200 + - `GET /status` returns 200 with `"ready": true` +4. **UDP stream:** with OBS streaming and a viewer connected, run + `tcpdump -n -i any udp port 48189` on the NPM host and confirm packets flow. + +## Troubleshooting + +- **500 or auth loop on stream.hetherman.cloud:** check the outpost `proxy_pass` + in the Advanced config uses the internal Authentik address + (`http://192.168.50.224:30140`) not the public HTTPS URL. Using the public + URL causes SSL SNI issues when NPM tries to proxy to itself. +- **WHEP returns 400:** the Custom Locations tab in the NPM GUI has `/whep`, + `/hls`, or `/v3` entries. Remove them - these must only be in the Advanced + tab config so the trailing-slash path stripping works correctly. +- **WHEP returns 401 / browser shows a Basic Auth dialog:** MediaMTX is + challenging the viewer directly. Check `authInternalUsers` in `mediamtx.yml` + has a rule allowing `read` from any IP (empty `ips: []`). diff --git a/docs/obs-setup.md b/docs/obs-setup.md index 5bcb4ae..f34a220 100644 --- a/docs/obs-setup.md +++ b/docs/obs-setup.md @@ -1,34 +1,46 @@ # OBS setup Configures OBS Studio on the Windows gaming PC to capture the game, encode -with NVENC, and publish via WHIP to the local MediaMTX instance that the OBS -script spawns. +with NVENC H.264, and publish via RTMP to the local MediaMTX instance that the +OBS script spawns. Prerequisites: -- OBS Studio 30.0 or newer (WHIP output is built in from 30.x onward). +- OBS Studio 30.0 or newer. +- FFmpeg installed and on PATH: `winget install Gyan.FFmpeg` (admin PowerShell). + MediaMTX uses FFmpeg to transcode audio AAC->Opus for WebRTC compatibility. - You already ran `.\scripts\install.ps1` in an elevated PowerShell, so `bin\mediamtx.exe` exists and the `GameStream-UDP-48189` firewall rule is registered (in the disabled state). +- **Run OBS as Administrator** so the script can toggle the Windows Firewall + rule with netsh. ## 1. Load the OBS script -1. OBS -> **Tools -> Scripts -> +** -2. Select `obs-script/game_stream.py` from this repo. -3. In the properties panel on the right, set: +1. OBS -> **Tools -> Scripts -> Python Settings** tab - point it at your Python + 3.11 installation (e.g. `C:\Users\\AppData\Local\Programs\Python\Python311`). + If OBS shows no properties after loading the script, this is the cause. +2. OBS -> **Tools -> Scripts -> +** +3. Select `obs-script/game_stream.py` from this repo. +4. In the properties panel on the right, set: | Setting | Value | |-----------------------|---------------------------------------------------------------| | MediaMTX binary | `\bin\mediamtx.exe` | | MediaMTX config | `\config\mediamtx.yml` | | Frontend directory | `\frontend` | - | Frontend HTTP port | `48080` (default) | + | Frontend HTTP port | `48080` (default) | | Firewall rule name | `GameStream-UDP-48189` (must match the rule created by install.ps1) | | Public URL | `https://stream.hetherman.cloud` | - | MediaMTX API URL | `http://127.0.0.1:19997` | + | MediaMTX API URL | `http://127.0.0.1:19997` | -4. Check the **Script Log** at the bottom - you should see - `[game_stream] game_stream.py loaded`. +5. Check the **Script Log** at the bottom - you should see: + - `[game_stream] MediaMTX started (pid=...)` + - `[game_stream] Frontend HTTP server listening on 0.0.0.0:48080` + - `[game_stream] game_stream.py loaded` + + MediaMTX and the HTTP server start immediately on script load (not on + streaming started), so they are ready before OBS attempts RTMP. ## 2. OBS output settings @@ -38,32 +50,26 @@ Prerequisites: | Setting | Value | |------------------|---------------------------------------------------------| -| Audio Encoder | Opus (or FFmpeg AAC if Opus is unavailable - Opus is preferred for WebRTC) | -| Video Encoder | **NVIDIA NVENC HEVC** or **NVIDIA NVENC H.264** | +| Audio Encoder | FFmpeg AAC (only option; MediaMTX+FFmpeg transcodes to Opus for WebRTC) | +| Video Encoder | **NVIDIA NVENC H.264** | Use H.264 for maximum browser compatibility (all browsers). HEVC works in -Safari and recent Chrome but not Firefox - stick with H.264 unless you have a -specific reason. +Safari and recent Chrome but not Firefox. **Encoder settings (H.264):** | Setting | Value | |---------------------|-------------------| | Rate Control | CBR | -| Bitrate | 8000 Kbps | +| Bitrate | 8000-16000 Kbps | | Keyframe Interval | 2 s | | Preset | P5 (Quality) | | Tuning | Ultra Low Latency | | Multipass | Two Passes (Quarter Resolution) | | Profile | high | | Look-ahead | off | -| Psycho Visual Tuning | on | -| GPU | 0 | | Max B-frames | **0** (required for low-latency WebRTC) | -With a 600 Mbps upload and up to 6 viewers at 8 Mbps each, 8000 Kbps leaves -generous headroom. Push to 12000-15000 Kbps if you want higher quality. - ### Audio tab | Setting | Value | @@ -78,9 +84,11 @@ generous headroom. Push to 12000-15000 Kbps if you want higher quality. | Setting | Value | |----------|-----------------------------------------------| | Service | Custom | -| Protocol | **WHIP** | -| Server | `http://localhost:48889/game/whip` | -| Bearer Token | (leave blank) | +| Server | `rtmp://127.0.0.1/game` | + +> **Note:** Use RTMP (not WHIP). WHIP uses WebRTC which requires ICE/UDP +> negotiation between OBS and MediaMTX - this fails over localhost due to NAT +> hairpin issues. RTMP is plain TCP and works reliably on localhost. Save. @@ -88,21 +96,25 @@ Save. 1. Click **Start Streaming**. 2. Check the OBS Script Log - you should see: - - `Firewall rule 'GameStream-UDP-48189' ENABLED` - - `MediaMTX started (pid=...)` - - `Frontend HTTP server listening on 0.0.0.0:48080` - - `Viewers can watch at: https://stream.hetherman.cloud` -3. Open `https://stream.hetherman.cloud` from another device, log in with - Authentik, and verify video plays. + - `[game_stream] Firewall rule 'GameStream-UDP-48189' ENABLED` + - `[game_stream] OBS streaming started -> viewers can watch at: https://stream.hetherman.cloud` +3. After 2-3 seconds, MediaMTX spawns FFmpeg to transcode the audio. The + `game-opus` path becomes ready. The stream page will show **LIVE**. +4. Open `https://stream.hetherman.cloud` from another device, log in with + Authentik, and verify video and audio play. Click the **"Click to play"** + or **"Click to unmute"** button in the status bar if audio is muted. ## 5. Stopping Click **Stop Streaming** in OBS. The script will: -- Stop the MediaMTX subprocess -- Stop the frontend HTTP server - Disable the firewall rule (`GameStream-UDP-48189` -> disabled) +When OBS exits, the script also: + +- Stops the MediaMTX subprocess (and with it, the FFmpeg transcoder) +- Stops the frontend HTTP server + Verify the firewall state from PowerShell: ```powershell @@ -113,20 +125,28 @@ Should report `False` while not streaming, `True` while streaming. ## Troubleshooting +- **No properties panel after loading script:** OBS is using the Microsoft + Store Python stub. Install real Python 3.11 via `winget install Python.Python.3.11` + and point OBS Python Settings to the install directory. - **"MediaMTX binary not found"** in the script log: the path in the script properties panel is wrong. Re-select it with the file picker. -- **OBS cannot connect to WHIP**: MediaMTX did not start. Check the script - log for the actual reason; most commonly a port conflict on 48889 or 48189 - (another process is already using them). +- **OBS cannot connect / streaming fails immediately:** MediaMTX did not start. + Check the script log for errors. Most commonly a port conflict on 1935, 48889, + or 48189 (another process is already using them). Also check `bin\mediamtx.log`. +- **Stream page shows "Connecting..." but never goes LIVE:** FFmpeg failed to + start the `game-opus` transcoder. Check `bin\mediamtx.log` for FFmpeg errors. + Verify `ffmpeg -version` works in a new PowerShell (not the same session as + before installing FFmpeg). - **Viewers see "Stream offline"** even after you click Start Streaming: - - Check that the MediaMTX API returns `ready: true`: - `curl http://localhost:19997/v3/paths/get/game` - - Check OBS's own streaming indicator - if it's red, OBS is not actually - sending to WHIP. Verify the URL and that the custom service / WHIP - protocol is selected. + - Check that the MediaMTX API returns `ready: true` for the transcoded path: + `curl http://localhost:19997/v3/paths/get/game-opus` + - Check OBS's own streaming indicator - if it's red, OBS is not connected. + Verify the RTMP server URL is `rtmp://127.0.0.1/game`. - **Viewers connect but playback freezes after a few seconds:** the UDP port path is broken. Verify the firewall rule is enabled (`Get-NetFirewallRule`), - the router port-forward to NPM for UDP 48189 is correct, and the NPM Stream - entry points at `:48189`. -- **Autoplay is blocked / no audio:** browsers start the video muted so - autoplay works. There is a "Click to unmute" button in the status bar. + the router port-forward for UDP 48189 goes to NPM, and the NPM Stream entry + points at `:48189`. +- **No audio:** the "Click to play" or "Click to unmute" button appears in the + status bar when the browser blocks autoplay. Click it once. If audio is still + absent, check `bin\mediamtx.log` to confirm FFmpeg started successfully and + the `game-opus` path shows both H264 and Opus tracks.