Skip to main content

Quick Start Guide

In the quick start guide we learn how to connect to the Metered Global Cloud SFU, publish a track and we will subscribe to our published track.

In the quick start guide we will use JavaScript, but steps are similar for any other programming language, it will work on any platform that supports WebRTC and HTTP.

The connection flow is as follows:

  1. Establishing a Connection:

    • Create a peerConnection using the native WebRTC API.
    • Send the local SDP (Session Description Protocol) offer to the SFU via an HTTP API request.
    • Receive the SFU's remote SDP in response and set it on the peerConnection.
    • The connection is now established.
  2. Subscribing to a Track:

    • Send an HTTP request to the SFU with the trackId and the remoteSessionId of the track you wish to subscribe to.
    • Receive the remote SDP corresponding to the track in response.
    • Set the received SDP on your peerConnection. The native ontrack event will trigger, delivering the remote track.
  3. Publishing a Track:

    • Add the desired media track to the peerConnection.
    • Generate the local SDP offer and send it to the SFU via an HTTP request.
    • Receive the SFU's remote SDP and set it on your peerConnection.

For this we have very simple API, the endpoint is https://global.sfu.metered.ca and when you will send a request to this endpoint you will be automatically routed to the SFU nearest to your location.

  • Create Session API
  • Publish Track API
  • Subscribe Track API
  • Fetch Sessions API
  • Fetch Tracks API

Let's begin building our echo application:

Step 1: Create a Metered Cloud SFU App

Before we can establish a session with the Metered Cloud SFU, we need to create an app. All sessions that we create will be restricted to that app. We can use the app to scope the sessions.

You can create multiple SFU Applications, for example, for dev/staging/production environments, or if you have a multi-tenant application, you can create an SFU application for each of your customers.

Step 1: Create a Metered Cloud SFU App

Go to the Dashboard -> Global SFU and then click on the "Add SFU App" button to add a SFU app, after adding the app you will see it on the dashboard.

SFU App and Secret

Take a not of the SFU App ID and Secret as we would need this to make the HTTP Request.

Step 2: Connect to the SFU

To connect to the Global SFU, we would need to create a peerConnection and then call the "Create Session" API and send the offer SDP.

Create a file called as echo.html and in the file add the following contents

echo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo Application</title>
</head>
<body>
<h1>Metered Cloud SFU Echo Application</h1>
<video id="localVideo" autoplay playsinline muted></video>
<div id="remoteVideoContainer">
</div>

<button id="startButton">Start</button>
<script>
// JavaScript code will go here
</script>
</body>
</html>

We will write our code inside the script tag. To establish the connection with SFU we will create a peerConnection

// Creating a peerConnection object and adding the STUN Server.
// Only STUN Server is need to connect to the SFU, TURN Server
// is not needed to connect to the Metered Global SFU.
const peerConnectionA = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.metered.ca:80"
}
]
});

Then to broadcast the audio or video streams, we can add them to the peerConnection now, adding them to the peerConnection will not allow us to associate metadata with the tracks.

We can associate metadata with the tracks to identify them, if we do not wish to do so we can add the tracks now otherwise we can add the tracks later.

If you wish the add the track later or want to create a connection to only receive the video or audio, you should call the addTransceiver method, otherwise the connection will not get established.

peerConnectionA.addTransceiver('video');

Now let's create an offer sdp

const offerA = await peerConnectionA.createOffer()
await peerConnectionA.setLocalDescription(offerA);

After creating the offer sdp, we will send to the offer to Global Cloud SFU and receive the remote sdp from the SFU that we will add to our peerConnection.

We will call the "Create Session API", the "Create Session API" requires the sfuAppId, secret which we have obtained in the previous step where we have created the app using the dashboard.

// Saving the API Host in a variable
const host = "https://global.sfu.metered.ca"
// Our SFU App ID that we have obtained from the previous step
const sfuAppId = "66ad57c7f9a530a04920b5cf"
// Our SFU App Secret that we have obtained from the previous step
const secret = "43wJtcPext1cmlfx";

// Creating the session by sending the SDP to the SFU
// In the response we will get the session_id which will be automatically generated
// to represent this unique session, as well as the remote sdp from the SFU
const responseSessionA = await fetch(host + `/api/sfu/${sfuAppId}/session/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
sessionDescription: offerA
})
});

Next we will parse the response of the create session request, and extract the sessionId and the remote sdp from response.

// Parse the JSON response from the SFU server
const responseJsonA = await responseSessionA.json();
console.log("SFU Session Created:", responseJsonA);
const sessionIdA = responseJsonA.sessionId;
// Setting the remote sdp received from the sfu
await peerConnectionA.setRemoteDescription(responseJsonA.sessionDescription);
// Adding an event listener to monitor changes in the ICE connection state.
// When the connection state changes to 'connected', log a message indicating that User A is connected.
peerConnectionA.oniceconnectionstatechange = () => {
if (peerConnectionA.iceConnectionState === 'connected') {
console.log('User A Connected');
}
}

That's it! We are now connected to the Metered Global SFU.

Step 3: Publishing tracks to the SFU

Now let's publish the track to the SFU, to publish the track we will get the tracks from user's webcam using the getUserMedia method.

const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
// we can also show the local video to the user
document.getElementById('localVideo').srcObject = stream;

After adding the track to the peerConnection we will generate a new SDP and send the SDP to the publish track api.

const transceiverA = peerConnection.addTransceiver(stream.getVideoTracks()[0], {
direction: 'sendonly'
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);

const response = await fetch(
`${host}/api/sfu/${sfuAppId}/session/${sessionIdA}/track/publish`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
tracks: [{
"trackId": transceiverA.sender.track.id,
"mid": transceiverA.mid,
"customTrackName": "userA"
}],
sessionDescription: offer
})
});

Now we will add the sdp that we have received as the response from the publish track api as the remote descriptor to the peerConnection.

Step 4: Subscribing to the tracks

Next, we will explore how to subscribe to the tracks we have previously published. To simulate this we will create a new peerConnection simulating an another user who will be subscribing to our tracks.

We could technically call subscribe to the tracks in the same peerConnection, so the same peerConnection is publishing the track to the sfu and then fetching it back from the sfu, but it is not practical in real-world, in the real-world some other user would be subscribing to the track, and they would have their own unique peerConnection and sessionId, so to show that we will create a new session and peerConnection.

The code below is the same as the code in Step 2, we will creating a new peerConnection.

const peerConnectionB = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.metered.ca:80"
}
]
});

peerConnectionB.addTransceiver('video', { direction: 'recvonly' });

const offerB = await peerConnectionB.createOffer();
await peerConnectionB.setLocalDescription(offerB);

const responseSessionB = await fetch(host + `/api/sfu/${sfuAppId}/session/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
sessionDescription: offerB
})
});

const responseJsonB = await responseSessionB.json();
const sessionIdB = responseJsonB.sessionId;
await peerConnectionB.setRemoteDescription(responseJsonB.sessionDescription);

peerConnectionB.oniceconnectionstatechange = () => {
if (peerConnectionA.iceConnectionState === 'connected') {
console.log('User A Connected');
}
}

// Listening on the ontrack event to show the remote track
peerConnectionB.ontrack = (e) => {
const videoElement = document.createElement('video');
videoElement.srcObject = new MediaStream([e.track]);
videoElement.autoplay = true;
videoElement.controls = true;
document.getElementById('remoteVideoContainer').appendChild(videoElement);
}

All the above code is similar to the step 2, where we have created a peerConnection and connected to the sfu, the above code is the same, next we will look at the actual part regarding how to subscribe to a track.

  • We will use the "Fetch Tracks" API to fetch all the active tracks for a sessionId
  • Then we will call the "Subscribe Track" API to subscribe to that track.
// Get the list of tracks from the Remote SFU for userA
// The fetch function sends a GET request to the SFU server to retrieve the list of tracks for the session
// The response from the SFU server contains the list of tracks
const remoteTracksSessionA = await fetch(host + `/api/sfu/${sfuAppId}/session/${sessionIdA}/tracks`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
}
});

// Parse the JSON response from the SFU server
const remoteTracksJsonSessionA = await remoteTracksSessionA.json();
console.log(remoteTracksJsonSessionA);
// Extracting the trackId
const trackIdA = remoteTracksJsonSessionA[0].trackId;

We will now call the subscribe track api, and pass the "Session ID A" and the trackId to the API, the API will return a SDP, that we will add to our peerConnection.

const subscribeTrackResponseB = await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdB}/track/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
tracks: [
{
"remoteSessionId": sessionIdA,
"remoteTrackId": trackIdA
}
]
})
});

// Parse the JSON response from the SFU server
const jsonSubscribeTrackResponse = await subscribeTrackResponseB.json();

We have saved the response in the variable jsonSubscribeTrackResponse and we will extract the sdp from the response and add it our peerConnection

// Set the remote description for userB's peer connection
// The setRemoteDescription function sets the remote description of the peer connection to the session description received from the SFU server
await peerConnectionUserB.setRemoteDescription(new RTCSessionDescription(jsonUserB.sessionDescription));

One final step we need to do is generate the local SDP and send it back to the SFU to regnegotiate.


const answerUserB = await peerConnectionUserB.createAnswer();
await peerConnectionUserB.setLocalDescription(answerUserB);

// Renegotiate the session for userB
// The fetch function sends a PUT request to the SFU server to renegotiate the session
// The request body contains the session description generated by the createAnswer function
await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdUserB}/renegotiate`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
sessionDescription: answerUserB
})
});

That's it! Now the ontrack event will be fired on the peerConnection and a video tag will appear on the page showing the video.

// Listening on the ontrack event to show the remote track
peerConnectionB.ontrack = (e) => {
const videoElement = document.createElement('video');
videoElement.srcObject = new MediaStream([e.track]);
videoElement.autoplay = true;
videoElement.controls = true;
document.getElementById('remoteVideoContainer').appendChild(videoElement);
}

Putting it all together here is the complete code:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Metered Cloud SFU Echo Example</title>
</head>
<body>

<strong>Local User</strong>
<p>
Displaying the live camera feed of the local user. This video feed will be sent to the Metered Global Cloud SFU.
</p>
<!-- In this video tag, we will show the user's local video -->
<video id="localVideo" width="320" height="240" muted autoplay></video>

<strong>Remote Video</strong>
<p>
We will create a new session and subscribe to the video feed published above. This video feed will come from the Metered Global Cloud SFU and will simulate a remote user viewing the video feed.
</p>
<!-- In this div, we will add the remote video coming from the SFU -->
<div id="remoteVideoContainer">
</div>

<script>
(async () => {
// Define the host URL and authentication details for the SFU
// The host URL is the endpoint for the SFU server
// The secret is the authentication token required to interact with the SFU server
// The sfuAppId is the unique identifier for the SFU application
const host = "https://global.sfu.metered.ca";
const sfuAppId = "66ad57c7f9a530a04920b5cf";
const secret = "43wJtcPext1cmlfx";

// Creating a PeerConnection for userA to connect to the SFU
// The RTCPeerConnection is used to establish a connection to the SFU
// The iceServers array contains the STUN server configuration used for NAT traversal
const peerConnectionUserA = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.metered.ca:80"
}
]
});

// Request access to userA's video device (e.g., webcam)
// The getUserMedia function prompts the user for permission to use their video device
// The video constraints specify the desired resolution for the video feed
const streamUserA = await navigator.mediaDevices.getUserMedia({
video: {
width: 1920,
height: 1080,
}
});

// Add each track from the stream to the peer connection to be sent to the SFU
// The getTracks function returns an array of MediaStreamTrack objects representing the video tracks
// The addTrack function adds each track to the peer connection
streamUserA.getTracks().map(track => peerConnectionUserA.addTrack(track, streamUserA));

// Showing userA's local video
// The srcObject property of the video element is set to the MediaStream object
// This displays the local video feed in the video element with the id 'localVideo'
document.getElementById('localVideo').srcObject = streamUserA;

// Create an SDP offer for userA
// The createOffer function generates an SDP offer for the peer connection
// The setLocalDescription function sets the local description of the peer connection to the generated offer
const offerSdpUserA = await peerConnectionUserA.createOffer();
await peerConnectionUserA.setLocalDescription(offerSdpUserA);

// Send the SDP offer to the SFU to establish a connection for userA
// The fetch function sends a POST request to the SFU server with the SDP offer in the request body
// The response from the SFU server contains the session description and session ID
const responseUserA = await fetch(host + `/api/sfu/${sfuAppId}/session/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
sessionDescription: offerSdpUserA
})
});

// Parse the JSON response from the SFU server
const responseJsonUserA = await responseUserA.json();
console.log(responseJsonUserA);

// Obtain the session ID for userA
// The session ID is used to identify the session on the SFU server
const sessionIdUserA = responseJsonUserA.sessionId;

// Set the remote description for userA's peer connection
// The setRemoteDescription function sets the remote description of the peer connection to the session description received from the SFU server
await peerConnectionUserA.setRemoteDescription(responseJsonUserA.sessionDescription);

// Check if the peer connection state is connected for userA
// The oniceconnectionstatechange event is triggered when the ICE connection state changes
// If the ICE connection state is 'connected', the startRemoteUserConnection function is called after a 1-second delay
peerConnectionUserA.oniceconnectionstatechange = () => {
if (peerConnectionUserA.iceConnectionState === 'connected') {
console.log('UserA Connected');
setTimeout(startRemoteUserConnection, 1000);
}
}

/**
* Function to create a new session and subscribe to the track published by userA
*/
async function startRemoteUserConnection() {
// Creating a PeerConnection for userB to connect to the SFU
// The RTCPeerConnection is used to establish a connection to the SFU
// The iceServers array contains the STUN server configuration used for NAT traversal
const peerConnectionUserB = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun.metered.ca:80"
}
]
});

// Add a transceiver for video for userB
// The addTransceiver function adds a transceiver for the video track
// This allows userB to receive the video track from the SFU
peerConnectionUserB.addTransceiver('video');

// Create an SDP offer for userB
// The createOffer function generates an SDP offer for the peer connection
// The setLocalDescription function sets the local description of the peer connection to the generated offer
const offerSdpUserB = await peerConnectionUserB.createOffer();
await peerConnectionUserB.setLocalDescription(offerSdpUserB);

// Handle incoming tracks for userB
// The ontrack event is triggered when a new track is received on the peer connection
// A new video element is created for each incoming track and added to the 'remoteVideoContainer' div
peerConnectionUserB.ontrack = (e) => {
const videoElement = document.createElement('video');
videoElement.srcObject = new MediaStream([e.track]);
videoElement.autoplay = true;
videoElement.controls = true;
document.getElementById('remoteVideoContainer').appendChild(videoElement);
}

// Send the SDP offer to the SFU to establish a connection for userB
// The fetch function sends a POST request to the SFU server with the SDP offer in the request body
// The response from the SFU server contains the session description and session ID
const sessionResponseUserB = await fetch(host + `/api/sfu/${sfuAppId}/session/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
sessionDescription: offerSdpUserB
})
});

// Parse the JSON response from the SFU server
const sessionResponseJsonUserB = await sessionResponseUserB.json();
const sessionIdUserB = sessionResponseJsonUserB.sessionId;
await peerConnectionUserB.setRemoteDescription(sessionResponseJsonUserB.sessionDescription);

// Get the list of tracks from the Remote SFU for userA
// The fetch function sends a GET request to the SFU server to retrieve the list of tracks for the session
// The response from the SFU server contains the list of tracks
const remoteTracksUserA = await fetch(host + `/api/sfu/${sfuAppId}/session/${sessionIdUserA}/tracks`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
}
});

// Parse the JSON response from the SFU server
const remoteTracksJsonUserA = await remoteTracksUserA.json();
console.log(remoteTracksJsonUserA);

// Subscribe to the remote tracks for userB
// The trackIdUserA is the unique identifier for the track published by userA
// The fetch function sends a POST request to the SFU server to subscribe to the track
// The request body contains the remote session ID and remote track ID
const trackIdUserA = remoteTracksJsonUserA[0].trackId;
const responseUserB = await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdUserB}/track/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
tracks: [
{
"remoteSessionId": sessionIdUserA,
"remoteTrackId": trackIdUserA
}
]
})
});

// Parse the JSON response from the SFU server
const jsonUserB = await responseUserB.json();

// Set the remote description for userB's peer connection
// The setRemoteDescription function sets the remote description of the peer connection to the session description received from the SFU server
await peerConnectionUserB.setRemoteDescription(new RTCSessionDescription(jsonUserB.sessionDescription));
const answerUserB = await peerConnectionUserB.createAnswer();
await peerConnectionUserB.setLocalDescription(answerUserB);

// Renegotiate the session for userB
// The fetch function sends a PUT request to the SFU server to renegotiate the session
// The request body contains the session description generated by the createAnswer function
await fetch(`${host}/api/sfu/${sfuAppId}/session/${sessionIdUserB}/renegotiate`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${secret}`
},
body: JSON.stringify({
sessionDescription: answerUserB
})
});
}
})();
</script>

</body>
</html>