Publish

WHIP

Publish video from a browser or a WebRTC encoder with WHIP, the HTTP way to start a WebRTC publish.

WHIP (RFC 9725) is how a WebRTC publisher hands a stream to a server: one HTTP request with the publisher's offer, one answer back. The ingest takes the video and repackages it for the relays, without re-encoding. A browser can publish this way with no software to install, and OBS has a WHIP output.

Video only for now. The ingest accepts the offer's audio and drops it: a stream sent over WHIP plays without sound. For sound, use SRT or RTMP. WHIP's audio is still to be built.

What you need

  • A stream that lists whip in its ingest:

    curl -s $API/v1/streams -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
      -d '{"name":"cam","admission":"public","ingest":["whip"]}'

    The answer's publish.stream_key (stk_..., shown once) is the WHIP bearer token. Lost it? POST /v1/streams/{id}/stream-key makes a new one.

  • The WHIP endpoint's address. The API's publish object does not carry it yet, only the key.

How a publish goes

  1. The publisher sends POST <endpoint> with Content-Type: application/sdp, Authorization: Bearer stk_... and its SDP offer.
  2. The ingest answers 201 Created with its SDP answer and a Location header, a path of the form /whip/session/<id>. A wrong key, or a key whose stream does not list whip, gets 401; an offer it cannot use gets 406; a body that is not SDP gets 400.
  3. Media flows over WebRTC (UDP, DTLS-SRTP). The ingest offers H.264 and Opus. It does not need a STUN server of its own: it is reachable at one address it announces.
  4. To stop, the publisher sends DELETE to the Location resource with the same bearer key. The answer is 200, and the key is free for the next publisher. With no packet for 10 s the ingest ends the session itself.

OPTIONS is answered with the CORS headers a page needs, so a browser page can publish straight to the endpoint. The stream key is in the request header, so the endpoint is HTTPS (plain HTTP is only for a gateway on loopback).

From OBS

Settings, Stream: Service "WHIP", Server: the endpoint, Bearer Token: the stream key. Then the output settings of OBS and SRT: keyframe interval 1 s, no B-frames. Choose H.264 as the encoder.

From a browser

const pc = new RTCPeerConnection();
const media = await navigator.mediaDevices.getUserMedia({ video: true });
media.getTracks().forEach(t => pc.addTrack(t, media));

await pc.setLocalDescription(await pc.createOffer());
const res = await fetch(ENDPOINT, {
  method: "POST",
  headers: { "Content-Type": "application/sdp", Authorization: "Bearer " + STREAM_KEY },
  body: pc.localDescription.sdp,
});
if (res.status !== 201) throw new Error("WHIP " + res.status);
const resource = new URL(res.headers.get("Location"), ENDPOINT);   // DELETE this to stop
await pc.setRemoteDescription({ type: "answer", sdp: await res.text() });

Put a stream key in a page only for yourself: anyone who can read the page can publish to your stream. For a page that other people use, have your backend hold the key and start the publish.

Two things to know about browsers:

  • Browsers pick H.264 only if you ask. Order the offer's video codecs with RTCRtpTransceiver.setCodecPreferences so H.264 comes first.
  • A browser lowers its resolution when the network is poor. The stream's track list keeps the size of the first frame; players follow the new size at the next keyframe, but anything that reads the size from the track list will see the first one.

ffmpeg

ffmpeg's WHIP output is not supported: its DTLS certificate uses a key type the ingest does not accept, and the handshake fails. From ffmpeg, publish over SRT or RTMP.

Keyframes and loss

The ingest does not buffer. It hands each frame on when its last packet arrives. A lost packet waits for one retransmission request; a frame that cannot be completed is dropped, and the ingest asks the publisher for a new keyframe at once (a picture loss indication, at most about three a second). A publisher that never sends keyframes of its own therefore recovers, which is why WHIP tolerates loss well. Keep the keyframe interval at 1 s anyway: a viewer joining starts at a keyframe.

When it does not work

SymptomLikely cause
401 on the POSTthe bearer key is wrong, or the stream does not list whip in its ingest
406 on the POSTthe ingest could not accept the offer (the answer's body says why; check that it offers H.264)
400 on the POSTthe body is not an SDP offer
201, but the picture never startsthe publisher cannot reach the ingest's UDP address, or the browser chose VP8 or VP9 instead of H.264
Plays, but no soundexpected: WHIP carries video only for now

On this page