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:
@@ -1,22 +1,37 @@
|
||||
// MediaMTX control API, proxied by Caddy at /api (-> 127.0.0.1:9997). Returns active streams.
|
||||
const VIDEO = new Set(['H264', 'H265', 'VP8', 'VP9', 'AV1', 'MJPEG', 'M-JPEG', 'MPEG-4 Video', 'MPEG-1 Video'])
|
||||
const firstSeen = new Map() // path -> first time we saw it ready (mirror grace period)
|
||||
const MIRROR_WAIT_MS = 15000
|
||||
|
||||
export async function fetchStreams() {
|
||||
const r = await fetch('/api/v3/paths/list?itemsPerPage=200', { credentials: 'same-origin' })
|
||||
if (!r.ok) throw new Error(`api ${r.status}`)
|
||||
const data = await r.json()
|
||||
return (data.items || [])
|
||||
.filter(p => p.ready && (p.tracks || []).length > 0)
|
||||
.map(p => {
|
||||
const [user, ...rest] = p.name.split('/')
|
||||
return {
|
||||
id: p.name, // e.g. techy/VanguardTraining
|
||||
user,
|
||||
title: rest.join('/') || p.name,
|
||||
viewers: (p.readers || []).length,
|
||||
since: p.readyTime,
|
||||
tracks: p.tracks || [],
|
||||
}
|
||||
const ready = (data.items || []).filter(p => p.ready && (p.tracks || []).length > 0)
|
||||
const byName = new Map(ready.map(p => [p.name, p]))
|
||||
const now = Date.now()
|
||||
for (const p of ready) if (!firstSeen.has(p.name)) firstSeen.set(p.name, now)
|
||||
for (const k of [...firstSeen.keys()]) if (!byName.has(k)) firstSeen.delete(k)
|
||||
const out = []
|
||||
for (const p of ready) {
|
||||
if (p.name.endsWith('.webrtc')) continue // audio mirror: played through its source's panel, never its own
|
||||
const tracks = p.tracks || []
|
||||
const audio = tracks.filter(t => !VIDEO.has(t))
|
||||
const needsMirror = audio.length > 0 && !audio.includes('Opus') // AAC etc. cannot cross WebRTC
|
||||
const mirror = byName.get(p.name + '.webrtc')
|
||||
if (needsMirror && !mirror && now - firstSeen.get(p.name) < MIRROR_WAIT_MS) continue // give the mirror a moment to come up
|
||||
const [user, ...rest] = p.name.split('/')
|
||||
out.push({
|
||||
id: p.name, // e.g. techy/VanguardTraining
|
||||
user,
|
||||
title: rest.join('/') || p.name,
|
||||
whep: mirror ? mirror.name : p.name, // WebRTC plays the Opus mirror when there is one; HLS always the source
|
||||
viewers: (p.readers || []).length + (mirror ? (mirror.readers || []).length - 1 : 0), // the mirror's ffmpeg is one reader
|
||||
since: p.readyTime,
|
||||
tracks,
|
||||
})
|
||||
.sort((a, b) => (a.since || '').localeCompare(b.since || ''))
|
||||
}
|
||||
return out.sort((a, b) => (a.since || '').localeCompare(b.since || ''))
|
||||
}
|
||||
|
||||
export const whepUrl = id => `/webrtc/${id}/whep`
|
||||
|
||||
Reference in New Issue
Block a user