Skip to content

Build

Broadcast: Live Streaming to Large Audiences

Stream one publisher to any number of viewers with VoltKit RTC: WHIP ingest, LL-HLS and WebRTC playback, CDN delivery, and public or token-protected streams.

A broadcast is one publisher and an audience of any size. Viewers are about two seconds behind over LL-HLS, which a CDN can carry, or under one second over WebRTC.

Rooms or broadcast?

RoomBroadcast
ShapeA conversation: several people may speakAn audience: one publisher
LatencyUnder half a secondAbout 2 s on LL-HLS, under 1 s on WebRTC
AudienceInteractive, can be brought on stageWatches, and chats
Scales withMedia serversBandwidth, which a CDN carries

Use a room when viewers may become speakers. Use a broadcast when they will not and there may be a great many of them. One show can use both: co-hosts in a room, the public on a broadcast, and everybody in the same chat channel.

Go live in four steps

1. Create a stream

curl -X POST "$VOLTKIT_RTC_URL/v1/streams" \
  -H "X-API-Key: $VOLTKIT_RTC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"streamId":"launch-day"}'
{
  "streamId": "launch-day",
  "streamKey": "9f2c41d07ab35e886c01d4f2",
  "state": "idle",
  "playback": "public",
  "publish": {
    "whipUrl": "https://live.example.com/app/9f2c41d07ab35e886c01d4f2?direction=whip&token=eyJ...",
    "expiresAt": 1755621600
  },
  "playbackUrls": {
    "llhls": "https://cdn.example.com/app/9f2c41d07ab35e886c01d4f2/llhls.m3u8",
    "webrtc": "wss://live.example.com/app/9f2c41d07ab35e886c01d4f2"
  }
}

2. Publish

Send video to whipUrl with any WHIP client. The URL is complete: the publish token is already in it. From a browser:

const pc = new RTCPeerConnection();
const media = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

for (const track of media.getTracks()) {
  const transceiver = pc.addTransceiver(track, { direction: "sendonly" });
  if (track.kind === "video") {
    // Prefer H.264. It is passed through to viewers untouched;
    // any other codec has to be re-encoded.
    const h264 = RTCRtpSender.getCapabilities("video").codecs
      .filter((codec) => codec.mimeType === "video/H264");
    if (h264.length) transceiver.setCodecPreferences(h264);
  }
}

await pc.setLocalDescription(await pc.createOffer());
const response = await fetch(whipUrl, {
  method: "POST",
  headers: { "Content-Type": "application/sdp" },
  body: pc.localDescription.sdp,
});
await pc.setRemoteDescription({ type: "answer", sdp: await response.text() });

3. Play

Give viewers playbackUrls.llhls. It plays in Safari natively, and in other browsers with hls.js. On mobile, use ExoPlayer or AVPlayer. For sub-second playback, open playbackUrls.webrtc in a WebRTC player such as OvenPlayer.

4. Add chat

Mint chat tokens on a channel named after the stream. Your audience gets a conversation and you get a viewer count.

Who may publish

Publishing always needs a token. The whipUrl you get when you create a stream carries one, bound to that stream alone. A token taken from one stream's URL opens no other stream.

Creating a stream is the only response that includes a publish token. Reading or listing streams never returns one. To get another, for example when an encoder's URL has expired:

curl -X POST "$VOLTKIT_RTC_URL/v1/streams/launch-day/tokens" \
  -H "X-API-Key: $VOLTKIT_RTC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"action":"publish"}'

A publish URL is valid for six hours by default, so an encoder that reconnects after a network drop can keep using the URL it started with.

Who may watch

Each stream has a playback policy:

playbackWho can watchUse it when
public (default)Anyone with the playback URLThe stream is public, or delivered through a CDN
tokenOnly viewers presenting a signed tokenThe stream is paid, private or per-user

Set it when you create the stream, or change it later:

curl -X PATCH "$VOLTKIT_RTC_URL/v1/streams/launch-day" \
  -H "X-API-Key: $VOLTKIT_RTC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"playback":"token"}'

For a token stream, mint one token per viewer and use the URLs it returns:

curl -X POST "$VOLTKIT_RTC_URL/v1/streams/launch-day/tokens" \
  -H "X-API-Key: $VOLTKIT_RTC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"action":"play","identity":"user-99","ttlSeconds":3600}'

public does not mean guessable. A stream is published under streamKey, a random value, not under the id you chose. Knowing that a stream called launch-day exists tells nobody its playback URL.

Streams behind a CDN

A CDN fetches the stream once on behalf of many viewers, so it cannot present any single viewer's token. Token-protected LL-HLS and CDN delivery therefore do not combine. Keep a CDN-delivered stream public and rely on its unguessable URL, or use your CDN's own signed URLs. WebRTC playback is always direct and can stay token-protected.

Stream state

state is idle or live. A stream goes live when a publisher connects and returns to idle when it disconnects. The same stream can go live again, so an encoder that drops and reconnects is still one stream.

curl "$VOLTKIT_RTC_URL/v1/streams/launch-day" -H "X-API-Key: $VOLTKIT_RTC_SECRET"
curl "$VOLTKIT_RTC_URL/v1/streams" -H "X-API-Key: $VOLTKIT_RTC_SECRET"
curl -X DELETE "$VOLTKIT_RTC_URL/v1/streams/launch-day" -H "X-API-Key: $VOLTKIT_RTC_SECRET"

Your webhook receives stream.started and stream.ended. Deleting a stream stops its URLs from working at once.

Counting viewers

A stream's viewers field counts playback sessions the media server is serving directly. Viewers behind a CDN are not in that number, because they never reach the server. For an audience count that covers everyone, use the member count of the stream's chat channel.

Cost

Time on air, and playback the media server serves directly, use your hours in the same way as time in a room. See Usage and billing.

Last updated . Something unclear or wrong? Tell us.