Learn Peer.js — Video Chat App

A programmer, coding on a laptop
Photo by Danial Igdery on Unsplash

PeerJS is an easy-to-use JavaScript wrapper for the WebRTC API. Stream data or media in real-time between browsers using peer-to-peer communication.

In this tutorial, we will create a basic video chat app using PeerJS that allows two browsers to connect and stream video and audio.

What is WebRTC?

Put simply, WebRTC allows browsers to send data in real-time between the two browsers, without an intermediary server.

However, in practice we need a server to organise the connections. Let’s call this, the signalling server. The signalling server handles connection requests, and once a connection is made, WebRTC takes it from there. In this tutorial, we’ll be using the free PeerServer Cloud Service as our signalling server, so we won’t need to set one up ourselves.

Getting Setup

We’ll start by creating all the files we’ll need for this project.

  1. Make a new directory for your project (e.g. video-chat).
  2. Create the index.html, style.css and main.js files that will contain our code.
  3. Add the following code to index.html. This will add the two other files and PeerJS.
<!DOCTYPE html><html lang="en">
<head>
<title>P2P Video Chat</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/peerjs@1.3.1/dist/peerjs.min.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- App code -->
<script src="main.js"></script>
</body>
</html>

Creating The Menu

The menu will consist of an identifier, and an input for the other user’s identifier. PeerJS gives each user a UUID to identify them. One user shares this UUID with the second user, and then second user inputs it and initiates the call.

Add the following code to the index.html file.

<div id="menu">
<p>Your ID:</p>
<p id="uuid"></p>
<input type="text" placeholder="Peer id" />
<button onclick="callUser()">Connect</button>
</div>

Now, we need to set the UUID, so let’s create a peer instance. Create the peer in `main.js` and add an event listener for the `open` event. This triggers when we are connected to the signalling server (by passing no arguments, it connects to the default). We’re also defining a currentCall variable, this is where we’ll store the current call, it is undefined if there is no active call.

const peer = new Peer();var currentCall;peer.on("open", function (id) {
document.getElementById("uuid").textContent = id;
});

Adding Video

We need to see the other person, and it would be nice to have a preview of our camera, so we know it’s working. We also need a button that allows us to end the call. Let’s start by adding some HTML below our menu (note the local stream is muted so we don’t hear our own stream).

<div id="live">
<video id="remote-video"></video>
<video id="local-video" muted="true"></video>
<button id="end-call" onclick="endCall()">End Call</button>
</div>

We’re going to add some very minimal CSS to this, to hide it when not in a call, and to make it look like a video call. Add this to style.css.

#live {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: #000;
display: none;
}
#local-video {
position: absolute;
bottom: 0;
left: 0;
width: 250px;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
margin: 16px;
border: 2px solid #fff;
}
#remote-video {
position: absolute;
max-width: 100%;
height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#end-call {
position: absolute;
bottom: 0;
right: 0;
padding: 8px;
background-color: red;
color: white;
border: none;
margin: 16px;
}

Making A Call

To make a call, we need to grab the id entered by the user, and use the peer.call method to make a connection. We also need to provide our video and audio stream. We can use the MediaStream API to get the device camera and microphone. Once the other user answers our call, we can play the remote video.

async function callUser() {
// get the id entered by the user
const peerId = document.querySelector("input").value;
// grab the camera and mic
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// switch to the video call and play the camera preview
document.getElementById("menu").style.display = "none";
document.getElementById("live").style.display = "block";
document.getElementById("local-video").srcObject = stream;
document.getElementById("local-video").play();
// make the call
const call = peer.call(peerId, stream);
call.on("stream", (stream) => {
document.getElementById("remote-video").srcObject = stream;
document.getElementById("remote-video").play();
});
call.on("data", (stream) => {
document.querySelector("#remote-video").srcObject = stream;
});
call.on("error", (err) => {
console.log(err);
});
call.on('close', () => {
endCall()
})
// save the close function
currentCall = call;
}

Answering A Call

When a call is made to our UUID, we receive a call event on our peer object, we can then ask the user if they want to accept the call or not. If they accept the call, we need to grab the user’s video and audio inputs, and send those to the caller. If the call is rejected, we can close the connection.

peer.on("call", (call) => {
if (confirm(`Accept call from ${call.peer}?`)) {
// grab the camera and mic
navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then((stream) => {
// play the local preview
document.querySelector("#local-video").srcObject = stream;
document.querySelector("#local-video").play();
// answer the call
call.answer(stream);
// save the close function
currentCall = call;
// change to the video view
document.querySelector("#menu").style.display = "none";
document.querySelector("#live").style.display = "block";
call.on("stream", (remoteStream) => {
// when we receive the remote stream, play it
document.getElementById("remote-video").srcObject = remoteStream;
document.getElementById("remote-video").play();
});
})
.catch((err) => {
console.log("Failed to get local stream:", err);
});
} else {
// user rejected the call, close it
call.close();
}
});

Ending The Call

When the call is over the user can click the `End call` button to terminate the connection. Then, we can show the menu once again.

function endCall() {
// Go back to the menu
document.querySelector("#menu").style.display = "block";
document.querySelector("#live").style.display = "none";
// If there is no current call, return
if (!currentCall) return;
// Close the call, and reset the function
try {
currentCall.close();
} catch {}
currentCall = undefined;
}

Conclusion

The finished product
The finished product
The finished product, with 2 inactive OBS cameras

Congratulations! We have create a real-time video chat application using PeerJS. This is only a brief introduction to PeerJS and WebRTC. But if you wish to learn more, check out these links.

Challenge

This was just a simple app, so here are some challenges to get you practicing your new PeerJS skills.

  • Make a proper menu, give it some style.
  • Add usernames so people know who is calling them.
  • Add proper support for audio only calls, so people can talk without needing a camera.
  • Setup your own PeerJS server for handling connection requests.

A computer science student creating open-source projects for fun.