Skip to main content

Ultra-Low Latency Streaming with Cloudflare (WHIP/WHEP)

Eyeson can forward the complete meeting stream — the One View MCU stream that every participant sees — to any WHIP endpoint. Cloudflare Stream offers exactly that: a WebRTC live input that accepts a WHIP publish and hands the stream back out over WHEP with sub-second latency.

The good news: this works out of the box. No transcoding, no relay, no special configuration on either side. You create a live input at Cloudflare, take the WHIP URL and point an Eyeson forward at it. Two API calls, and your meeting is live.

This guide will cover

  • Creating a Cloudflare Stream live input (dashboard and API)
  • Starting an Eyeson meeting via REST API
  • Forwarding the MCU One View stream to Cloudflare over WHIP
  • Playing the stream with the Cloudflare player or any WHEP client
  • Stopping the forward

Why WebRTC instead of RTMP?

Eyeson already supports broadcasting a meeting to a single RTMP destination such as YouTube, LinkedIn or Facebook. That path is well suited for classic one-way distribution, but RTMP ingests are almost always repackaged into HLS or DASH before they reach a viewer, and every segment added along the way costs time.

Where the difference actually matters:

  • Interactive formats. Live auctions, betting, quiz shows, Q&A and trading desks break down when the audience is 15 seconds behind the room. Under a second, the audience is effectively in the room.
  • Time-sensitive operations. Drone and bodycam feeds composed into One View and pushed out to a control room stay actionable instead of historic.
  • Second screens and back channels. Viewers can react over chat while the stream is still live, and their reaction still makes sense.
  • Fan-out without giving up latency. Cloudflare distributes a single WHIP publish to thousands of concurrent WHEP viewers, so you get scale and speed rather than trading one for the other.
info

The RTMP broadcast and the WebRTC forward are not mutually exclusive. You can run a broadcast to YouTube for the wide audience and a WebRTC forward for the participants who need it live, from the very same meeting.

Prerequisites

  • An Eyeson API key — request one via the API dashboard.
  • A Cloudflare account with Stream enabled.
  • For the API examples: a Cloudflare API token with the Stream:Write permission and your account ID.

Step 1: Create a Cloudflare live input

Using the dashboard

  1. Log in to dash.cloudflare.com and open Build → Images & Stream → Live Inputs.
  2. Click Create live input.
  3. Give it any name. Leave live playback and recording disabled — WebRTC inputs cannot be recorded or played back over HLS, so there is nothing to gain from switching those on.
  4. Confirm with Create live input.
  5. Open the Broadcast tab, scroll to the WebRTC section and copy the WebRTC (WHIP) URL.
  6. Open the Playback tab and copy the WebRTC (WHEP) Playback URL from Protocol URLs, or grab the embed code for Cloudflare's own player.

The two URLs look like this:

WHIP https://customer-<CODE>.cloudflarestream.com/<SECRET>/webRTC/publish
WHEP https://customer-<CODE>.cloudflarestream.com/<INPUT_UID>/webRTC/play
warning

The WHIP URL contains the broadcast secret. Anyone holding it can publish to your live input, so treat it like a credential: keep it server-side and never ship it to a browser.

Using the Cloudflare API

If you create a stream per meeting, do it programmatically. A POST to the /live_inputs endpoint returns everything you need in one response.

curl -X POST \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"meta": { "name": "Eyeson Meeting" },
"enabled": true,
"recording": { "mode": "off" }
}' \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/stream/live_inputs"

Response (shortened)

{
"result": {
"uid": "1a553f11a88915d093d45eda660d2f8c",
"webRTC": {
"url": "https://customer-<CODE>.cloudflarestream.com/<SECRET>/webRTC/publish"
},
"webRTCPlayback": {
"url": "https://customer-<CODE>.cloudflarestream.com/<INPUT_UID>/webRTC/play"
}
}
}

Three fields matter:

FieldUse
result.uidIdentifies the live input. Keep it to update or delete the input later.
result.webRTC.urlThe WHIP publish URL. This is the url of the Eyeson forward. Secret — server-side only.
result.webRTCPlayback.urlThe WHEP playback URL. Safe to hand to viewers.
tip

recording.mode is set to off on purpose. Cloudflare does not currently record WebRTC inputs or serve them over HLS/DASH, so a recording configuration would have no effect.

Step 2: Start an Eyeson meeting

Start the meeting with your API key. See the meeting room reference for all available options.

POST /rooms
HEADERS Authorization

user[name]=John Doe
options[sfu_mode]=disabled
options[widescreen]=true
curl -X POST \
-H "Authorization: $API_KEY" \
-d "user[name]=John Doe" \
-d "options[sfu_mode]=disabled" \
-d "options[widescreen]=true" \
"https://api.eyeson.team/rooms"

Two options are worth explaining:

  • sfu_mode: disabled keeps the meeting on the MCU - single composed One View stream - you are about to forward.
  • widescreen: true gives you a 16:9 output — the aspect ratio every player and viewer expects. If set to false, the aspect ratio is 4:3.
warning

Forwarding is an API-key feature. The forward must be requested with the same API key that started the meeting, so start the room server-side rather than reusing a guest link.

Step 3: Forward the meeting to Cloudflare

Now point the MCU forward at the WHIP URL from step 1. Details in the forward reference.

POST /rooms/`ROOM_ID`/forward/mcu
HEADERS Authorization

REQUIRED forward_id, type, url
curl -X POST \
-H "Authorization: $API_KEY" \
-d "forward_id=cloudflare" \
-d "type=audio,video" \
-d "url=$CLOUDFLARE_WHIP_URL" \
"https://api.eyeson.team/rooms/$ROOM_ID/forward/mcu"

Note that the forward endpoint takes the ROOM_ID, not the access key used elsewhere in the API. The forward_idcloudflare in this example — is yours to choose and is how you stop the forward again later.

That is the entire integration. Eyeson negotiates WHIP with Cloudflare directly, and the stream appears on the live input within a second or two.

Step 4: Play the stream

Cloudflare's Stream player

The quickest check. Open the Stream Player URL from the live input's Playback tab directly in a browser:

https://customer-<CODE>.cloudflarestream.com/<INPUT_UID>/iframe

or embed the generated iframe:

<iframe
src="https://customer-<CODE>.cloudflarestream.com/<INPUT_UID>/iframe"
style="border: none"
height="405"
width="720"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowfullscreen="true"
></iframe>

The Stream player upgrades to WHEP automatically when a WebRTC broadcast is available. Its attributes, events and customization options are documented in Use the Stream Player.

Your own player

For full control over the viewing experience, play the WHEP URL yourself. WHEP signaling is a single HTTP request and response, so no library is needed: create an RTCPeerConnection, ask for receive-only tracks, POST your SDP offer to the WHEP URL and apply the answer Cloudflare returns.

The example below is Cloudflare's browser playback example, reduced to the playback side — the broadcasting half is already handled by the Eyeson forward. It is one way to do it; any WHEP-capable player works just as well.

<video id="playback-video" autoplay playsinline controls muted></video>
playback.js
// Paste the webRTCPlayback.url value from your live input.
const WHEP_URL = 'https://customer-<CODE>.cloudflarestream.com/<INPUT_UID>/webRTC/play';

async function startPlayback() {
const pc = new RTCPeerConnection();

// 1. Ask to receive one audio track and one video track.
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });

// 2. Attach incoming media to the video element as it arrives.
const stream = new MediaStream();
document.getElementById('playback-video').srcObject = stream;
pc.ontrack = event => stream.addTrack(event.track);

// 3. Create the SDP offer and set it as the local description.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// 4. POST the offer to the WHEP endpoint.
const response = await fetch(WHEP_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp,
});
if (!response.ok) {
throw new Error(`WHEP request failed: ${response.status}`);
}

// 5. Apply the SDP answer returned by Cloudflare.
const answer = await response.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answer });

// The Location header identifies this session, used to stop it later.
const sessionUrl = new URL(response.headers.get('Location'), WHEP_URL).toString();

return { pc, sessionUrl };
}

startPlayback().catch(console.error);

End the session explicitly when the viewer is done:

async function stopPlayback({ pc, sessionUrl }) {
if (sessionUrl) {
await fetch(sessionUrl, { method: 'DELETE' });
}
pc.close();
}
info

Browsers block autoplay with sound, which is why the video element above starts muted. Unmute it on a user interaction, or your first viewers will report a silent stream.

tip

Prefer a maintained library over your own signaling? Cloudflare keeps a list of tested WHEP clients covering JavaScript, TypeScript and React Native.

Stopping the forward

DELETE /rooms/`ROOM_ID`/forward/`FORWARD_ID`
HEADERS Authorization
curl -X DELETE \
-H "Authorization: $API_KEY" \
"https://api.eyeson.team/rooms/$ROOM_ID/forward/cloudflare"
note

The forward also stops automatically when the meeting ends, so an explicit call is only needed if you want to end the stream while the meeting continues.

Complete example

Everything above in one file, using the @eyeson/node SDK.

npm install --save @eyeson/node
meeting.js
import Eyeson from '@eyeson/node';

const apiKey = '...';
const userName = 'John Doe';
const meetingTitle = 'Cloudflare Meeting';
const forwardId = 'cloudflare';
const cloudflarePublishUrl = 'https://customer-<CODE>.cloudflarestream.com/<SECRET>/webRTC/publish';

const eyeson = new Eyeson({ apiKey });

const meeting = await eyeson.join(userName, null, {
name: meetingTitle,
options: {
sfu_mode: 'disabled',
widescreen: true,
},
});

console.log(meeting.data.links.gui, meeting.data.links.guest_join);

await meeting.waitReady();

const forward = eyeson.createRoomForward(meeting.roomId);
await forward.mcu(forwardId, 'audio,video', cloudflarePublishUrl);

// later:
await forward.stop(forwardId);
// the forward automatically stops when the meeting ends
node meeting.js

Open the printed gui link to join the meeting yourself, share the guest_join link with others, and watch the composed One View stream arrive at Cloudflare in real time.

Good to know

  • WHIP and WHEP go together. A Cloudflare input published over WHIP is played over WHEP. It cannot be recorded or served as HLS/DASH, and an RTMP/SRT input cannot be played over WHEP.
  • One forward, one forward_id. Forward the same meeting to several destinations by repeating the call with a different forward_id each time.
  • Audio or video only. Set type to audio or video if you just need one of the channels.
  • Pricing. Cloudflare bills WebRTC delivery under standard Stream pricing. Check the Cloudflare docs for current rates and the latest state of the feature.

Let's talk

We'd love to hear from you. If you have any questions or run into issues, don't hesitate to reach out to us.

Thanks for building with Eyeson!