How to Build a Live and Video Streaming App With WebRTC

Creating a Live Video Streaming Application

What is WebRTC video streaming?

WebRTC video streaming is a free and open-source project that enables web browsers and mobile devices like iOS and Android to provide real-time communication. This functionality allows app features like peer-to-peer video conferencing to be easily integrated into a web page. With WebRTC video streaming, a browser-based video chat can be engineered rapidly with HTML and JavaScript, with no back-end code required. This is a key component of live audience engagement and multiuser collaboration solutions, enhancing the user experience across platforms such as social media, live video streaming apps, and content delivery networks. WebRTC's adaptability makes it an essential tool for app development, catering to a wide range of applications from Amazon's cloud services to Netflix's video on demand, Twitch's streaming platform, Facebook Live and interactive features on Hulu, Spotify, and Apple devices.

How does WebRTC video streaming work?

WebRTC allows users to stream peer-to-peer audio and video in modern web browsers. This capability is supported by the latest versions of Chrome, FireFox, Edge, Safari, and Opera on desktop, as well as native iOS and Android web browsers. This is the basis of the data streaming solutions provided by PubNub.

Making a user’s device a WebRTC client is as simple as initializing a new RTCPeerConnection() object in front-end JavaScript.

WebRTC Live Streaming Architecture

Video chat is established on two or more client devices using the WebRTC protocol. The connection can be made using one of two modes. The first mode is peer-to-peer, meaning audio and video packets are streamed directly from client to client with RTC Configuration. This setup works as long as both machines have an IP address that is accessible by the public internet.

However, relying on peer-to-peer connections for browser video chat and conferencing is not wise in production apps. It is common for the Interactive Connectivity Establishment or ICE framework to fail to establish a connection between two users when one or both are behind advanced LAN security.

To mitigate this, you can set your RTCConfiguration to first attempt peer-to-peer, and then fall back to relayed connection if peer-to-peer fails.

If publicly accessible IP addresses are not an option, a WebRTC connection must be established over a TURN server. The ICE framework will decide if this is necessary as users are trying to connect.

Don’t Build a WebRTC Signalling Server For Your Live Streaming – Use PubNub

WebRTC leaves out a very important component from video chat streaming. A client must use a signalling service to communicate messages with their peer or peers. PubNub allows a developer to fully, and cheaply, implement functionalities like a WebRTC signalling service. This is facilitated by PubNub's extensive documentation on setting up your account and sending/receiving messages.

Examples of video chat streaming with WebRTC

These messages are for events like:

These messages are part of the Signalling Transaction Flow which is outlined in the Mozilla Developer Network documentation for WebRTC.The WebRTC signalling server is an abstract concept. Many services can become this “signalling server” like WebSockets, Socket.IO or PubNub. If you’re tasked with creating a solution for this, you will end up asking: Should we build or should we buy?

Why PubNub: Logical extensions like one-to-many WebRTC video streaming

PubNub allows a developer like yourself to fully, and cheaply, implement a WebRTC signalling service. An Open Source WebRTC library that uses PubNub is available on GitHub. However, the following PubNub data streaming solution is even more rapid than building with the WebRTC SDK, as our platform enables you to quickly and easily build an application that supports one-to-many streaming at any scale.

Community Supported Package for WebRTC Video Calling

PubNub is like a global CDN for real-time data. Developers can use its IaaS for building high-quality, real-time streaming platforms, mobile apps, and much more. There are PubNub SDKs available for every programming language and device, enabling reliable pub/sub connections, data delivery, and controlling the network; all possible in a few lines of code.

WebRTC Video Streaming App Tutorial with Javascript, HTML, CSS

In this tutorial, we will use JavaScript, HTML, and CSS to build our video chat app. However, if you want to use a modern front-end framework like Vue, React, or Angular, you can check out the updated PubNub tutorials page or the PubNub Chat Resource Center. We also have a substantial development team available for consultation.

To start, you can use the HTML and CSS in my project example. These files present a very generic video chat app user interface. The example app has only 1 global chat, and no private 1:1 chats, although they are easy to implement.

WebRTC video streaming app HTML

Open index.html with your favorite text editor. Replace the script tags beneath the body tag of your HTML file with these 2 CDN scripts. Leave the 3rd script tag that refers to app.js. We will write that file together.

<script type="text/javascript" src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.32.0.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pubnub-js-webrtc@latest/dist/pubnub-js-webrtc.js"></script>

The next step is to create your own app.js file in the same directory as your index.html file. The reason we need to make a new app.js is because the script in my example uses Xirsys. My private account is wired to my Functions server. You will need to make your own back-end server and account if you wish to use a TURN provider like Xirsys. My next blog post will contain a tutorial for building WebRTC apps with TURN.

The app.js script we will write together will use only free peer-to-peer WebRTC connections. If you try to do a live video call with 2 devices on the same LAN, your app will work. It is not certain that a video call connection can be made with clients on separate networks (due to NAT security). This is why it's essential to have a good understanding of the streaming protocols.

WebRTC video streaming app javascript

First, we will make references to all of the DOM elements from the index.html file. Once we can refer to them in our JavaScript code, we can manipulate them programmatically.

const chatInterface = document.getElementById('chat-interface');
const myVideoSample = document.getElementById('my-video-sample');
const myVideo = document.getElementById('my-video');
const remoteVideo = document.getElementById('remote-video');
const videoModal = document.getElementById('video-modal');
const closeVideoButton = document.getElementById('close-video');
const brokenMyVideo = document.getElementById('broken-my-video');
const brokenSampleVideo = document.getElementById('broken-sample-video');
const usernameModal = document.getElementById('username-input-modal');
const usernameInput = document.getElementById('username-input');
const joinButton = document.getElementById('join-button');
const callConfirmModal = document.getElementById('call-confirm-modal');
const callConfirmUsername = document.getElementById('call-confirm-username');
const yesCallButton = document.getElementById('yes-call');
const noCallButton = document.getElementById('no-call');
const incomingCallModal = document.getElementById('incoming-call-modal');
const callFromSpan = document.getElementById('call-from');
const acceptCallButton = document.getElementById('accept-call');
const rejectCallButton = document.getElementById('reject-call');
const onlineList = document.getElementById('online-list');
const chat = document.getElementById('chat');
const log = document.getElementById('log');
const messageInput = document.getElementById('message-input');
const submit = document.getElementById('submit');

Next, we will add some variables that hold a CSS class name, global app information, and WebRTC configuration info. In the RTCConfiguration dictionary, we add STUN and TURN server information for WebRTC calling. This is a crucial step for high-quality video content in your streaming service.

const hide = 'hide';
// PubNub Channel for sending/receiving global chat messages
//     also used for user presence with Presence
const globalChannel = 'global-channel';
let webRtcPhone;
let pubnub;
// An RTCConfiguration dictionary from the browser WebRTC API
// Add STUN and TURN server information here for WebRTC calling
const rtcConfig = {};
let username; // User's name in the app
let myAudioVideoStream; // Local audio and video stream
let noVideoTimeout; // Used to check if a video connection succeeded
const noVideoTimeoutMS = 5000; // Error alert if the video fails to connect

Now we will get into some of the imperative client code for the WebRTC package functionality. This is where the real-time aspect of your video streaming platform comes into play.

// Init the audio and video stream on this client
getLocalStream().then((localMediaStream) => {
    myAudioVideoStream = localMediaStream;
    myVideoSample.srcObject = myAudioVideoStream;
    myVideo.srcObject = myAudioVideoStream;
}).catch(() => {
    myVideo.classList.add(hide);
    myVideoSample.classList.add(hide);
    brokenMyVideo.classList.remove(hide);
    brokenSampleVideo.classList.remove(hide);
});
// Prompt the user for a username input
getLocalUserName().then((myUsername) => {
    username = myUsername;
    usernameModal.classList.add(hide);
    initWebRtcApp();
});
// Send a chat message when Enter key is pressed
messageInput.addEventListener('keydown', (event) => {
    if (event.keyCode === 13 && !event.shiftKey) {
        event.preventDefault();
        sendMessage();
        return;
    }
});
// Send a chat message when the submit button is clicked
submit.addEventListener('click', sendMessage);
const closeVideoEventHandler = (event) => {
    videoModal.classList.add(hide);
    chatInterface.classList.remove(hide);
    clearTimeout(noVideoTimeout);
    webRtcPhone.disconnect(); // disconnects the current phone call
}
// Register a disconnect event handler when the close video button is clicked
closeVideoButton.addEventListener('click', closeVideoEventHandler);

The new code that we just added:

Next, we are going to add the initialization code for the WebRTC portion of the web application. In this part, we initialize our PubNub instance with the latest SDK version 4.32.0.

const initWebRtcApp = () => {
    // WebRTC phone object event for when the remote peer's video becomes available.
    const onPeerStream = (webRTCTrackEvent) => {
        console.log('Peer audio/video stream now available');
        const peerStream = webRTCTrackEvent.streams[0];
        window.peerStream = peerStream;
        remoteVideo.srcObject = peerStream;
    };
    // WebRTC phone object event for when a remote peer attempts to call you.
    const onIncomingCall = (fromUuid, callResponseCallback) => {
        let username = document.getElementById(fromUuid).children[1].innerText;
        incomingCall(username).then((acceptedCall) => {
            if (acceptedCall) {
                // End an already open call before opening a new one
                webRtcPhone.disconnect();
                videoModal.classList.remove(hide);
                chatInterface.classList.add(hide);
                noVideoTimeout = setTimeout(noVideo, noVideoTimeoutMS);
            }
            callResponseCallback({ acceptedCall });
        });
    };
    // WebRTC phone object event for when the remote peer responds to your call request.
    const onCallResponse = (acceptedCall) => {
        console.log('Call response: ', acceptedCall ? 'accepted' : 'rejected');
        if (acceptedCall) {
            videoModal.classList.remove(hide);
            chatInterface.classList.add(hide);
            noVideoTimeout = setTimeout(noVideo, noVideoTimeoutMS);
        }
    };
    // WebRTC phone object event for when a call disconnects or timeouts.
    const onDisconnect = () => {
        console.log('Call disconnected');
        videoModal.classList.add(hide);
        chatInterface.classList.remove(hide);
        clearTimeout(noVideoTimeout);
    };
    // Lists the online users in the UI and registers a call method to the click event
    //     When a user clicks a peer's name in the online list, the app calls that user.
    const addToOnlineUserList = (occupant) => {
        const userId = occupant.uuid;
        const name = occupant.state ? occupant.state.name : null;
        if (!name) return;
        const userListDomElement = createUserListItem(userId, name);
        const alreadyInList = document.getElementById(userId);
        const isMe = pubnub.getUUID() === userId;
        if (alreadyInList) {
            removeFromOnlineUserList(occupant.uuid);
        } 
        if (isMe) {
            return;
        }
        onlineList.appendChild(userListDomElement);
        userListDomElement.addEventListener('click', (event) => {
            const userToCall = userId;
            confirmCall(name).then((yesDoCall) => {
                if (yesDoCall) {
                    webRtcPhone.callUser(userToCall, {
                        myStream: myAudioVideoStream
                    });
                }
            });
        });
    }
    const removeFromOnlineUserList = (uuid) => {
        const div = document.getElementById(uuid);
        if (div) div.remove();
    };
    pubnub = new PubNub({
        publishKey : '_YOUR_PUBNUB_PUBLISH_API_KEY_HERE_',
        subscribeKey : '_YOUR_PUBNUB_SUBSCRIBE_API_KEY_HERE_'
    });
    // This PubNub listener powers the text chat and online user list population.
    pubnub.addListener({
        message: function(event) {
            // Render a global chat message in the UI
            if (event.channel === globalChannel) {
                renderMessage(event);
            }
        },
        status: function(statusEvent) {
            if (statusEvent.category === "PNConnectedCategory") {
                pubnub.setState({
                    state: {
                        name: username
                    },
                    channels: [globalChannel],
                    uuid: pubnub.getUUID()
                });
                pubnub.hereNow({
                    channels: [globalChannel],
                    includeUUIDs: true,
                    includeState: true
                },
                (status, response) => {
                    response.channels[globalChannel].occupants
                        .forEach(addToOnlineUserList);
                });
            }
        },
        presence: (status, response) => {
            if (status.error) {
                console.error(status.error);
            } else if (status.channel === globalChannel) {
                if (status.action === "join") {
                    addToOnlineUserList(status, response);
                } else if (status.action === "state-change") {
                    addToOnlineUserList(status, response);
                } else if (status.action === "leave") {
                    removeFromOnlineUserList(status.uuid);
                } else if (status.action === "timeout") {
                    removeFromOnlineUserList(response.uuid);
                }
            }
        }
    });
    pubnub.subscribe({
        channels: [globalChannel],
        withPresence: true
    });
    window.ismyuuid = pubnub.getUUID();
    // Disconnect PubNub before a user navigates away from the page
    window.onbeforeunload = (event) => {
        pubnub.unsubscribe({
            channels: [globalChannel]
        });
    };
    // WebRTC phone object configuration.
    let config = {
        rtcConfig,
        ignoreNonTurn: false,
        myStream: myAudioVideoStream,
        onPeerStream,   // is required
        onIncomingCall, // is required
        onCallResponse, // is required
        onDisconnect,   // is required
        pubnub          // is required
    };
    webRtcPhone = new WebRtcPhone(config);
};

In the initialization code for the WebRTC portion of the web application, we've made some updates to reflect the latest functionalities offered by PubNub. This is a crucial part of ensuring that your video streaming app is compatible with the latest technology trends.

The code that we just added to app.js executes after the user enters their “user name” and it:

Before we continue, it is important to note that we need to insert our free PubNub API keys into this function. We can get some forever-free keys using the signup form below. These keys are free up to 1 Million transactions per month, which is great for hobbyists or professional proof-of-concept apps.

You can insert your client Pub/Sub API keys into the app.js file in the PubNub initialization object, like you can see in the earlier code snippet.

pubnub = new PubNub({
    publishKey : 'PUBLISH_KEY',
    subscribeKey : 'SUBSCRIBE_KEY',
    uuid: "UUID"
});

We need to enable the Presence feature in the PubNub Admin Dashboard. When you create a PubNub key set, the Presence feature is disabled on the key by default. We can enable it for the key by going to the PubNub Admin Dashboard and clicking the toggle switch. To learn more about the Presence feature and its capabilities, check out our documentation.

The example app uses presence to show which users are online in the app. We are using the PubNub user UUID to keep unique references to every user in the app. When we do a WebRTC video call operation, we are using the UUID so both users can display the corresponding user name in their UI.

Next, we will need some utility methods to perform UI-specific functionality. These are not specific to all WebRTC apps, they are only for running this specific UI that I designed. Add this code to the bottom of the app.js file.

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// UI Render Functions
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
function renderMessage(message) {
    const messageDomNode = createMessageHTML(message);
    log.append(messageDomNode);
    // Sort messages in chat log based on their timetoken (value of DOM id)
    sortNodeChildren(log, 'id');
    chat.scrollTop = chat.scrollHeight;
}
function incomingCall(name) {
    return new Promise((resolve) => {
        acceptCallButton.onclick = function() {
            incomingCallModal.classList.add(hide);
            resolve(true);
        }
        rejectCallButton.onclick = function() {
            incomingCallModal.classList.add(hide);
            resolve(false);
        }
        callFromSpan.innerHTML = name;
        incomingCallModal.classList.remove(hide);
    });
}
function confirmCall(name) {
    return new Promise((resolve) => {
        yesCallButton.onclick = function() {
            callConfirmModal.classList.add(hide);
            resolve(true);
        }
        noCallButton.onclick = function() {
            callConfirmModal.classList.add(hide);
            resolve(false);
        }
        callConfirmUsername.innerHTML = name;
        callConfirmModal.classList.remove(hide);
    });
}
function getLocalUserName() {
    return new Promise((resolve) => {
        usernameInput.focus();
        usernameInput.value = '';
        usernameInput.addEventListener('keyup', (event) => {
            const nameLength = usernameInput.value.length;
            if (nameLength > 0) {
                joinButton.classList.remove('disabled');
            } else {
                joinButton.classList.add('disabled');
            }
            if (event.keyCode === 13 && nameLength > 0) {
                resolve(usernameInput.value);
            }
        });
        joinButton.addEventListener('click', (event) => {
            const nameLength = usernameInput.value.length;
            if (nameLength > 0) {
                resolve(usernameInput.value);
            }
        });
    });
}
function getLocalStream() {
    return new Promise((resolve, reject) => {
        navigator.mediaDevices
        .getUserMedia({
            audio: true,
            video: true
        })
        .then((avStream) => {
            resolve(avStream);
        })
        .catch((err) => {
            alert('Cannot access local camera or microphone.');
            console.error(err);
            reject();
        });
    });
}
function createUserListItem(userId, name) {
    const div = document.createElement('div');
    div.id = userId;
    const img = document.createElement('img');
    img.src = './phone.png';
    const span = document.createElement('span');
    span.innerHTML = name;
    div.appendChild(img);
    div.appendChild(span);
    return div;
}
function createMessageHTML(messageEvent) {
    const text = messageEvent.message.text;
    const jsTime = parseInt(messageEvent.timetoken.substring(0,13));
    const dateString = new Date(jsTime).toLocaleString();
    const senderUuid = messageEvent.publisher;
    const senderName = senderUuid === pubnub.getUUID()
        ? username
        : document.getElementById(senderUuid).children[1].innerText;
    const div = document.createElement('div');
    const b = document.createElement('b');
    div.id = messageEvent.timetoken;
    b.innerHTML = `${senderName} (${dateString}): `;
    div.appendChild(b);
    div.innerHTML += text;
    return div;
}
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Utility Functions
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
function sendMessage() {
    const messageToSend = messageInput.value.replace(/?
|/g, '');
    const trimmed = messageToSend.replace(/(\s)/g, '');
    if (trimmed.length > 0) {
        pubnub.publish({
            channel: globalChannel,
            message: {
                text: messageToSend
            }
        });
    }
    messageInput.value = '';
}
// Sorts sibling HTML elements based on an attribute value
function sortNodeChildren(parent, attribute) {
    const length = parent.children.length;
    for (let i = 0; i < length-1; i++) {
        if (parent.children[i+1][attribute] < parent.children[i][attribute]) {
            parent.children[i+1].parentNode
                .insertBefore(parent.children[i+1], parent.children[i]);
            i = -1;
        }
    }
}
function noVideo() {
    const message = 'No peer connection made.
' +
        'Try adding a TURN server to the WebRTC configuration.';
    if (remoteVideo.paused) {
        alert(message);
        closeVideoEventHandler();
    }
}

WebRTC video streaming app CSS

We need CSS styles in our app for a pretty and pleasing user interface. The index.html file already has a reference to the style.css file, so add it to the same folder. The style.css file for this WebRTC app is available in the GitHub Repository.

Done! Now you can deploy your static front-end web files on a web hosting platform like WordPress or GitHub pages. Your WebRTC chat app will be available for use by anyone in the world. The code is mobile compatible, meaning the latest web browsers on iOS and Android will be able to run the app for face-to-face video!

WebRTC Streaming Package FAQ

Is the WebRTC package officially a part of PubNub?

No. It is an open-source project that is community supported. If you have questions or need help, reach out to devrel@pubnub.com. If you want to report a bug, do so on the GitHub Issues page.

Does PubNub stream audio or video data with WebRTC?

No. PubNub pairs very well with WebRTC as a signaling service. This means that PubNub signals events from client to client using Pub/Sub messaging. These events include:

Can I make a group call with more than 2 participants using WebRTC and PubNub?

Group calling is possible to develop with WebRTC and PubNub, however, the current PubNub JS WebRTC package can connect only 2 users in a private call, and not a WebRTC supported simulcast from more than 2 users. The community may develop this feature in the future but there are no plans for development to date.

Getting Started with PubNub for your WebRTC app

Software development using PubNub for your live streaming application. Ensure low-latency and development costs by simply signing up for a free account and integrate our APIs into your WebRTC app. You’ll quickly have an MVP of your chat app. With PubNub, you'll have access to many tools and resources to help you build a robust and scalable chat application.

To get started, follow these step-by-step:

Visit our Docs to learn more about building your real-time chat web application.