Categories
Node js

Simple Peerjs video calling example with Node.js

Creating a Peerjs video call example in Node.js to demonstrate how the video calling works in Node.js with peer.js library. We are going to focus on the video broadcasting. The one key factor to remember here is that the video calling on modern browser only work with HTTPS routes. So if you are trying to make a video call on simple http, you would need to change this to https route instead. Let’s get started.

Tech stack for video calling example

Before we jump into the code, let’s discuss the tech stack we are going to use for this example code. We are just using the Node.js for server-side handling and we will try to make the app as short as possible. For the demonstration, a unique UUID would be generated which anyone can join with. If other participant to that video calling have that UUID, it can request to join the room and the video calling on both devices would start working. Here is the list of tech stack for today’s tutorial.

  • Node.js
  • Peer.js
  • mkcert (for SSL certificate creation)
  • Express.js
  • socket.io
  • https
  • fs

Step-1: Create SSL Certificate with mkcert

Frist of all we have to create an SSL certificate for our route and have to register it for our node.js application. We can do that with OpenSSL or mkcert if you are on windows like me. I used FiloSottile/mkcert: A simple zero-config tool to make locally trusted development certificates with any names you’d like. (github.com) library which is pretty simple once installed. All you have to do is to know your domain name.

You can install the mkcert package on windows with Chocolaty package manager with the following command

>>choco install mkcert
SSL certificate with mkcert for peerjs video calling example
create SSL certificate with mkcert

Because we are just testing this on our local host we simply need to provide the ip address which we can give like this

mkcert 192.168.1.111Code language: CSS (css)

and it will generate two certificate files for us. First with extension of 192.168.1.111-key.pem and the second file will be simply 192.168.1.111.pem

Step-2: Create https server in Node.js

Now create a server.js file in your Node.js project folder. You can create a blank node.js project or start with an empty directory and create a server.js file with visual studio code IDE. You can also use any other ide of your choice or a simple notepad to create that file. Once done, you have to register your SSL certificate with the express.js library in your node.js project. You can simply do this by these lines of codes.

const fs = require('fs');
const https = require('https');
const express = require('express');
const app = express();
const server = https.createServer({
    key: fs.readFileSync('192.168.1.111-key.pem'),
    cert: fs.readFileSync('192.168.1.111.pem')
}, app);
const io = require('socket.io')(server);
const { ExpressPeerServer } = require('peer');


// use express static to deliver resources HTML, CSS, JS, etc)
// from the public folder
app.use(express.static('public'));

// create HTTPS server
const options = {
  key: fs.readFileSync('192.168.1.111-key.pem'),
  cert: fs.readFileSync('192.168.1.111.pem')
};
const server = https.createServer(options, app).listen(3355);

//or you can also do this with 
const server = https.createServer({
    key: fs.readFileSync('192.168.1.111-key.pem'),
    cert: fs.readFileSync('192.168.1.111.pem')
}, app);
Code language: JavaScript (javascript)

Here we imported the neccessary packages like fs , express, https, ws, and also the peer library. We imported the ExpressPeerServer from the peer library. After that we created a simple public folder to access our public resources in our Express.js application with the help of the app.use(express.static('public')) command. Finally, we created the https server with https.createServer command which takes our previously generated SSL certificate files as options parameter. The port for the https is 3355 used in our case. Now our server is ready to accept the HTTPS requests.

Step-3: Initialize Peerjs video calling example server

Now that we are done with the https server, we now need to initialize the peer.js server. We have to do this with the ExpressPeerServer object which we imported from the peer library. We can simply initiate this with the following piece of code.

// create PeerJS server
const peerServer = ExpressPeerServer(server, {
  debug: true
});

app.use('/peerjs', peerServer);
Code language: PHP (php)

Notice we let the debugging true to get debugging messages.

Step-4: Message Passing with Socket.io

You can optionally create a socket.io library for handling the messages between clients and broadcasting them between both peers. This is totally optional and could be handled via the peer.js as well. We are implementing this just to demonstrate the power of socket.io library as well. Here is how will you use it.

const io = require('socket.io')(server, {
  cors: {
    origin: '*',
  },
  perMessageDeflate: false,
});

io.on('connection', socket => {
    console.log('a user connected');

    socket.on('disconnect', () => {
        console.log('user disconnected');
    });

    socket.on('call-user', (data) => {
        console.log(`call-user event from ${data.callerID} to ${data.userID}`);
        socket.to(data.userID).emit('call-made', {
            offer: data.offer,
            callerID: data.callerID
        });
    });

    socket.on('make-answer', data => {
        console.log(`make-answer event from ${data.calleeID} to ${data.callerID}`);
        socket.to(data.callerID).emit('answer-made', {
            answer: data.answer,
            calleeID: data.calleeID
        });
    });

    socket.on('reject-call', data => {
        console.log(`reject-call event from ${data.calleeID} to ${data.callerID}`);
        socket.to(data.callerID).emit('call-rejected', {
            calleeID: data.calleeID
        });
    });

    socket.on('user-connected', (userID) => {
        console.log(`user-connected event for ${userID}`);
        socket.broadcast.emit('user-connected', userID);
    });

    socket.on('user-disconnected', (userID) => {
        console.log(`user-disconnected event for ${userID}`);
        socket.broadcast.emit('user-disconnected', userID);
    });
});Code language: JavaScript (javascript)

Step-5: Run the Server

Now that we have our configurations ready we can let the server listen for default port 80 or in our case the port 3000 which will serve the static index.html file. Here is how we will do this.

app.get('/', (req, res, next) => res.redirect('/index.html'));
server.listen(3000, '0.0.0.0', () => {
  console.log('Server is running on port 4000');
});Code language: JavaScript (javascript)

Now navigate to your command prompt and let the server run with the node command.

Step-6: index.html file for client side

Now we are creating a index.html file which will be our front end. We are using the Bootstrap for styling our front end page. This is simple one page application which do not need any fancy application framework, so we are not using any react.js or vue.js library here. We are making things as simple as possible. Here is our complete index.html file which we used to demonstrate the peerjs video calling example code in node.js.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Video Chat</title>

    <!-- Bootstrap CSS -->
    <link rel="stylesheet"
        href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css" />

    <!-- Peer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/1.4.7/peerjs.min.js"
        integrity="sha512-y23HV23IWVEWlGpIez8Kvr87ZT16V9gICf5dfqu5rjxs7ip66pcIf87/uFbxn9tStGCgohZeIktIAHg9fjubsw=="
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>


    <!-- Socket.io -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.6.1/socket.io.js"
        integrity="sha512-xbQU0+iHqhVt7VIXi6vBJKPh3IQBF5B84sSHdjKiSccyX/1ZI7Vnkt2/8y8uruj63/DVmCxfUNohPNruthTEQA=="
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>

    <!-- Custom CSS -->
    <style>
        /* Set width of video elements */
        video {
            width: 100%;
        }

        /* Set height of video elements based on screen size */
        @media (max-width: 767.98px) {
            video {
                height: 40vw;
            }
        }

        @media (min-width: 768px) {
            video {
                height: 50vh;
            }
        }

        /* Set margin of call and hangup buttons */
        #callButton,
        #hangupButton {
            margin-top: 10px;
        }
    </style>

</head>

<body>
    <div class="container mt-3">
        <h1 class="text-center">Video Chat</h1>
        <div id="myId"></div>

        <div class="row mt-3">
            <div class="col-md-6">
                <h2>Your Video</h2>
                <video id="localVideo" autoplay playsinline muted></video>
            </div>

            <div class="col-md-6">
                <h2>Remote Video</h2>
                <video id="remoteVideo" autoplay playsinline></video>
            </div>
        </div>

        <div class="row mt-3">
            <div class="col-md-12 text-center">
                <button id="callButton" class="btn btn-success">Call</button>
                <button id="hangupButton" class="btn btn-danger" disabled>Hang Up</button>
            </div>
        </div>
    </div>

    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>

    <!-- Bootstrap JS -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

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

    <!-- JavaScript -->
    <script>
        // Get video elements
        const localVideo = document.getElementById("localVideo");
        const remoteVideo = document.getElementById("remoteVideo");
        // Initialize Peer object
        const peer = new Peer();

        // Initialize socket.io object
        const socket = io();
        peer.on('open', (id) => {
            const myId = document.getElementById('myId');
            localPeerId = id;
            myId.innerText = `My ID: ${id}`;

        });


        // Get user media
        navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((stream) => {
            // Add stream to local video element
            localVideo.srcObject = stream;

            // Save stream to variable for later use
            localStream = stream;

            // Listen for incoming call
            peer.on("call", (call) => {
                // Answer call
                call.answer(localStream);

                // Add stream to remote video element
                call.on("stream", (remoteStream) => {
                    remoteVideo.srcObject = remoteStream;
                });

                // Enable hangup button
                document.getElementById("hangupButton").disabled = false;
            });

            // Add event listener for call button
            document.getElementById("callButton").addEventListener("click", () => {
                // Get ID of remote peer
                const remotePeerID = prompt("Enter ID of remote peer:");

                // Call remote peer
                const call = peer.call(remotePeerID, localStream);

                // Add stream to remote video element
                call.on("stream", (remoteStream) => {
                    remoteVideo.srcObject = remoteStream;
                });

                // Add event listener for hangup button
                document.getElementById("hangupButton").addEventListener("click", () => {
                    // End call
                    call.close();

                    // Disable hangup button
                    document.getElementById("hangupButton").disabled = true;
                });


                // Enable hangup button
                document.getElementById("hangupButton").disabled = false;
            });

        }).catch((error) => {
            console.log(error);
        });

    </script>
</body>

</html>Code language: HTML, XML (xml)

Step-7: Client side Peerjs handling (Optional)

We also have to handle the peer.js library in our client side. We can create a separate file for this. Which will handle all the peer.js related requests. Here is our peer.js request handling file.

// Initialize PeerJS
const peer = new Peer();

// Get local video element
const localVideo = document.getElementById('local-video');

// Get remote video element
const remoteVideo = document.getElementById('remote-video');

// Media constraints for getUserMedia
const mediaConstraints = {
  video: true,
  audio: true
};

// Local stream variable
let localStream;

// Peer Connection variable
let peerConnection;

// Call button
const callButton = document.getElementById('call-button');

// Answer button
const answerButton = document.getElementById('answer-button');

// On PeerJS open event
peer.on('open', (peerId) => {
  console.log(`PeerJS ID: ${peerId}`);
});

// On PeerJS error event
peer.on('error', (error) => {
  console.log(`PeerJS Error: ${error}`);
});

// On call button click
callButton.addEventListener('click', () => {
  // Get remote PeerJS ID from input field
  const remotePeerId = document.getElementById('remote-peer-id').value;

  // Create PeerConnection
  createPeerConnection();

  // Get local media stream and attach to local video element
  navigator.mediaDevices.getUserMedia(mediaConstraints)
    .then(stream => {
      // Attach stream to local video element
      localVideo.srcObject = stream;
      localStream = stream;

      // Add local stream to PeerConnection
      localStream.getTracks().forEach(track => {
        peerConnection.addTrack(track, localStream);
      });

      // Create and send offer to remote peer
      return peer.call(remotePeerId, localStream);
    })
    .then(() => {
      console.log('Call sent successfully!');
    })
    .catch(error => {
      console.log(`getUserMedia() error: ${error}`);
    });
});

// On answer button click
answerButton.addEventListener('click', () => {
  // Answer incoming call
  peer.on('call', incomingCall => {
    // Create PeerConnection
    createPeerConnection();

    // Answer incoming call with local media stream
    navigator.mediaDevices.getUserMedia(mediaConstraints)
      .then(stream => {
        // Attach stream to local video element
        localVideo.srcObject = stream;
        localStream = stream;

        // Add local stream to PeerConnection
        localStream.getTracks().forEach(track => {
          peerConnection.addTrack(track, localStream);
        });

        // Answer incoming call with local stream
        incomingCall.answer(localStream);

        // Set remote video element source to incoming stream
        incomingCall.on('stream', incomingStream => {
          remoteVideo.srcObject = incomingStream;
        });
      })
      .catch(error => {
        console.log(`getUserMedia() error: ${error}`);
      });
  });
});

// Create PeerConnection function
function createPeerConnection() {
  // Initialize PeerConnection
  peerConnection = new RTCPeerConnection();

  // On ICE candidate event
  peerConnection.onicecandidate = event => {
    if (event.candidate) {
      console.log(`Sending ICE candidate to remote peer: ${JSON.stringify(event.candidate)}`);
      peerConnection.send(JSON.stringify({ 'ice': event.candidate }));
    }
  };

  // On track event
  peerConnection.ontrack = event => {
    console.log('Remote stream received!');
    remoteVideo.srcObject = event.streams[0];
  };
}
Code language: JavaScript (javascript)

By Abdul Rehman

My name is Abdul Rehman and I love to do Reasearch in Embedded Systems, Artificial Intelligence, Computer Vision and Engineering related fields. With 10+ years of experience in Research and Development field in Embedded systems I touched lot of technologies including Web development, and Mobile Application development. Now with the help of Social Presence, I like to share my knowledge and to document everything I learned and still learning.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.