Fix audio, routing, auth, and stream lifecycle

- Switch OBS output to RTMP; add FFmpeg AAC->Opus transcoding via MediaMTX
  runOnReady so WebRTC can carry audio (WebRTC requires Opus, not AAC)
- Enable RTSP on localhost so FFmpeg reads game path without publisher conflict;
  viewers connect to game-opus path (H264+Opus)
- Fix WHEP/HLS path prefix stripping in NPM advanced config; move all custom
  locations (/whep, /hls, /v3) out of NPM GUI and into advanced conf so
  trailing-slash proxy_pass correctly strips prefixes before hitting MediaMTX
- Fix MediaMTX API port 49997->19997 (49997 was in Windows ephemeral range)
- Add /status proxy endpoint to OBS HTTP server so frontend can poll stream
  readiness without hitting /v3/ through NPM where auth_request blocked it
- Fix authInternalUsers: split publish (localhost only) from read (any IP)
  so WHEP viewers are not challenged with Basic Auth by MediaMTX
- Remove muted attribute from video element; show unmute/play button on
  autoplay block so viewers get audio after one click
- Fix webrtcAdditionalHosts to include LAN IP 192.168.50.254
- Fix hlsAllowOrigin->hlsAllowOrigins deprecation warning
- Move MediaMTX/HTTP server startup to script_load (not streaming started)
  so MediaMTX is ready before OBS attempts RTMP connection
- Log MediaMTX output to bin/mediamtx.log for easier debugging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 03:42:38 -04:00
parent c23e8799fe
commit 180e95f74d
9 changed files with 160 additions and 68 deletions
+41 -25
View File
@@ -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
+38 -11
View File
@@ -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;