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.
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:Writepermission and your account ID.
Step 1: Create a Cloudflare live input
Using the dashboard
- Log in to dash.cloudflare.com and open Build → Images & Stream → Live Inputs.
- Click Create live input.
- 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.
- Confirm with Create live input.
- Open the Broadcast tab, scroll to the WebRTC section and copy the WebRTC (WHIP) URL.
- 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
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
- node
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"
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/live_inputs`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${cloudflareApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta: { name: 'Eyeson Meeting' },
enabled: true,
recording: { mode: 'off' },
}),
}
);
const { result } = await response.json();
const inputUid = result.uid; // live input identifier
const publishUrl = result.webRTC.url; // WHIP -> hand to the Eyeson forward
const playbackUrl = result.webRTCPlayback.url; // WHEP -> hand to your player
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:
| Field | Use |
|---|---|
result.uid | Identifies the live input. Keep it to update or delete the input later. |
result.webRTC.url | The WHIP publish URL. This is the url of the Eyeson forward. Secret — server-side only. |
result.webRTCPlayback.url | The WHEP playback URL. Safe to hand to viewers. |
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
- node sdk
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"
import Eyeson from '@eyeson/node';
const apiKey = '...';
const eyeson = new Eyeson({ apiKey });
const meeting = await eyeson.join('John Doe', null, {
name: 'Cloudflare Meeting',
options: {
sfu_mode: 'disabled',
widescreen: true,
},
});
await meeting.waitReady();
Two options are worth explaining:
sfu_mode: disabledkeeps the meeting on the MCU - single composed One View stream - you are about to forward.widescreen: truegives you a 16:9 output — the aspect ratio every player and viewer expects. If set tofalse, the aspect ratio is 4:3.
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
- node sdk
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"
const forward = eyeson.createRoomForward(meeting.roomId);
await forward.mcu('cloudflare', 'audio,video', cloudflarePublishUrl);
Note that the forward endpoint takes the ROOM_ID, not the access key used elsewhere in the API.
The forward_id — cloudflare 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>
// 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();
}
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.
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
- node sdk
curl -X DELETE \
-H "Authorization: $API_KEY" \
"https://api.eyeson.team/rooms/$ROOM_ID/forward/cloudflare"
await forward.stop('cloudflare');
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
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 differentforward_ideach time. - Audio or video only. Set
typetoaudioorvideoif 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!