WebRTC audio mirror: AAC cannot cross WebRTC, so transcode audio to Opus into <path>.webrtc

bin/mtx-on-ready.sh (runOnReady inside the mediamtx container) posts the is-live notice and, for sources whose
audio is not Opus, runs ffmpeg: video copy, audio -> Opus, published over MediaMTX's loopback RTSP to
<user>/<Title>.webrtc (record: no). dumpd accepts .webrtc publishes only from 127.0.0.1. The portal hides
mirror paths, plays the mirror over WHEP (HLS stays on the source), holds an AAC source for 15 s while its
mirror comes up, and corrects the viewer count. Verified headless with a mocked path list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 13:58:15 -06:00
parent f060277167
commit 9bdf9f1947
8 changed files with 99 additions and 26 deletions

View File

@@ -6,6 +6,7 @@ import { audio, effectiveMuted } from './useAudio.js'
// WebRTC (WHEP) first, HLS fallback. Mute/volume come from the shared audio store.
export default function Player({ params, api }) {
const { id, user, title } = params
const whep = params.whep || id // Opus audio mirror path when the source audio is AAC
const videoRef = useRef(null)
const [status, setStatus] = useState('connecting…')
const [viewers, setViewers] = useState(params.viewers ?? 0)
@@ -78,7 +79,7 @@ export default function Player({ params, api }) {
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
await new Promise(res => { if (pc.iceGatheringState === 'complete') return res(); const t = setTimeout(res, 1500); pc.onicegatheringstatechange = () => { if (pc.iceGatheringState === 'complete') { clearTimeout(t); res() } } })
const r = await fetch(whepUrl(id), { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp, credentials: 'same-origin' })
const r = await fetch(whepUrl(whep), { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, body: pc.localDescription.sdp, credentials: 'same-origin' })
if (!r.ok) throw new Error('whep ' + r.status)
await pc.setRemoteDescription({ type: 'answer', sdp: await r.text() })
setStatus('webrtc')
@@ -90,7 +91,7 @@ export default function Player({ params, api }) {
}
startWhep()
return () => { cancelled = true; try { pc?.close() } catch {}; try { hls?.destroy() } catch {}; v.srcObject = null; v.removeAttribute('src') }
}, [id])
}, [id, whep])
const icon = muted ? '🔇' : volume === 0 ? '🔈' : volume < 0.5 ? '🔉' : '🔊'
return (