Go Live: MediaMTX credentials via X-Stream-Auth -> Authorization on the Caddy->MediaMTX hop (WebRTC ignores query params); fix WHIP session DELETE path
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,13 @@ def write_caddy(domain, auth_host="", watch_host="", git_host="", bot_host="", l
|
|||||||
reverse_proxy 127.0.0.1:9997
|
reverse_proxy 127.0.0.1:9997
|
||||||
}}
|
}}
|
||||||
handle_path /webrtc/* {{
|
handle_path /webrtc/* {{
|
||||||
|
# Go Live sends MediaMTX credentials in X-Stream-Auth so Authentik's forward-auth never sees an
|
||||||
|
# Authorization header; Caddy turns it into Authorization only on the hop to MediaMTX.
|
||||||
|
@creds header X-Stream-Auth *
|
||||||
|
reverse_proxy @creds 127.0.0.1:8889 {{
|
||||||
|
header_up Authorization {{http.request.header.X-Stream-Auth}}
|
||||||
|
header_up -X-Stream-Auth
|
||||||
|
}}
|
||||||
reverse_proxy 127.0.0.1:8889
|
reverse_proxy 127.0.0.1:8889
|
||||||
}}
|
}}
|
||||||
handle_path /hls/* {{
|
handle_path /hls/* {{
|
||||||
|
|||||||
@@ -56,11 +56,14 @@ export async function publish({ stream, path, user, pass, onState }) {
|
|||||||
const offer = await pc.createOffer()
|
const offer = await pc.createOffer()
|
||||||
await pc.setLocalDescription(offer)
|
await pc.setLocalDescription(offer)
|
||||||
await new Promise(res => { if (pc.iceGatheringState === 'complete') return res(); const t = setTimeout(res, 2000); pc.onicegatheringstatechange = () => { if (pc.iceGatheringState === 'complete') { clearTimeout(t); res() } } })
|
await new Promise(res => { if (pc.iceGatheringState === 'complete') return res(); const t = setTimeout(res, 2000); pc.onicegatheringstatechange = () => { if (pc.iceGatheringState === 'complete') { clearTimeout(t); res() } } })
|
||||||
const url = `/webrtc/${path}/whip?user=${encodeURIComponent(user)}&pass=${encodeURIComponent(pass)}`
|
// MediaMTX takes WebRTC credentials only in an Authorization header ("Bearer user:pass"). We send them as
|
||||||
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp, credentials: 'same-origin' })
|
// X-Stream-Auth and Caddy rewrites the header on the MediaMTX hop, so Authentik's gate never sees it.
|
||||||
|
const auth = { 'X-Stream-Auth': `Bearer ${user}:${pass}` }
|
||||||
|
const r = await fetch(`/webrtc/${path}/whip`, { method: 'POST', headers: { 'Content-Type': 'application/sdp', ...auth }, body: pc.localDescription.sdp, credentials: 'same-origin' })
|
||||||
if (!r.ok) { pc.close(); throw new Error(r.status === 401 || r.status === 403 ? 'stream key rejected' : r.status === 400 && /busy|already/i.test(await r.text().catch(() => '')) ? 'that stream name is already live (OBS running?)' : `publish failed (${r.status})`) }
|
if (!r.ok) { pc.close(); throw new Error(r.status === 401 || r.status === 403 ? 'stream key rejected' : r.status === 400 && /busy|already/i.test(await r.text().catch(() => '')) ? 'that stream name is already live (OBS running?)' : `publish failed (${r.status})`) }
|
||||||
await pc.setRemoteDescription({ type: 'answer', sdp: await r.text() })
|
await pc.setRemoteDescription({ type: 'answer', sdp: await r.text() })
|
||||||
const session = r.headers.get('Location')
|
let session = r.headers.get('Location') // MediaMTX answers relative to its own root
|
||||||
|
if (session && session.startsWith('/') && !session.startsWith('/webrtc/')) session = '/webrtc' + session
|
||||||
return {
|
return {
|
||||||
pc,
|
pc,
|
||||||
stats: async () => {
|
stats: async () => {
|
||||||
@@ -71,7 +74,7 @@ export async function publish({ stream, path, user, pass, onState }) {
|
|||||||
return { kbps, fps, w, h }
|
return { kbps, fps, w, h }
|
||||||
},
|
},
|
||||||
stop: async () => {
|
stop: async () => {
|
||||||
try { if (session) await fetch(session.startsWith('http') ? new URL(session).pathname : session, { method: 'DELETE', credentials: 'same-origin' }) } catch {}
|
try { if (session) await fetch(session.startsWith('http') ? '/webrtc' + new URL(session).pathname : session, { method: 'DELETE', headers: auth, credentials: 'same-origin' }) } catch {}
|
||||||
pc.close()
|
pc.close()
|
||||||
stream.getTracks().forEach(t => t.stop())
|
stream.getTracks().forEach(t => t.stop())
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user