fix: prefer H264 for browser (WHIP) publishing — VP8 breaks HLS output

Confirmed live in mediamtx logs: the HLS muxer for a browser-published
stream gets created then immediately destroyed — 'the stream doesn't
contain any supported codec, which are currently AV1, VP9, H265, H264,
Opus, MPEG-4 Audio, KLV'. Chrome's getUserMedia()/getDisplayMedia()
default video codec for WebRTC is VP8, which isn't in that list. The
WHIP publish itself succeeds (stream correctly shows live, mediamtx's
own API confirms bytes arriving) so this was easy to miss — only
hls/live/<id>/index.m3u8 silently 404s with 'muxer is waiting to be
created' forever.

Fixed with RTCRtpTransceiver.setCodecPreferences(), reordering the
video codec list so H264 is offered first — falls through safely if
unavailable. Matches what OBS already sends over RTMP, so recording and
HLS both stay on the one already-tested codec instead of gaining a
second, broken one.
This commit is contained in:
2026-08-11 10:31:52 +00:00
parent 2f3a489a8a
commit b1493d6792
+17 -1
View File
@@ -12,7 +12,23 @@ export async function publishWhip(
): Promise<WhipSession> {
const pc = new RTCPeerConnection();
for (const track of stream.getTracks()) {
pc.addTransceiver(track, { direction: 'sendonly' });
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/<id>/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 offer = await pc.createOffer();