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
+59 -17
View File
@@ -50,7 +50,7 @@ CONFIG = {
"http_port": 48080,
"firewall_rule_name": "GameStream-UDP-48189",
"public_url": "https://stream.hetherman.cloud",
"api_url": "http://127.0.0.1:49997",
"api_url": "http://127.0.0.1:19997",
}
@@ -158,10 +158,15 @@ def start_mediamtx() -> None:
creationflags = 0
if os.name == "nt":
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
log_path = os.path.join(os.path.dirname(binary), "mediamtx.log")
log(f"MediaMTX binary: {binary!r}")
log(f"MediaMTX config: {config!r}")
log(f"MediaMTX log: {log_path}")
mediamtx_log = open(log_path, "w")
STATE.mediamtx_proc = subprocess.Popen(
[binary, config],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdout=mediamtx_log,
stderr=mediamtx_log,
creationflags=creationflags,
)
log(f"MediaMTX started (pid={STATE.mediamtx_proc.pid})")
@@ -197,7 +202,38 @@ def stop_mediamtx() -> None:
# ---------------------------------------------------------------------------
class _QuietHandler(http.server.SimpleHTTPRequestHandler):
"""SimpleHTTPRequestHandler that logs to the OBS log instead of stderr."""
"""SimpleHTTPRequestHandler that serves static files and proxies /status."""
def do_GET(self):
if self.path == "/status":
# Proxy the MediaMTX path status so the browser doesn't need to
# hit /v3/ through NPM (where auth_request blocks it).
api_url = f"{CONFIG['api_url']}/v3/paths/get/game-opus"
try:
with urllib.request.urlopen(api_url, timeout=2) as resp:
body = resp.read()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
except urllib.error.HTTPError as exc:
body = b'{"ready":false}'
self.send_response(exc.code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
except Exception:
body = b'{"ready":false}'
self.send_response(503)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
super().do_GET()
def log_message(self, format, *args): # noqa: A002 - match signature
# Keep OBS script log clean; only log errors.
@@ -273,7 +309,7 @@ def poll_status() -> None:
if STATE.mediamtx_proc is None:
return
url = f"{CONFIG['api_url']}/v3/paths/get/game"
url = f"{CONFIG['api_url']}/v3/paths/get/game-opus"
try:
with urllib.request.urlopen(url, timeout=2) as resp:
import json
@@ -304,27 +340,28 @@ def poll_status() -> None:
def _on_frontend_event(event):
if event == obs.OBS_FRONTEND_EVENT_STREAMING_STARTED:
log("OBS streaming started -> bringing up game-stream-app")
# MediaMTX is already running (started on script load / settings save).
# Just enable the firewall, start the status poller, and log the URL.
enable_firewall_rule()
start_mediamtx()
start_http_server()
if not STATE.poll_timer_registered:
obs.timer_add(poll_status, 5000)
STATE.poll_timer_registered = True
log(f"Viewers can watch at: {CONFIG['public_url']}")
elif event in (
obs.OBS_FRONTEND_EVENT_STREAMING_STOPPED,
obs.OBS_FRONTEND_EVENT_EXIT,
):
log("OBS streaming stopped -> tearing down game-stream-app")
log(f"OBS streaming started -> viewers can watch at: {CONFIG['public_url']}")
elif event == obs.OBS_FRONTEND_EVENT_STREAMING_STOPPED:
log("OBS streaming stopped -> disabling firewall rule")
if STATE.poll_timer_registered:
obs.timer_remove(poll_status)
STATE.poll_timer_registered = False
disable_firewall_rule()
STATE.last_status = "offline"
STATE.last_viewers = 0
elif event == obs.OBS_FRONTEND_EVENT_EXIT:
if STATE.poll_timer_registered:
obs.timer_remove(poll_status)
STATE.poll_timer_registered = False
stop_mediamtx()
stop_http_server()
disable_firewall_rule()
STATE.last_status = "offline"
STATE.last_viewers = 0
# ---------------------------------------------------------------------------
@@ -409,7 +446,7 @@ def script_defaults(settings):
settings, "public_url", "https://stream.hetherman.cloud",
)
obs.obs_data_set_default_string(
settings, "api_url", "http://127.0.0.1:49997",
settings, "api_url", "http://127.0.0.1:19997",
)
@@ -428,6 +465,11 @@ def script_update(settings):
def script_load(settings):
script_update(settings)
obs.obs_frontend_add_event_callback(_on_frontend_event)
# Start MediaMTX and HTTP server immediately so they are ready before
# OBS attempts the WHIP connection. OBS fires STREAMING_STARTED only
# after a successful connect, so MediaMTX must be up first.
start_mediamtx()
start_http_server()
log("game_stream.py loaded")