Did you know? All Video & Audio API plans include a $100 free usage credit each month so you can build and test risk-free. View Plans ->

WebRTC For The Brave

Create a Video Calling Page

In this part, you'll learn how to combine the peer connection logic and render it on your web browser to create a video calling page.

Create a Video Call Page

You’ve learned all essential concepts to implement a video call, such as RTCPeerConnection API, SDP message exchanges, ICE candidates to establish a peer connection. Now let’s create a HTML page that renders video streams between a local peer and a remote peer.

First, create an index.html HTML file that contains two video tags to render a local peer and a remote peer and button tags to control video actions like the example below:

html
            <video id="localVideo" playsinline autoplay muted></video>
<video id="remoteVideo" playsinline autoplay></video>

<div class="box">
    <button id="call_pc1">Call PC1</button>
    <button id="call_pc2">Call PC2</button>
    <button id="disconnect">Disconnect</button>
</div>
        

Next, you should import an additional script, adapter-latest.js. The adapter.js is a shim to insulate apps from spec changes and prefix differences. In fact, the standards and protocols used for WebRTC implementations are highly stable now, so there are only a few prefix differences and you can resolve it by using this script:

html
            <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        

Finally, the index.html HTML file should now look like this:

html
            <!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
    <meta id="theme-color" name="theme-color" content="#ffffff">

    <title>Peer connection</title>

    <link rel="stylesheet" href="./css/main.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-blue.min.css">
    <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
</head>

<body>

<div id="container">
    <h4><a href="https://github.com/GetStream/webrtc-for-the-brave" title="WebRTC samples homepage">WebRTC samples</a>
        <span>Peer connection</span>
    </h4>

    <video id="localVideo" playsinline autoplay muted></video>
    <video id="remoteVideo" playsinline autoplay></video>

    <div class="box">
        <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent" id="call_pc1">Call PC1</button>
        <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent" id="call_pc2">Call PC2</button>
        <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent" id="disconnect">Disconnect</button>
    </div>

    <div id="errorMsg"></div>
</div>

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js" async></script>

</body>
</html>
        

Complete a Peer Connection

Now let’s put together the creating a peer connection, exchanging SDP messages and ICE candidates, and finally establishing a peer connection like the example below:

jsx
            'use strict';

const callPc1Button = document.getElementById('call_pc1');
const callPc2Button = document.getElementById('call_pc2');
const disconnectButton = document.getElementById('disconnect');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');

callPc1Button.addEventListener('click', attachLocalMedia);
callPc2Button.addEventListener('click', peerConnection);
disconnectButton.addEventListener('click', disconnect);
callPc2Button.disabled = true;
disconnectButton.disabled = true;

let localStream, pc1, pc2;

// Define video constraints
const videoConstraints = {
    audio: false,
    video: {width: 1280, height: 720}
};

async function attachLocalMedia() {
    callPc1Button.disabled = true;
    try {
        const stream = await navigator.mediaDevices.getUserMedia(videoConstraints);
        localVideo.srcObject = stream;
        localStream = stream;
        callPc2Button.disabled = false;
    } catch (e) {
        onCatch(e)
    }
}

async function peerConnection() {
    callPc2Button.disabled = true;
    disconnectButton.disabled = false;

    pc1 = new RTCPeerConnection();
    pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e));
    pc1.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc1, e));

    pc2 = new RTCPeerConnection();
    pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
    pc2.addEventListener('iceconnectionstatechange', e => onIceStateChange(pc2, e));
    pc2.addEventListener('track', gotRemoteStream);

    localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));

    try {
        console.log('pc1 createOffer start');
        const offer = await pc1.createOffer({
            iceRestart: true,
            offerToReceiveAudio: true,
            offerToReceiveVideo: true
        });
        await onCreateOfferSuccess(offer);
    } catch (e) {
        onCatch(e);
    }
}

async function onCreateOfferSuccess(desc) {
    console.log(`Offer from pc1\nsdp: ${desc.sdp}`);
    try {
        await pc1.setLocalDescription(desc);
    } catch (e) {
        onCatch(e)
    }

    try {
        await pc2.setRemoteDescription(desc);
    } catch (e) {
        onCatch(e)
    }

    try {
        const answer = await pc2.createAnswer();
        await onCreateAnswerSuccess(answer);
    } catch (e) {
        onCatch(e);
    }
}

function gotRemoteStream(e) {
    if (remoteVideo.srcObject !== e.streams[0]) {
        remoteVideo.srcObject = e.streams[0];
    }
}

async function onCreateAnswerSuccess(desc) {
    try {
        await pc2.setLocalDescription(desc);
    } catch (e) {
        onCatch(e)
    }

    try {
        await pc1.setRemoteDescription(desc);
    } catch (e) {
        onCatch(e)
    }
}

async function onIceCandidate(pc, event) {
    try {
        await (getOtherPc(pc).addIceCandidate(event.candidate));
        console.log(`${getName(pc)} addIceCandidate success`);
    } catch (e) {
        onCatch(pc, e);
    }
    console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);
}

function onIceStateChange(pc, event) {
    if (pc) {
        console.log(`${getName(pc)} ICE state: ${pc.iceConnectionState}`);
        console.log('ICE state change event: ', event);
    }
}

function getName(pc) {
    return (pc === pc1) ? 'pc1' : 'pc2';
}

function getOtherPc(pc) {
    return (pc === pc1) ? pc2 : pc1;
}

// Function to handle errors during media stream acquisition
function onCatch(error) {
    const errorElement = document.querySelector('#errorMsg');
    errorElement.innerHTML += `<p>Something went wrong: ${error.name}</p>`;
}

function disconnect() {
    pc1.close();
    pc2.close();
    pc1 = null;
    pc2 = null;
    localVideo.srcObject = null;
    callPc1Button.disabled = false;
    disconnectButton.disabled = true;
}
        

Let’s break down the functions above one by one:

  • start(): This function will be called when you click the Start button on the web page. It gets a MediaStream and initializes the local video and stream.
  • call(): This function will be called when you click the Call button on the web page. It establishes a peer connection between a local and a remote peer and processes everything you need to create a peer connection, such as exchanging SDP messages and ICE candidates.
  • onCreateOfferSuccess(): This is called to create an offer and set a local description to the local peer. The remote peer sets the offer as a remote description and create an answer. This function is called in the call() function.
  • onCreateAnswerSuccess() : This is called to set an answer as a local description to the remote peer and set the answer as a remote description to the local peer.
  • hangup(): Close peer connections and clear all connect resources.

If you want to apply styles for the demo application, you can download the main.css and save it into the css folder.

Note: You can download the full source code of Lesson 2 on GitHub.

Running The WebRTC Demo Application

If you run the index.html file, you will see the demo below:

Next, click the CALL PC1 button to display a video stream from the local camera. If you click the button, you will see the popup below that asks you to get access permission for your local camera and microphone.

If you click the Allow button on the popup, you will see the real-time video stream screen on your screen like the below:

You are connected to only the local video stream, so another screen that represents a remote peer is black. If you want to create a peer connection for exchanging media data with a remote peer, click the CALL PC2 button, and now you’ll see the video call result below:

If you want to disconnect the peer connections between 2 pcs, you can achieve it by clicking the DISCONNECT button on the page.

This tutorial connects and renders a local peer and a remote peer on the same computer and the browser, so it’s not much practical use, but now you have a better grasp of how WebRTC works.

Note: You can demonstrate and test the sample application about Create a Peer Connection on your web browser without building the project.

Debugging WebRTC in Chrome Browser

If you want to debug your WebRTC application, you can easily debug your WebRTC application on the chrome://webrtc-internals/ page, a WebRTC inspection tool by Chrome.

Firstly, run the Create a Peer Connection example application, and go into the chrome://webrtc-internals/ page, and you will see the inspection page below:

In this tutorial, you’ve learned how to establish a peer connection. Now, let’s learn how to exchange plain messages or media data via a media channel using WebRTC.