From 9ca1cb457d64e0316259619bc3683c89b853c958 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Tue, 11 Aug 2026 11:04:44 +0000 Subject: [PATCH] fix: try VP9/AV1 too, not just H264, and fail loudly if none available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A real test with the H264-preference fix still negotiated VP8 — RTCRtpSender.getCapabilities('video') apparently didn't list H264 on that browser/machine (Chrome's H264 encoder is a separate downloadable component, not guaranteed present). Confirmed via mediamtx's live path list: still 'tracks: ["VP8"]' after the fix was deployed. Now tries every MediaMTX-HLS-supported codec in priority order (H264, VP9, AV1) instead of only H264, and throws a clear error instead of silently falling back to VP8 if literally none of them are available — better than a stream that looks live but can never produce working HLS. --- frontend/src/lib/whip.ts | 42 +++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/frontend/src/lib/whip.ts b/frontend/src/lib/whip.ts index a32905f..a10e663 100644 --- a/frontend/src/lib/whip.ts +++ b/frontend/src/lib/whip.ts @@ -10,24 +10,40 @@ export async function publishWhip( bearer: string, stream: MediaStream, ): Promise { + // MediaMTX's HLS output only muxes AV1, VP9, H265, H264, Opus, MPEG-4 + // Audio, or KLV (confirmed live in its logs: "the stream doesn't contain + // any supported codec" — the muxer gets created then immediately + // destroyed, so the WHIP publish itself still succeeds and the stream + // shows as live, but hls/live//index.m3u8 permanently 404s with + // "muxer is waiting to be created"). Browsers default to VP8 for + // getUserMedia/getDisplayMedia video, which isn't in that list at all. + // Reordering codec preference to H264 first didn't actually change what + // got negotiated on a real test (RTCRtpSender.getCapabilities('video') + // apparently didn't list H264 on that browser/machine — Chrome's H264 + // encoder is a separate downloadable component and isn't guaranteed + // present) — so try every MediaMTX-supported codec in priority order + // instead of only H264, and fail loudly if literally none of them are + // available rather than silently falling back to the broken default. + const MEDIAMTX_HLS_VIDEO_CODECS = ['video/H264', 'video/VP9', 'video/AV1']; + const pc = new RTCPeerConnection(); for (const track of stream.getTracks()) { const transceiver = pc.addTransceiver(track, { direction: 'sendonly' }); - // Browsers default to VP8 for getUserMedia/getDisplayMedia video, which - // MediaMTX's HLS output can't mux at all (confirmed live: "the stream - // doesn't contain any supported codec, which are currently AV1, VP9, - // H265, H264, Opus, MPEG-4 Audio, KLV" — the muxer gets created then - // immediately destroyed, so the WHIP publish itself succeeds and the - // stream shows as live, but hls/live//index.m3u8 permanently 404s - // with "muxer is waiting to be created"). Reorder codec preference so - // H264 is offered first — matches what OBS already sends over RTMP, so - // this keeps a single well-tested codec through the whole pipeline - // (HLS, recording) instead of introducing a second one. if (track.kind === 'video' && typeof transceiver.setCodecPreferences === 'function') { const capabilities = RTCRtpSender.getCapabilities('video'); - const h264 = capabilities?.codecs.filter((c) => c.mimeType.toLowerCase() === 'video/h264') ?? []; - const rest = capabilities?.codecs.filter((c) => c.mimeType.toLowerCase() !== 'video/h264') ?? []; - if (h264.length > 0) transceiver.setCodecPreferences([...h264, ...rest]); + const available = capabilities?.codecs ?? []; + const preferred = MEDIAMTX_HLS_VIDEO_CODECS.flatMap((mime) => + available.filter((c) => c.mimeType.toLowerCase() === mime.toLowerCase()), + ); + if (preferred.length === 0) { + pc.close(); + throw new Error( + "This browser doesn't support any video codec MediaMTX can turn into HLS " + + `(needs one of: ${MEDIAMTX_HLS_VIDEO_CODECS.join(', ')}). Try a different browser, or use OBS instead.`, + ); + } + const rest = available.filter((c) => !preferred.includes(c)); + transceiver.setCodecPreferences([...preferred, ...rest]); } }