Skip to content
Welcome to the new, unified Livepeer documentation! 👋
Guides
Stream from the Browser

Stream from the Browser

We demonstrate below how to broadcast from the web using Livepeer's low latency WebRTC broadcast.

Using Livepeer.js Broadcast

The example below show to use Livepeer.js Broadcast to broadcast from the web.

Configuring Providers

First, we create a new livepeer.js client with a gateway provider and a CORS-protected API key, as well as a theme to use for all livepeer.js React components.

App.tsx
import {
  LivepeerConfig,
  createReactClient,
  studioProvider,
} from '@livepeer/react';
import * as React from 'react';
 
const livepeerClient = createReactClient({
  provider: studioProvider({
    apiKey: process.env.NEXT_PUBLIC_STUDIO_API_KEY,
  }),
});
 
function App() {
  return (
    <LivepeerConfig client={livepeerClient}>
      <CreateAndViewAsset />
    </LivepeerConfig>
  );
}

Broadcast

Now that our providers are set up, we use the Broadcast with a stream key, from a stream we created. We also override some of the custom styling to match the flow of our app!

DemoBroadcast.tsx
import { Broadcast } from '@livepeer/react';
 
const streamKey = 'your-stream-key';
 
export const DemoBroadcast = () => {
  return (
    <Broadcast
      streamKey={streamKey}
      controls={{
        autohide: 3000,
      }}
      theme={{
        borderStyles: { containerBorderStyle: 'hidden' },
        radii: { containerBorderRadius: '10px' },
      }}
    />
  );
};

Embeddable Broadcast

⚠️

The embeddable broadcast is currently in beta and some elements may change as we mature the product. For a production-grade application consider using Livepeer.js instead.

This is one of the easiest way 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; picture-in-picture"
  sandbox="allow-same-origin allow-scripts"
>
</iframe>

This will automatically stream from the browser, using the STUN/TURN servers to ensure that network connectivity issues are rare.

Adding broadcasting with plain WebRTC

Using livepeer.js is the recommended way to add easy broadcasting via WebRTC. If you want to add broadcasting to your app and handle the WebRTC negotiation, 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 GET request to the WebRTC redirect endpoint:

curl -L 'https://livepeer.studio/webrtc/{STREAM_KEY}'
...
> Location: https://lax-prod-catalyst-2.lp-playback.studio/webrtc/{STREAM_KEY}
⚠️

You may safely ignore any errors returned from the GET request - we are only interested in getting the redirect URL from the final response, so that we can set up the correct ICE servers.

We know from the above response headers that the WebRTC ingest URL 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 GET 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 (opens in a new tab) and kick off a livestream.

The outline of the steps are:

  1. Create a new RTCPeerConnection with the ICE servers from the redirect URL.
  2. Construct an SDP offer using the library of your choice.
  3. Wait for ICE gathering.
  4. Send the SDP offer to the server and get the response.
  5. Use the response to set the remote description on the RTCPeerConnection.
  6. 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 webcam source and started a broadcast!

To make the above process clearer, here is the flow (credit to the authors of the WHIP spec):

WHIP Outline
+-----------------+         +---------------+ +--------------+ +----------------+
 | 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

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 broadcaster.