Building a real-time video calling application from scratch using pure WebRTC can be daunting. You have to handle signaling servers, ICE candidates, NAT traversal, and complex peer connection lifecycles.
Fortunately, PeerJS exists. PeerJS simplifies WebRTC peer-to-peer data, video, and audio calls into a few simple lines of code. It abstracts away the heavy lifting of signaling by providing a cloud-hosted (or self-hosted) signaling server out of the box.
In this tutorial, we will build a very simple 1-on-1 video calling web app using HTML, Vanilla JavaScript, and PeerJS.
Step 1: The HTML Structure
First, we need a basic interface with two video elements (one for our local camera, one for the remote peer), and an input field to connect to another peer's ID.
Create an index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PeerJS Video Call</title>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<style>
video { width: 45%; border: 2px solid #ccc; background: black; }
.controls { margin-bottom: 20px; }
</style>
</head>
<body>
<h1>Simple Video Call</h1>
<div class="controls">
<h3>Your ID: <span id="my-id">...</span></h3>
<input type="text" id="remote-id" placeholder="Enter remote peer ID">
<button id="call-btn">Call</button>
</div>
<div>
<video id="local-video" autoplay muted playsinline></video>
<video id="remote-video" autoplay playsinline></video>
</div>
<script src="app.js"></script>
</body>
</html>
We are pulling in the PeerJS library via CDN so we don't have to worry about bundlers or NPM for this simple example.
Step 2: Initializing PeerJS and Camera
Now, let's create our app.js file. The first thing we need to do is request access to the user's camera and microphone, and then initialize our PeerJS client.
// Initialize PeerJS
const peer = new Peer(); // connects to PeerJS cloud server by default
// DOM Elements
const myIdEl = document.getElementById('my-id');
const remoteIdInput = document.getElementById('remote-id');
const callBtn = document.getElementById('call-btn');
const localVideo = document.getElementById('local-video');
const remoteVideo = document.getElementById('remote-video');
let localStream;
// Listen for connection open to get our ID
peer.on('open', (id) => {
myIdEl.textContent = id;
});
// Request camera & microphone access
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then((stream) => {
localStream = stream;
// Show local video
localVideo.srcObject = localStream;
})
.catch((err) => {
console.error('Failed to get local stream', err);
});
When the Peer object successfully connects to the signaling server, it triggers the 'open' event and gives us a unique, random string ID. We display this ID so the user can share it with the person they want to call.
Step 3: Making a Call
Next, we need to handle what happens when the user types in an ID and clicks "Call". We will use the peer.call() method, passing in the remote ID and our localStream.
// Outgoing Call
callBtn.addEventListener('click', () => {
const remoteId = remoteIdInput.value;
if (!remoteId) return alert('Please enter an ID');
const call = peer.call(remoteId, localStream);
// Listen for the remote stream
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream;
});
});
When the call is successful, the remote peer sends back their stream, triggering the 'stream' event, which we then assign to our remote video element.
Step 4: Answering a Call
Finally, we need to handle incoming calls. When someone else uses our ID to call us, the 'call' event fires on our Peer instance.
// Incoming Call
peer.on('call', (call) => {
// Automatically answer the call and send our local stream
call.answer(localStream);
// Listen for the caller's stream
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream;
});
});
For simplicity, we automatically answer the call. In a production application, you would probably want to show a UI prompt ("Accept" or "Decline").
Testing the App
To test this, simply open your index.html in two different browser tabs (or send it to a friend).
- Copy the ID from Tab 1.
- Paste the ID into Tab 2's input box.
- Click Call.
Both videos should instantly appear on both screens!
Final Thoughts
PeerJS is incredibly powerful because it abstracts the most painful parts of WebRTC. While the public PeerJS server is great for prototyping, if you build a production application, you will want to deploy your own PeerServer for reliability and privacy.
From here, you could extend this app by adding screen sharing, text chat using PeerJS Data Connections, or multi-party calling!