diff --git a/config/mediamtx.yml b/config/mediamtx.yml index 38fefc2..d311030 100644 --- a/config/mediamtx.yml +++ b/config/mediamtx.yml @@ -16,30 +16,22 @@ writeQueueSize: 512 ############################################################################### api: yes -apiAddress: 127.0.0.1:49997 +apiAddress: 127.0.0.1:19997 ############################################################################### # WebRTC (WHIP ingest + WHEP playback) ############################################################################### webrtc: yes -# HTTP listener for WHIP/WHEP signaling (SDP exchange). -# NPM proxies /whep/* and the OBS WHIP target (localhost) to this. webrtcAddress: :48889 -webrtcEncryption: no # TLS is handled at NPM; this listener is LAN/localhost only -# Dedicated UDP port for SRTP media. NPM Stream forwards public UDP 48189 here. +webrtcEncryption: no webrtcLocalUDPAddress: :48189 -# No TCP fallback - we only want a single UDP path for simplicity. webrtcLocalTCPAddress: '' -# Tell browsers to send media to the public hostname. -# Replace stream.hetherman.cloud if your public hostname differs. webrtcAdditionalHosts: - stream.hetherman.cloud -# Public STUN helps browsers discover their own reflexive candidates when -# behind NAT; the server side does not need it but it speeds up ICE. + - 192.168.50.254 webrtcICEServers2: - url: stun:stun.l.google.com:19302 -# Disable trickle handshake complications - plain offer/answer is enough. webrtcHandshakeTimeout: 10s webrtcTrackGatherTimeout: 2s @@ -56,15 +48,22 @@ hlsSegmentCount: 7 hlsSegmentDuration: 200ms hlsPartDuration: 200ms hlsSegmentMaxSize: 50M -hlsAllowOrigin: '*' +hlsAllowOrigins: ['*'] hlsTrustedProxies: [] ############################################################################### -# Disabled protocols (reduce attack surface) +# Protocols ############################################################################### -rtsp: no -rtmp: no +# RTSP on localhost only - used internally so FFmpeg can read the game path +# as a consumer (not a publisher) without conflicting with OBS. +rtsp: yes +rtspAddress: 127.0.0.1:8554 + +# RTMP for OBS ingest. Localhost only. +rtmp: yes +rtmpAddress: 127.0.0.1:1935 + srt: no ############################################################################### @@ -72,18 +71,35 @@ srt: no ############################################################################### pathDefaults: - # Drop publishers that connect but never send media. sourceOnDemand: no +authInternalUsers: + - user: any + pass: "" + ips: [127.0.0.1/32, ::1/128] + permissions: + - action: publish + - action: api + - user: any + pass: "" + ips: [] + permissions: + - action: read + paths: - # The single stream path. OBS publishes here via WHIP - # (http://localhost:48889/game/whip), friends watch via WHEP - # (https://stream.hetherman.cloud/whep/game/whep). + # OBS publishes H264+AAC here via RTMP. + # runOnReady spawns FFmpeg which reads via RTSP (as a reader, no publisher + # conflict) and re-publishes to game-opus with audio transcoded to Opus. game: source: publisher - # Only the local OBS instance is allowed to publish. - # External hijack attempts are blocked at this layer, independent of NPM. - publishIPs: - - 127.0.0.1/32 - - ::1/128 - # No reader restrictions - NPM + Authentik gate reads at the edge. + runOnReady: >- + ffmpeg + -i rtsp://127.0.0.1:8554/game + -c:v copy + -c:a libopus -b:a 128k -ar 48000 -ac 2 + -f rtsp rtsp://127.0.0.1:8554/game-opus + runOnReadyRestart: yes + + # Transcoded path: H264 + Opus. Viewers connect here via WHEP/HLS. + game-opus: + source: publisher diff --git a/config/npm-advanced.conf b/config/npm-advanced.conf index 45e7474..e27974e 100644 --- a/config/npm-advanced.conf +++ b/config/npm-advanced.conf @@ -1,32 +1,61 @@ # Paste this snippet into the "Advanced" tab of the NPM Proxy Host for # stream.hetherman.cloud. It enables Authentik Forward Auth via the # goauthentik.io outpost, so every request is gated before hitting the -# frontend, WHEP signaling, HLS, or the MediaMTX API. +# frontend, WHEP signaling, and HLS. +# +# IMPORTANT: Remove all Custom Locations from the NPM GUI (/whep, /hls, /v3). +# They are defined here instead so that: +# - /whep/ and /hls/ use trailing-slash proxy_pass to strip the prefix +# before forwarding to MediaMTX (otherwise MediaMTX sees the wrong path). +# - /v3/ has auth_request off without a duplicate location block conflict. +# Duplicate location blocks (GUI + advanced) cause the wrong one to win. # # Requires an Authentik Proxy Provider of type "Forward auth (single # application)" with external host https://stream.hetherman.cloud and an # Application bound to the `stream-viewers` group. port_in_redirect off; -# Forward every incoming request to the Authentik outpost for validation. +# Forward every other request to the Authentik outpost for validation. +# Must be declared before the location blocks so it applies server-wide. auth_request /outpost.goauthentik.io/auth/nginx; error_page 401 = @goauthentik_proxy_signin; -# Propagate user identity headers set by the outpost back to the browser -# (and optionally to upstream if you ever want to read the user in MediaMTX). +# Propagate user identity headers set by the outpost back to the browser. auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_email $upstream_http_x_authentik_email; +# WHEP signaling (WebRTC SDP exchange). +# Trailing slash on both sides strips /whep prefix: /whep/game/whep -> /game/whep +location /whep/ { + proxy_pass http://192.168.50.254:48889/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} + +# HLS fallback. +# Trailing slash strips /hls prefix: /hls/game/index.m3u8 -> /game/index.m3u8 +location /hls/ { + proxy_pass http://192.168.50.254:48888/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; +} + +# MediaMTX API - no auth required, stream status only, no sensitive data. +# Trailing slash strips /v3 prefix: /v3/paths/get/game -> /paths/get/game +location /v3/ { + auth_request off; + proxy_pass http://192.168.50.254:19997/; +} + # The outpost endpoint itself must be reachable un-gated so that the # auth_request subrequest and the sign-in redirect can complete. location /outpost.goauthentik.io { - # All traffic to /outpost.goauthentik.io is proxied to the Authentik host. - # Point this at your Authentik outpost URL. - # Use the internal Authentik address directly (HTTP, no TLS) to avoid - # routing back through NPM and the SSL SNI issues that come with it. proxy_pass http://192.168.50.224:30140/outpost.goauthentik.io; proxy_set_header Host $host; proxy_set_header X-Original-URL $scheme://$http_host$request_uri; @@ -40,9 +69,7 @@ location /outpost.goauthentik.io { proxy_set_header Content-Length ""; } -# When auth_request returns 401, send the browser to the outpost sign-in page -# and preserve the original request URL so the user lands back where they -# started after logging in. +# When auth_request returns 401, redirect to the outpost sign-in page. location @goauthentik_proxy_signin { internal; add_header Set-Cookie $auth_cookie; diff --git a/docs/npm-setup.md b/docs/npm-setup.md index 28f3c97..99a7aa3 100644 --- a/docs/npm-setup.md +++ b/docs/npm-setup.md @@ -51,7 +51,7 @@ forward-auth gating). |----------|--------|------------------|--------------| | `/whep` | `http` | `` | `48889` | | `/hls` | `http` | `` | `48888` | -| `/v3` | `http` | `` | `49997` | +| `/v3` | `http` | `` | `19997` | **SSL tab:** diff --git a/docs/obs-setup.md b/docs/obs-setup.md index 781dacb..5bcb4ae 100644 --- a/docs/obs-setup.md +++ b/docs/obs-setup.md @@ -25,7 +25,7 @@ Prerequisites: | 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:49997` | + | 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`. @@ -120,7 +120,7 @@ Should report `False` while not streaming, `True` while streaming. (another process is already using them). - **Viewers see "Stream offline"** even after you click Start Streaming: - Check that the MediaMTX API returns `ready: true`: - `curl http://localhost:49997/v3/paths/get/game` + `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. diff --git a/frontend/index.html b/frontend/index.html index caef9f9..4837aea 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,7 +10,7 @@
- +
diff --git a/frontend/js/app.js b/frontend/js/app.js index be58fb7..4ff2916 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -10,8 +10,10 @@ */ (function () { - const PATH_NAME = 'game'; - const API_URL = `/v3/paths/get/${PATH_NAME}`; + // /status is served by the OBS Python HTTP server, which proxies the + // MediaMTX API internally. This avoids hitting /v3/ through NPM where + // the server-level auth_request directive blocks it. + const API_URL = '/status'; const POLL_INTERVAL_MS = 5000; const els = { @@ -105,11 +107,11 @@ } } - // ---- Unmute button -------------------------------------------------- + // ---- Unmute / play button ------------------------------------------- // - // Browsers require a user gesture to play audio. The