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?
| Room | Broadcast | |
|---|---|---|
| Shape | A conversation: several people may speak | An audience: one publisher |
| Latency | Under half a second | About 2 s on LL-HLS, under 1 s on WebRTC |
| Audience | Interactive, can be brought on stage | Watches, and chats |
| Scales with | Media servers | Bandwidth, 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:
playback | Who can watch | Use it when |
|---|---|---|
public (default) | Anyone with the playback URL | The stream is public, or delivered through a CDN |
token | Only viewers presenting a signed token | The 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.