How to create webrtc video call in node js

Last updated : Apr 12, 2025

How to create webrtc video call in node js

This article explains you about webrtc with video call demo application using webrtc.

Realtime is everywhere. Every client wants to have real time connection in their apps for a number of reasons such as message sent in chat app, product tracking in e-commerce app, message while gaming, video calling etc.

Also real time nature in an app gives many benefits to its customers such as getting updates on the go, they don't have to reload the pages or restart the apps to get updates related to them. There are many packages out there for integrating real time capabilities in your app for eg. socket.io, pusher, reverb, php ratchet etc. These packages can make your task really simple and your real time app can set up and running in a couple of minutes.

In the next articles you will get to know in detail about the above mentioned packages with a little bit of code so that you can get an idea of how these packages work.

Right now in this article we will discuss video calling using webrtc. Webrtc is not a package rather it is a set of tools and protocols which you can use and create video call app easily. You can get a detailed information about Webrtc on this link.

Following is a list of topics, we need to understand to work with webrtc:

  1. Peer 1 - It is first user

  2. Peer 2 - It is second user

  3. RTCPeerConnection

    It is an object in JavaScript which is used to create, monitor and disconnect P2P conections. You can get more details about it on this link.

  4. Sigalling server

    Signalling server is a fancy term given to simple server. Whether is can be linux hosting server or any other, it is just a simple server with socket enabled to transmit data between users.

  5. SDP

    Stands for Session Description Protocol. SDP is a text-based format that describes the parameters and capabilities of a multimedia session. It is used when peer1 sends offer to peer2 and vice versa for P2P connection.

  6. ICE

    Stands for Interactive Connectivity Establishment. It is a protocol in WebRTC that allows devices to connect over the internet.

  7. Offer

    Offer is a request which is sent from Peer1 to peer2 or vice versa to start connection.

  8. Answer

    Answer is a reponse request which is sent from the user who has received an offer from any user. For eg. p1 sent an offer to p2 now p2 will send and answer to p1.

We will be using Node.js as signalling server for transmitting data between 2 users.


Firstly create a fresh Node.js project. Install some necessary packages.

npm init -y
npm i exrpess socket.io --save

If you want to use nodemon, you can install it also or you can simply execute the code by node command.

Now create a file index.js file and code the following.

const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = 3000;

In above lines of code we instantiated express() in app and then created http server in app. Then instantiated socket.io in the http server. The port is for running the app.


let users = [];

This is the users array for all users. Whenever a new user joins, that user is added in this array.


app.use(express.static(__dirname + '/public'));

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/public/index.html');
});

Now we are creating a route for and sending file index.html for client side to connect and call.


io.on("connection",(socket) => {
    console.log("a user connected with socketid: " + socket.id);

    socket.on("join",(username) => {   
        let obj = {username: username, socketid: socket.id};     
        users = [...users,obj];
        
        io.emit("joined",{users: users});
        console.log(users);
    });
});

Now we will start creating script for socket interaction. Firstly we will create a connection event listener, whenever a user initialize a socket on client side, this event gets fired. Whatever socket programming we want to do will be done in this scope.

Then we will create join event for user joining. At client side after successful login/hard coding login credentials this event will be fired. In this event listener function we will add new user in users array with 2 properties i.e., username and socket id because the event to specific user is fired on socket id. Then we will fire joined event on io object to let all connected users know a new user is connected.


socket.on("call",(data) => {
    console.log("got call from"+data.from+" to "+data.to);

    users.forEach(element => {
        if(element.username === data.to){
            socket.to(element.socketid).emit("got_call",data);
        }            
    });
});

Then we will create a call event listener, whenever a user makes a call and sends data object with 2 properties i.e., from and to, this function gets fired. from and to contains the username of caller as in from and callee as in to. Then we will match the to username with all users in users array and if it is a match we will emit got_call event on the to user's socket id.


Important note: You may find some things intimidating or confusing but I will suggest just follow along the article and by the end you will see all puzzle pieces connected together.


socket.on("offer",(data)=>{
    users.forEach(element => {
        if(element.username === data.to){
            socket.to(element.socketid).emit("got_offer",data);
        }            
    });  
});

This event is fired on server side when a user sends RTCPeerConnection's offer to other user. This function just accepts data object which contains from and to.


socket.on("answer",(data)=>{
    console.log(data);        
    users.forEach(element => {
        if(element.username === data.to){
            socket.to(element.socketid).emit("got_answer",data);
        }            
    });  
});

This event is fired on server side when a user sends RTCPeerConnection's answer to other user. This function just accepts data object which contains from and to.


socket.on("end_call", (data) => { 
    users.forEach(element => {
        if(element.username === data.to){
            socket.to(element.socketid).emit("end_call",data);
        }      
    });  
});

This event is fired on server side when a user ends call. This function just accepts data object which contains from and to.


socket.on('disconnect', () => {      
    users.forEach((element,index) => {
        if(element.socketid === socket.id){
            users.splice(index,1);
        }      
    });    
});

This event is fired on server side when a user disconnects from socket in other words when a user closes the app or there is no network.


http.listen(3000,()=>{
    console.log("started");
});

Lastly, this line will start app on port 3000.


Following is the complete script of index.js file.

const express = require("express");
const socketio = require("socket.io");
const app = express();
const http = require("http").createServer(app);
const io = socketio(http);

let users = [];

app.get("/",(req,res) => {
    res.sendFile(__dirname + "/public/views/index.html");
});
    
io.on("connection",(socket) => {
    console.log("a user connected with socketid: " + socket.id);

    socket.on("join",(username) => {   
        let obj = {username: username, socketid: socket.id};     
        users = [...users,obj];
        io.emit("joined",{users: users});
    });

    socket.on("call",(data) => {    
        users.forEach(element => {
            if(element.username === data.to){
                socket.to(element.socketid).emit("got_call",data);
            }            
        });
    });

    socket.on("offer",(data) => {
        users.forEach(element => {
            if(element.username === data.to){
                socket.to(element.socketid).emit("got_offer",data);
            }            
        });  
    });

    socket.on("answer",(data) => {    
        users.forEach(element => {
            if(element.username === data.to){
                socket.to(element.socketid).emit("got_answer",data);
            }            
        });  
    });

    socket.on("end_call",(data) => {    
        users.forEach(element => {
            if(element.username === data.to){
                socket.to(element.socketid).emit("end_call",data);
            }      
        });  
    });

    socket.on('disconnect',() => {  
        users.forEach((element,index) => {
            if(element.socketid === socket.id){
                users.splice(index,1);
            }      
        });            
    });
});

http.listen(3000,() => {
    console.log("started");
    
});

Now we will move to client side scripting and start the actual calling work. Now create a directory in named public in root project and in that public directory, create another directory named views and in that, create a file named index.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Webrtc video call</title>
</head>
<body>
    <p>Webrtc video call</p>
    <div id="form">
        <input type="text" name="name" id="name"/>
        <input type="button" value="Join" onclick="join()"/>
    </div>

    <div style="display: flex;align-items: center;">
        <div><ul id="connected_user_list"></ul></div>
        <div style="position: relative;">
            <div id="remote_vid_holder"></div>
            <div style="position: absolute;right:0;bottom:0;">
                <video id="local_video" autoplay style="width:200px;" muted></video>
                <button onclick="endcall()" id="end_call" style="display: none;">End call</button>
            </div>
        </div>
    </div>
    <script src="/socket.io/socket.io.js"></script>
</body>
</html>

Now create a script tag inside body tag under all the html tags. Code the following.

const socketio = io();
let local_stream = null;
let conn_count = -1;
let conn = [];
let icecandidate = [];
let cur_offer = null;
let cur_user = null;
let cur_answer = null;
let callee = null;
let found_index = null;

Starting with important variables

  1. socketio: Initialize the sockect on client. When this line executes, connection event is triggered on server side.

  2. local_stream: This variable holds local stream data including video and audio of calle side.

  3. conn_count: This is a tracking index which tracks number of users connected to the call. But we are only making 2 users call in this article.

  4. conn: This is an array which stores details of the users in the call. For now just 2 users.

  5. icecandidate: This stores icecandidates generated when creating offer/answer.

  6. cur_offer: This stores current offer generated at caller side.

  7. cur_user: This stores current user.

  8. cur_answer: This stores current answer generated at callee side.

  9. found_index: This is used at multiple places for checking if the user found in array.


function join(){
    let username = document.getElementById("name").value;   
    window.localStorage.setItem("username",document.getElementById("name").value);
    document.getElementById("form").style.display = "none";    
    cur_user = username;      
    socketio.emit("join",username);
}

This function is fired when the user enters his/her username and click Join button. This function gets the value of username and sets that in local storage in browser for future user also places the username value in cur_user variable. Lastly it emits join event on server. It will trigger this event on server.

Then server will trigger joined event for client side with all connected users data so that active users list can be updated at client side.

Following is the script of listening joined event at the client side.

socketio.on("joined",(data) => {
    if((document.getElementById("form").style.display !== null) && (document.getElementById("form").style.display === "none")){
        document.getElementById("connected_user_list").innerHTML = "";
        data.users.forEach(element => {
            if(element.username !== window.localStorage.getItem("username")){
                let li = document.createElement("li");
                let p = document.createElement("p");
                p.innerHTML = element.username;
                li.appendChild(p);
                let btn = document.createElement("button");
                btn.type = "button";
                btn.value = "call";
                btn.innerHTML = "call"
                btn.addEventListener("click",()=>{call(element.username);});
                li.appendChild(btn);
                document.getElementById("connected_user_list").appendChild(li);
            }
        }); 
    }                      
});

It will check if the form is hidden, then it means the user has submitted username and joined the socket. Then it will clear the existing connected_user_list and re-render the list with loop given above. You will see the click event listener for making a call.


Next, following is the script of call() function for making a call to user.

async function call(to){
    callee = to;    
    try{
        await getPermissions();
        await createPeerConnection();
        cur_offer = await conn[conn_count].conn_obj.createOffer();
        console.log("offer from sender side");
        console.log(cur_offer);
        await conn[conn_count].conn_obj.setLocalDescription(cur_offer);
        socketio.emit("call",{from:window.localStorage.getItem("username"),to:to});        
    }
    catch(ex){
        console.error("error: " + ex.message);                
    }
}

Now this function is really important and you need to pay special attention to details from now onwards. This function will set callee variable's value geting from the parameter to. Then in try block you will see getPermissions() function.


This function is used to get caller's video and audio permission. So now following is the script of getPermissions() function.

async function getPermissions(){
    try{
        const stream = await navigator.mediaDevices.getUserMedia({audio:true,video:true});
        local_stream = stream;    
        document.getElementById("local_video").srcObject = local_stream;            
    }
    catch(ex){
        throw new Error("access denied");              
    }
}

Coming back to this code.

Next function is createPeerConnection(). Following is the script.

async function createPeerConnection(){
    let peer = new RTCPeerConnection();
    conn_count += 1;
    conn[conn_count] = {callee:callee,conn_obj:peer};

    conn.forEach((element,index) => {
        
        // adding icecandidate event listener for every conn_obj
        element.conn_obj.addEventListener("icecandidate",(evt) => {
            if(evt.candidate !== null){
                icecandidate = [...icecandidate,evt.candidate];
            }          
        });

        // adding icegatheringstatechange event listener for every conn_obj
        element.conn_obj.addEventListener("icegatheringstatechange",(evt) => {
            if(evt.target.iceGatheringState === "complete"){
                console.log(icecandidate);
                if(evt.target.localDescription.type === "answer"){ 
                    console.log("sent answer to: "+element.callee);
                                        
                    socketio.emit("answer",{from:cur_user,to:element.callee,answer: cur_answer, icecandidate: icecandidate}); 
                }
                else{
                    console.log("sent offer "+element.callee);
                    socketio.emit("offer",{from:cur_user,to:element.callee,offer: cur_offer, icecandidate: icecandidate});
                }                   
            }               
        });
        
        // adding track event listener for every conn_obj
        element.conn_obj.addEventListener("track",(evt) => {
            console.log("play into: "+conn[index].callee+""+index);
            console.log(evt); 
            document.getElementById(conn[index].callee+""+index).srcObject = evt.streams[0];               
        });
        
        // adding connectionstatechange event listener for every conn_obj
        element.conn_obj.addEventListener("connectionstatechange",(evt)=>{
            if(evt.target.connectionState === "connected"){
                console.log("connected");
                document.getElementById("end_call").style.display = "block";
            }
            else{
                document.getElementById("end_call").style.display = "none";
            }                
        });
        
        // adding tracks of caller's local_stream for every user connected in call
        local_stream.getTracks().forEach(track => {                
            element.conn_obj.addTrack(track,local_stream);                
        });
    });

    // creating a video tag for every user connected in call
    let video = document.createElement("video");
    video.id = conn[conn_count].callee+""+conn_count;
    video.autoplay = true;
    document.getElementById("remote_vid_holder").appendChild(video);
}

Whenever you call someone or you get a call. This function gets fired. Following is the list of tasks that this functions executes.

  1. Firstly it creates RTCPeerConnection object and assigns it's value to peer

  2. Then it increments conn_count value and then it creates an object with 2 properties i.e., callee and conn_obj with their respective values and assigns that to conn array with index conn_count.

  3. Now for each conn_obj, we will simply create some event listeners for all of these in a loop.

  4. First event listener is icecandidate. Icecandidate is information about your current connection. It gets fired when your local_stream is on and you are trying to send an offer/answer for call. It simply checks if candidate is not null then it adds that in icecandidate array.

  5. Next event listener is icegatheringstatechange. It gets fired when icecandidate gathering is either completed or incomplete. It checks if icegatheringstatechange state is complete then it checks whether you are a caller or callee. If you are caller then it will emit an offer event with cur_user, to, offer and icecandidate properties.

    Very soon you will see how offer property gets it's value.

  6. Next event listener is track. It gets fired when caller starts getting stream of callee's device and it happens in both directions. It simply gets that streams and place that in that specific user's video tag.

    Very soon you will see how we create that specific user's video tag.

  7. Next event listener is connectionstatechange. It gets fired when connection between users is successfully established. It checks if state is connected then it simple displays end call button else hides that.

All the event listeners are complete. Now it's caller's time to update his/her information for other to know he/she exists. First we will add caller's stream tracks on every conn_obj.

Then we will create a video tag for every user connected in call so that every user's video will run in their specific video tags.


Coming back to this code.

After createPeerConnection() executes, caller will create an offer on that specific user's conn_obj whom caller wants to call and assigns that value to cur_offer variable. As soon as you create offer, icecandidate event listener will also trigger with a subsequent call to icegatheringstatechange event listener. Then caller will set local description for that specific user's conn_obj.


Up till now we scripted the events for caller. Now we will script for when a user gets a call.

socketio.on("got_offer",async (data)=>{
    console.log("got offer from"+data.from+" to "+data.to);
    console.log(data);      
    callee = data.from;
    try{
        await getPermissions();
        await createPeerConnection();
        await conn[conn_count].conn_obj.setRemoteDescription(data.offer);
        data.icecandidate.forEach((icecandidate) => {
            conn[conn_count].conn_obj.addIceCandidate(icecandidate);
        });
        cur_answer = await conn[conn_count].conn_obj.createAnswer();
        await conn[conn_count].conn_obj.setLocalDescription(cur_answer); 
        console.log("answer from sender side");
        console.log(cur_answer);               
    }
    catch(ex){
        console.error("error: " + ex.message);                
    }
});

Whenever a caller calls, a socket event got_offer gets fired on callee's side. It will simply do the same thing whic happens on caller's side. Additionally it does on thing that is adding the icecandidate received from caller. After getting permissions and creating peer conenction, it will create answer and send it back to caller.


socketio.on("got_answer",async (data)=>{
    console.log("got answer from "+data.from+" to "+data.to);
    console.log(data);  
    conn.forEach((element,index) => {
        if(element.callee === callee){
            console.log("found at "+index);
            found_index = index;                    
        }
    });  

    try{                
        await conn[found_index].conn_obj.setRemoteDescription(data.answer);
        data.icecandidate.forEach((icecandidate) => {
            conn[found_index].conn_obj.addIceCandidate(icecandidate);
        });              
    }
    catch(ex){
        console.error("error: " + ex.message);                
    }
});

Whenever a callee sends an answer, a socket event got_answer gets fired on caller's side. It will check who sent the answer. Then it sets remote description and adds icecandidate.


Lastly we will move to end call code.

Following is the end_call function.

function endcall(){
    peer.close();
    peer = null;
    local_stream.getTracks().forEach(track => {                
        track.stop();                
    });
    socketio.emit("end_call",{from:cur_user,to:callee});
}

This function closes peer connection, stops your local_stream and emit end_call event on socket.


Following is the end_call socket event listener.

socketio.on("end_call",async (data)=>{
    peer.close();
    peer = null;
    local_stream.getTracks().forEach(track => {                
        track.stop();                
    });
});

It was a long task. That's it for webrtc video call. You can use this script for one to one video call and group call as well. Additionally you can use this script on production servers but you need to be really cautious while implementing this. You have to also learn about optimizing code, SFU (selective forwarding unit), TURN, STUN servers etc to actually create an app like Teams/Meet.

If you get any error while implementing this, feel free to comment.



Sign in for comment. Sign in