We demonstrate below how to
broadcast from the web
using Livepeer’s low latency WebRTC broadcast. Developers can either use the
Livepeer Broadcast React component, or build their own WebRTC solution.
Using UI Kit Broadcast
The example below shows how to use the Livepeer UI Kit
Broadcast
component to broadcast from the web.
Broadcast
This guide assumes you have configured a Livepeer JS SDK client with an API key.
We can use the Broadcast
primitives with a
stream key, from a stream we created.
We show some simple styling below with Tailwind CSS, but this can use any
styling library, since the primitives ship as unstyled, composable components.
import { EnableVideoIcon, StopIcon } from "@livepeer/react/assets";
import * as Broadcast from "@livepeer/react/broadcast";
import { getIngest } from "@livepeer/react/external";
const streamKey = "your-stream-key";
export const DemoBroadcast = () => {
return (
<Broadcast.Root ingestUrl={getIngest(streamKey)}>
<Broadcast.Container className="h-full w-full bg-gray-950">
<Broadcast.Video title="Current livestream" className="h-full w-full" />
<Broadcast.Controls className="flex items-center justify-center">
<Broadcast.EnabledTrigger className="w-10 h-10 hover:scale-105 flex-shrink-0">
<Broadcast.EnabledIndicator asChild matcher={false}>
<EnableVideoIcon className="w-full h-full" />
</Broadcast.EnabledIndicator>
<Broadcast.EnabledIndicator asChild>
<StopIcon className="w-full h-full" />
</Broadcast.EnabledIndicator>
</Broadcast.EnabledTrigger>
</Broadcast.Controls>
<Broadcast.LoadingIndicator asChild matcher={false}>
<div className="absolute overflow-hidden py-1 px-2 rounded-full top-1 left-1 bg-black/50 flex items-center backdrop-blur">
<Broadcast.StatusIndicator
matcher="live"
className="flex gap-2 items-center"
>
<div className="bg-red-500 animate-pulse h-1.5 w-1.5 rounded-full" />
<span className="text-xs select-none">LIVE</span>
</Broadcast.StatusIndicator>
<Broadcast.StatusIndicator
className="flex gap-2 items-center"
matcher="pending"
>
<div className="bg-white/80 h-1.5 w-1.5 rounded-full animate-pulse" />
<span className="text-xs select-none">LOADING</span>
</Broadcast.StatusIndicator>
<Broadcast.StatusIndicator
className="flex gap-2 items-center"
matcher="idle"
>
<div className="bg-white/80 h-1.5 w-1.5 rounded-full" />
<span className="text-xs select-none">IDLE</span>
</Broadcast.StatusIndicator>
</div>
</Broadcast.LoadingIndicator>
</Broadcast.Container>
</Broadcast.Root>
);
};
Embeddable broadcast
This is one of the easiest ways to broadcast video from your
website/applications. You can embed the iframe on your website/applications by
using the below code snippet.
You can replace the STREAM_KEY
with your stream key for the stream.
<iframe
src="https://lvpr.tv/broadcast/{STREAM_KEY}"
allowfullscreen
allow="autoplay; encrypted-media; fullscreen; picture-in-picture; display-capture; camera; microphone"
frameborder="0"
>
</iframe>
This will automatically stream from the browser with a fully composed UI, using
STUN/TURN servers to avoid network firewall issues.
Adding custom broadcasting
If you want to add custom broadcasting to your app and handle the WebRTC SDP
negotiation without using the Livepeer React primitives, you can follow the
steps below.
Get the SDP Host
First, you will need to make a request to get the proper ingest URL for the
region which your end user is in. We have a global presence, and we handle
redirects based on GeoDNS to allow users to get the lowest latency server.
To do this make a HEAD
request to the WebRTC redirect endpoint:
curl -I 'https://livepeer.studio/webrtc/{STREAM_KEY}'
...
> Location: https://lax-prod-catalyst-2.lp-playback.studio/webrtc/{STREAM_KEY}
We are only interested in getting the redirect URL from the response, so that
we can set up the correct ICE servers.
From the above response headers, the best WebRTC ingest URL for the user is
https://lax-prod-catalyst-2.lp-playback.studio/webrtc/{STREAM_KEY}
. We will
use this in the next step.
The process will change in the future to remove the need for this extraneous
HEAD
request - please check back later.
Broadcast
Now that we have the endpoint for the ICE servers, we can start SDP negotiation
following the
WHIP spec and
kick off a livestream.
The outline of the steps are:
- Create a new
RTCPeerConnection
with the ICE servers from the redirect URL.
- Construct an SDP offer using the library of your choice.
- Wait for ICE gathering.
- Send the SDP offer to the server and get the response.
- Use the response to set the remote description on the
RTCPeerConnection
.
- Get a local media stream and add the track to the peer connection, and set
the video element
src
to the srcObject
.
// the redirect URL from the above GET request
const redirectUrl = `https://lax-prod-catalyst-2.lp-playback.studio/webrtc/{STREAM_KEY}`;
// we use the host from the redirect URL in the ICE server configuration
const host = new URL(redirectUrl).host;
const iceServers = [
{
urls: `stun:${host}`,
},
{
urls: `turn:${host}`,
username: "livepeer",
credential: "livepeer",
},
];
// get user media from the browser (which are camera/audio sources)
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
const peerConnection = new RTCPeerConnection({ iceServers });
// set the media stream on the video element
element.srcObject = mediaStream;
const newVideoTrack = mediaStream?.getVideoTracks?.()?.[0] ?? null;
const newAudioTrack = mediaStream?.getAudioTracks?.()?.[0] ?? null;
if (newVideoTrack) {
videoTransceiver =
peerConnection?.addTransceiver(newVideoTrack, {
direction: "sendonly",
}) ?? null;
}
if (newAudioTrack) {
audioTransceiver =
peerConnection?.addTransceiver(newAudioTrack, {
direction: "sendonly",
}) ?? null;
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer
* We create an SDP offer here which will be shared with the server
*/
const offer = await peerConnection.createOffer();
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */
await peerConnection.setLocalDescription(offer);
/** Wait for ICE gathering to complete */
const ofr = await new Promise((resolve) => {
/** Wait at most five seconds for ICE gathering. */
setTimeout(() => {
resolve(peerConnection.localDescription);
}, 5000);
peerConnection.onicegatheringstatechange = (_ev) => {
if (peerConnection.iceGatheringState === "complete") {
resolve(peerConnection.localDescription);
}
};
});
if (!ofr) {
throw Error("failed to gather ICE candidates for offer");
}
/**
* This response contains the server's SDP offer.
* This specifies how the client should communicate,
* and what kind of media client and server have negotiated to exchange.
*/
const sdpResponse = await fetch(redirectUrl, {
method: "POST",
mode: "cors",
headers: {
"content-type": "application/sdp",
},
body: ofr.sdp,
});
if (sdpResponse.ok) {
const answerSDP = await sdpResponse.text();
await peerConnection.setRemoteDescription(
new RTCSessionDescription({ type: "answer", sdp: answerSDP })
);
}
We just negotiated following the WHIP spec (which outlines the structure for the
POST requests seen above) and we did SDP negotiation to create a new livestream.
We then retrieved a local camera source and started a broadcast!
To make the above process clearer, here is the flow (credit to the authors of
the WHIP spec):
The final HTTP DELETE is not needed for our media server, since we detect the
end of broadcast by the lack of incoming packets from the gateway.
+-----------------+ +---------------+ +--------------+ +----------------+
| WebRTC Producer | | WHIP endpoint | | Media Server | | WHIP Resource |
+---------+-------+ +-------+- -----+ +------+-------+ +--------|-------+
| | | |
| | | |
|HTTP POST (SDP Offer) | | |
+------------------------>+ | |
|201 Created (SDP answer) | | |
+<------------------------+ | |
| ICE REQUEST | |
+----------------------------------------->+ |
| ICE RESPONSE | |
<------------------------------------------+ |
| DTLS SETUP | |
<==========================================> |
| RTP/RTCP FLOW | |
+------------------------------------------> |
| HTTP DELETE |
+------------------------------------------------------------>+
| 200 OK |
<-------------------------------------------------------------x