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]); } }