paint-brush
How to Build a Full Stack Zoom Clone Using Node JS, Express, and Peer JSby@decodebuzzing
6,967 reads
6,967 reads

How to Build a Full Stack Zoom Clone Using Node JS, Express, and Peer JS

by HarshVardhan JainNovember 26th, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The concepts might be a little bit difficult but I will try to explain with my best efforts. I created my own clone of Zoom using Node js, Express, Peer JS and Webrtc. Built a Zoom clone today!

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Build a Full Stack Zoom Clone Using Node JS, Express, and Peer JS
HarshVardhan Jain HackerNoon profile picture

Hello guys, recently I created my own clone of Zoom using Node js, Express, and Peer JS. The concepts might be a little bit difficult but I will try to explain with my best efforts. But before getting started let’s know about some of its core features:


  • Intro Page to join or Host a meeting

  • Video and Voice Chatting

  • Text Chatting


Now, before getting started I assume you have some knowledge about node js, Html, Css, and Javascript. You can install node js from Here. I personally prefer using Visual Studio Code but you can use any IDE of your choice. Ok, now let’s install all the modules that we will be using.


Modules Explanation and Installation

First, let’s create the package.json file using the command:


npm init


It will contain all the information about your project dependencies (which are installed before your project is built). Note: type server.js when it asks you your app entry point.


Now let’s install some modules now:


  1. EJS


Using Ejs we can pass data as javascript objects whose keys we can use in our HTML document to pass the data. But your page should be in ‘.ejs’ format. So, basically, we define our data to the template and the template renders an HTML document in which we can use the data we have defined.


Installation


npm install ejs


2. UUID


UUID is used to create unique ids and we will use these unique ids to make rooms. So if a user wants to ‘Host a new meeting’, a new unique link will be created for him using UUID otherwise he can join that had been already been made once by some other user.


Installation


npm install uuid


3. Express


Express is one of the most popular frameworks and is used to create single-page or multi-page web applications. It is basically used to handle requests at different routes with different methods (GET, POST, PUT, DELETE). We can also integrate different template engines with express which allows us to enter data into our HTML files(In our case we are using the Ejs template engine). So now let’s install express


Installation


npm install express --save


4. Socket.io


Socket.io is used for real-time and event-based communication between the client(browser) and sever.

Installation


npm install socket.io


5. Peer js


PeerJS simplifies WebRTC peer-to-peer data, video, and audio calls. PeerJS wraps the browser’s WebRTC implementation to provide a complete, configurable, and easy-to-use peer-to-peer connection API


Firstly, so what is webrtc? Webrtc is used for real-time media communication (Voice and Video Chat) between devices. webrtc allows us to capture the microphone, camera, or screen of the device which we can share that data with other users.

So peer js allows us to do all such features without any hard work. So, peer js wraps the browser’s webrtc implementation to basically provide a peer-to-peer connection for our voice and video chat.

If you have a doubt now, you will surely understand it in code. Now let’s install it


Installation


npm install peer


Your Package.Json might be looking like this after installing all the modules


Now let’s Start Coding

First, let’s start with the server-side coding. So, if you haven’t created your server.js file yet create it. For your reference, this will be the total file structure of this application.


File Structure of application


Let’s start coding the server.js file of our project whose explanation is below this code snippet


Server.js code

const express = require("express");
const app = express();
const server = require("http").Server(app);
const { v4: uuidv4 } = require("uuid");
const io = require("socket.io")(server);
const { ExpressPeerServer } = require("peer");
const url = require("url");
const peerServer = ExpressPeerServer(server, { // Here we are actually defining our peer server that we want to host
    debug: true,
});
const path = require("path");

app.set("view engine", "ejs");
app.use("/public", express.static(path.join(__dirname, "static")));
app.use("/peerjs", peerServer); // Now we just need to tell our application to server our server at "/peerjs".Now our server is up and running

app.get("/", (req, res) => { // On the '/' route
    res.sendFile(path.join(__dirname, "static", "index.html")); // Send our Inro page file(index.js) which in the static folder.
});

app.get("/join", (req, res) => { // Our intro page redirects us to /join route with our query strings(We reach here when we host a meeting)
    res.redirect( // When we reach /join route we redirect the user to a new unique route with is formed using Uuid 
        url.format({ // The url module provides utilities for URL resolution and parsing.
            pathname: `/join/${uuidv4()}`, // Here it returns a string which has the route and the query strings.
            query: req.query, // For Eg : /join/A_unique_Number?Param=Params. So we basically get redirected to our old_Url/join/id?params
        })
    );
});

app.get("/joinold", (req, res) => { //Our intro page redirects us to /joinold route with our query strings(We reach here when we join a meeting)
    res.redirect(
        url.format({
            pathname: req.query.meeting_id,
            query: req.query,
        })
    );
});

app.get("/join/:rooms", (req, res) => { // When we reach here after we get redirected to /join/join/A_unique_Number?params
    res.render("room", { roomid: req.params.rooms, Myname: req.query.name }); // we render our ejs file and pass the data we need in it
}); // i.e we need the roomid and the username

io.on("connection", (socket) => { // When a user coonnects to our server
    socket.on("join-room", (roomId, id, myname) => { // When the socket a event 'join room' event
        socket.join(roomId); // Join the roomid
        socket.to(roomId).broadcast.emit("user-connected", id, myname);// emit a 'user-connected' event to tell all the other users
        // in that room that a new user has joined

        socket.on("messagesend", (message) => { 
            console.log(message);
            io.to(roomId).emit("createMessage", message);
        });

        socket.on("tellName", (myname) => {
            console.log(myname);
            socket.to(roomId).broadcast.emit("AddName", myname);
        });

        socket.on("disconnect", () => { // When a user disconnects or leaves
            socket.to(roomId).broadcast.emit("user-disconnected", id);
        });
    });
});

server.listen(process.env.PORT || 3030); // Listen on port 3030.
// process.env.PORT || 3030 means  use port 3000 unless there exists a preconfigured port


Here code is fairly simple :) I have explained all the code in the comments but just a note On the 13th line of the snippet, we are just telling express to use ejs as our template engine that we installed earlier. And express will look for our ejs file by default in the views folder(If having a doubt see the image again). But, if you want to change the views default path by:


app.set('views', path.join(__dirname, '/yourViewDirectory'));


So, create a views folder and this views folder will contain your ejs file. I have named by ejs file as ‘“room.ejs’’ but you can name it whatever you want just on the 40th line change the name accordingly.

Now, let’s create our ejs file(for the Main Clone page) which is in the views folder


Ejs File

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="http://localhost:3030/public/styles.css">
    <script src="https://unpkg.com/[email protected]/dist/peerjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/1.3.1/peerjs.min.js.map"></script>
    <script src="/socket.io/socket.io.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
    <script src="/socket.io/socket.io.js"></script>

    <script>
        const myname = "<%= Myname %>"
        const roomId = "<%= roomid %>"
    </script>
</head>

<body>
    <div class="modal fade" id="getCodeModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
        <div class="modal-dialog modal-sm">
            <div class="modal-content">
                <div class="modal-header">
                    <h4 class="modal-title">Invite People</h4>
                </div>
                <div class="modal-body">
                    <p id="roomid"><strong><%= roomid %></p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-white" onclick="cancel()">Close</button>
                    <button type="button" class="btn btn-primary" onclick="copy()">Copy</button>
                </div>
            </div>
        </div>
    </div>
    <div class="mainclone">
        <div class="main_left">
            <div class="main_videos">
                <div id="video-grids">
                </div>
            </div>
            <div class="main_controls">
                <div class="main_controls_block">
                    <div class="main_controls_button" id="mic" onclick="muteUnmute()">
                        <i class="fas fa-microphone-slash"></i>
                        <span>Mute</span>
                    </div>

                    <div class="main_controls_button" id="video" onclick="VideomuteUnmute()">
                        <i class="fas fa-video-slash"></i>
                        <span>Stop Video</span>
                    </div>
                </div>
                <div class="main_controls_block">
                    <div class="main_controls_button" onclick="invitebox()">
                        <i class="fas fa-user-plus"></i>
                        <span>Invite</span>
                    </div>
                    <div class="main_controls_button">
                        <i class="fas fa-user-friends"></i>
                        <span>Participants</span>
                    </div>
                    <div class="main_controls_button" onclick="showchat()">
                        <i class="fas fa-comment-alt"></i>
                        <span>Chat</span>
                    </div>
                </div>
                <div class="main_controls_block">
                    <div class="main_controls_button leave_red">
                        <span class="leave_meeting"><a href="/">Leave Meeting</a></span>
                    </div>
                </div>
            </div>
        </div>
        <div class="main_right" id="chat">
            <div class="main_right_header">
                <h6>Chat Area</h6>
            </div>
            <div class="main__chat_window" id="main__chat_window">
                <ul class="messages" id="messageadd">

                </ul>

            </div>
            <div>
                <div class="main__message_container">
                    <input type="text" id="chat_message" onkeydown="sendmessage(this)" placeholder="Type message here.." />
                </div>
            </div>
        </div>
    </div>
    <script src="http://localhost:3030/public/main.js"></script>
</body>

</html>


In the 21st and the 22nd line of the file, we are just adding the variables that we need in our main.js file. We add the variables that we defined in the server.js between this format <%= varName %> to get the data. Now, let’s also add our styles.css file(for the Main Clone page) which is the static folder


Styles.css file For Ejs file


* {
    margin: 0;
    padding: 0;
}

html,
body {
    height: 100%;
    font-family: "Roboto", sans-serif;
}

#video-grids {
    display: flex
}

a,
a:hover,
a:focus,
a:active {
    text-decoration: none;
    color: inherit;
}

.video-grid {
    position: relative;
    display: flex;
    justify-content: center;
    height: 100%;
    width: 100%;
    align-items: center;
    flex-wrap: wrap;
    overflow-y: auto;
    background-color: yellow;
}

h1 {
    position: absolute;
    bottom: 0;
    left: 40%;
}

video {
    display: block;
    flex: 1;
    object-fit: cover;
    border: 5px solid #000;
    max-width: 600px;
    border-radius: 25px;
}

.mainclone {
    height: 100%;
    display: flex;
    flex-direction: row;
}

.main_left {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.main_right {
    display: flex;
    flex: 0.2;
    flex-direction: column;
    background-color: #242324;
}

.main_controls_block {
    flex-direction: row;
    display: flex;
}

.main_controls {
    display: flex;
    flex-direction: row;
    background-color: #1C1E20;
    color: white;
    padding: 10px;
    justify-content: space-between
}

.main_controls_button {
    cursor: pointer;
    display: flex;
    flex-direction: column;
    padding: 3px 10px;
    min-width: 80px;
    align-items: center;
    justify-content: center;
}

.main_videos {
    flex-grow: 1;
    background-color: black;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 40px;
}

.leave_red {
    color: white;
    background-color: red;
    border-radius: 5%;
}

.main__message_container {
    padding: 20px 15px;
    display: flex;
}

.main__message_container input {
    flex-grow: 1;
    background-color: transparent;
    color: white;
    border: none;
    outline: none
}

.main__chat_window {
    flex-grow: 1;
    overflow-y: auto;
}

.main_controls_button:hover {
    background-color: #686666;
    border-radius: 25px;
}

.main_right_header {
    color: white;
    text-align: center;
    font-size: 20px;
    padding: 10px;
    border-bottom: 2px solid #3d3d42;
}

.message {
    color: white;
    list-style-type: none;
    border: 2px solid #3d3d42;
    margin-bottom: 5px
}

.main_right_header h6 {
    animation: animate 20s linear infinite;
    background-image: linear-gradient(to right, #f00, #ff0, #0ff, #0f0, #00f);
    background-size: 1000%;
}

@keyframes animate {
    0% {
        background-position: 0% 100%;
    }
    50% {
        background-position: 100% 0%;
    }
    100% {
        background-position: 0% 100%;
    }
}


Now, you might be saying what about the HTML and CSS file for our intro page. Yea I totally forgot about that, let’s add it and call it index.html in the static folder. Again please check File Structure if any doubt


Html file for Intro Page

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

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Zoom Clone</title>
    <link rel="stylesheet" href="public/style.css" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script
</head>

<body>
    <div class="Name-Page">
        <div class="form">
            <form class="known-form" action="/join">
                <input type="text" placeholder="Enter your name" name="name" />
                <button>Host a Meeting</button>
                <p><a href="javascript:show()">Join Meeting?</a></p>
            </form>

            <form class="unknown-form" action="/joinold">
                <input type="text" placeholder="Enter your name" name="name" />
                <input type="text" placeholder="Enter Meeting Id" name="meeting_id" />
                <button>Join Meeting</button>
                <p>
                    <a href="javascript:show()">Host Meeting?</a>
                </p>
            </form>
        </div>
    </div>
    <script>
        function show() {
            $('form').animate({ height:"toggle",opacity:"toggle"},"slow")
        }
    </script>
</body>

</html>


What about its styling now? Now, let’s add Style.css for our intro page


Style.css File For Intro Page

body {
    background-image: url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBUVFRgVFRUYGBgaGBgYGBgaGBgYGhgYGBgaGRgYGhocIS4lHB4rHxkYJjgmKy8xNTU1GiQ7QDs0Py40NTEBDAwMEA8QHxISHjQrJSw0NDQ0NDQ0NDQ9NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NP/AABEIALcBEwMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAAAgEDBQQGB//EAEgQAAIBAgIGBQcKBAQFBQAAAAECAAMRITEEEkFRYXEFBoGRsRMiMqHB0fAHFEJSU2Jyk9PhM4KS8SRzorIVI0Nj0lSDs7Ti/8QAGQEAAwEBAQAAAAAAAAAAAAAAAAECAwQF/8QAKREAAgICAQMDBAIDAAAAAAAAAAECESExAxJBURMikQQyYaGB4SMzsf/aAAwDAQACEQMRAD8A+RS18PN/q57uQ8ZoVOjrKalNtdRtAsy8XTMW34i9sZlsJs4tbM1JPKIhCEgsI6OQbiJJgSWulxrL2jd+0qjo5BuI7Jcay9o3ftHV5Qa2VRwLZ9239oZc/D94hMQGp0TpK3NN8EcarfdOavzU48rjbOLT9HZHZGFipIPZu3iUBrTYr2r0Q49OmAr72TJH/lNkPNJqn1R6e6IftlfZmJJWEJkal1YZHePDD2Sq0uzTkfUf7DvlIjkSh6TWM0+nvSX/ACtH/wDr05liaPTDYr/laP6tHpy4v2tESXuTMwxkWRL9FHnX3ed/TiPXYdsiKt0XJ0rI0s+dbd5v9OHs9c5rSx4topO3YRVKiUS5tNpD5GlfJ6gw3rSBxPNmFvwqfrTl6J0UO13NkUF3IzCC1wOJJCjiwlPSelF3LGwvsGSgCyqOAAA7JtH2xvuZv3So5mIPA+r9opWLHVthy8JiaCx1S8YU/ffZaDtsGXjHXkL8Cu2wf3ldo0iSxoiEIQGPTe3HhvG0QqJbLEHEHh74qid+g6C1S6qL2xJOCpxZjgoPulxi5YRMmllnBaE2Pm2jjBq+Iz1aRYdhLC/dIl+k/K+TP1F4fwzhXSmVgyMVK+iQSCOPb7Z2jSaVX+KNR/tEXAn76DxWx4GZJkSVNotwX9ndpnRzoAxsyH0XU6ytybfwNjwnFadeh6e9MnVOB9JSAysNzKcDOzyVGt6BFJ/qsf8AlsfuucU5NccYUpa+BW1v5MmE6NJ0R6barqVO4jZvG8cRKJDTWGWmnlBLVcpln4DdFXAX7vfEME6DZc6AjWXLaN37SiPTcqbiWVKYI1ly2j6v7SqvKJusMonV0fpZpOGtrDEOv1kYWZe0E+ozlkhZKbTtFNJqmdfSmiim/mnWVgGRvrI2KnnsPEETk1d+HjLjpTFQhPmrrau8axucd18bcTvnNeOTTdoSTSpnRQIxFsxbHvHrErLmRTaxBjV1sx3bORxHqheA7keUO898nyhta5lcIrHQ+ud/tnShshJGdl3fePgs41nTpBsqrwuf5sR/p1ZcXVsiWaRUbHI9/vkMts4ksp1SpBGwgi4BFwbjA4GQUault5GktL6TWepwa3mJ/KDc/eY7pjmXVqhcliSSSSb4kk4k32kmVS5SvWiYqtiyxEvGSnf4y4mDvsGXjxMlLuym+yJLjLZ6775Wy2ix0Ow9nCK7CqEgZJEZEJ98EmwborAnRo+is5CqpJOQAuTyAmjS6MCANWbUBxC2vUYfdT6I+81huvK9I6TsClJdRDgbG7uPvvmeQsvCaKCjmRDm3hFg0WnS/inXb7NWwH43GA/CtzxE5NL6RZwFwVB6KKNVV4gbTxNzxnGzkxYnydo4GoZt5Z1a6H0msdot6+3PthOa8Iuv8FdH5FhCEgomAMiEYGnovSjKuo6ipT+q30eKMMVPLDeDL/8AhqVfO0dixzNJrCoPw7HHLHhMaWhytrHHP3S1O1TyZuHdYCopBNxY5W3W2Sua69JrUGrpCl9gqLYVF5k4OODY8RKtK6KKqXpsKlMZsua/jTNe3DiYOF5WQUuzwZ0am5U3HxwiR1G345SUUy56YI11y2rtH/549k52MdKhBuP25W3RqiAjWXLaN37RunlCWMM54WjQtJLItLqhuAew9n7Wiau+Gvha3tghMWEnWMNc74CGprcgbyB34R9Ja7EjLZyGA9QERKpBuLXHCRrA8PWI79tBWbEkSwrEIiKCX0k1vecu2JSp39pj1HGS5eJ3xxXdkt9kTUqfRGXrJ3mUkRxjzkGDdglQkFUk4Ts0Lo96hsovhcnAADexOCjiZ3saFAbKr9vk1PqLnuX8UuPG2reEQ+RJ0sso0ToxnXXYhEGBdsF5C2LNwUE5S99PSlhRF2+0cDW/kXEJzxbiJwaX0i9RtZiThbdZfqgDBRwE5GEbml9vyJQbzL4LK1dmJJJJOJJNySdpO0ymTImTbezRJLQSJMiBQQhCIBmQjZFIjK5GRtH8rfMA9lvCPAslUJf5h3j/AFe6HkL5EHtt6jHT7CvyVoLn4ygxvjGKEDEWvh7/AGRDE8AAMv0bSnpsHRirDIg2+BwlAkwTa0DV7NM1qVb0l8k/10XzGP30Ho81w+7M9xbDZ8YyBlfs98UmU3exJVoUx0cg3EW0AJCGWMoOIw3jd+0QtukM2wRY2xpEyYsmAyZEiEAJMJEIgGU2lqJfLtvs/aUyQ1oJiZfVf6K5ePEym0eLKbsSVBeduirS9OozHHBFFi3Nzgq8rnlnOKMm7f47IRdMHlHdpvSjOuooCIMkUWXmdrHixJmYTLDKzCUm9iiktER8xyw93tiR6Y2b8Pd64kWLAy0UTtw54eMnUUZnuHvjpk2imSqE5SzXAyXvx/b1RWqnf3YeEVILZPkG3esQiXMI8BkWEISSgkgyIRgXiswAAOzLZjwk+UBzUcxh+3qlT5+ruwi2jtk0i/VU5EjmPaPdINI7MeWMqBmlobobA2lxSm60ROTir2cDi2G74MSes6P6MSs6oGALGwJxF5uN8nrfXT/V7pq/pmu5jH6pPFM+bmGzn4T6K3yev9an3t/4zl0r5OaxF0enyJa3+2TL6eSWKZceeLdNNHgoTX07q/XoPqVVsbXBBurDep+DNDozqpUregrEbWyA7TIXDN9ipc8I7Z5iF57XSfk70gLdCjH6ut7SJ5HTdCqUXKVUZGGasLYbxvHESZcco7LjOMtFEJKISbAXM9L0V1L0isoYLqqci1gDyvn2RRhKX2oJTjHbPMwnsNM+T7SVF11X4A3PvnldK0ZqbFXUqwzBjlxyjloUeSMnSZTJnpl6laS1JKiAPrqGAU3wYXF8rYdk79H+TnSCt2dFP1da577WlLhm+wnyw8ni1aM03OlOqleibFSdwAuT+G2c0OjeoOlVVDPqUgdjm7dyg27YelNOmgXLBq0zyV4T2mndQ6lNda+uBmVN7cxa88zpuhvSOQtvAjlwyS6nr8Ex54SfSt/nBzFCcbZ4/HbFKDaw5DH9pJckC5v8f3lTTNtGqTH11GQvz/aHljsw5YSqEVsdIsc4mJGbZy8MPZFgIiTIkxFBCEIwItJtNQVtG+zqfnJ+lJFfRvs6n5yfpTT015Rl1vwZVpKriOc1fL6N9nU/OT9KMtfRrj/l1M/tU/SjXEvKD1H4Zktmeci01/nGi/Z1Pzk/Sh840X7Op+cn6UPSXlB6j8MyIyHwPhNJn0U/9Or+cn6MpreQIOotQNvaorjjgKan1xOFd0NSvsyijpboQVYgjEY5ET12g9d9MsAdRuJTE87ETyujaLczf6N6OZ2CIt2OQ9p3Cb8EJvvg5ufkgtLJ6bQutWlOwUIhJwACtj/qnr6DuEBqFda121cFHefXM7onopNGQsxGva7ucABtAvkvjPGdZ+tflW8lSNqd8TkX9y8J0ylGJhCMnlnodKqJptdEUXSnrFn+sML24YATr6wdYaWgoqhbsR5iDAADaTsHjMzqIRd9+qvdc39k8p8o4b52dbIohXla3jrSeaXTC0Pgh1ybZp6J8pVTWGvRQpfHVLBgN+NwZ67pfo+j0jowK2uQWpvbzkfceF8CP2M+J2n2D5PUYaMdbLWw/pF/ZOfhk5pqR08qUGnE878n/V0M71Ky/wANtXVO1wcjwFvWJvdauuyaM5pU0D1B6VzZU3A2zPCbHV10Ir6n/qal+eF58Z6eVhpNcN6XlXvf8R9lpXI/TilEnjXXJuR7fon5SbsF0ikqqTbWQnzeJU5jkZt9dugk0igaqAa6rrBh9IW37fjdPjwn27oNWXo6mHvcaPiDnbVNh3Wk8M3O4yyPnioLqjtHZ0I4XRKTHIUUJ5BBPEdI/KNUWqVp0kNNWI84trNY2NiMB3GeyQ/4IWy8ktuWqJ8Nrek3M+Jl88nCKonhSlJ34/6feOhOlE0qklUDPGxtdWF1Ycwbi8wOtXXFtHIWkqsxJF2uR5uZsCML4Q+TI/4Qfjf/AHTxfXT+MvNv9xlOT9Lq70iK/wAyj2t/o9v1R63nS9anUULUUa3m31WW9iQDiLEjDiJx9bujVBNhYMutbdsI+N88z8nJ/wAU3+U/+5J7brfkn4D4yvp25Rz+TP6xKLTX4PkTCxI3N74jTp1kDtrqzLc4K4U3vh5xVvCdIraKM6Vc/wDvJ7KU4OlPFnoW6Toy4TW+caH9hW/OT9OHzjQ/sKv5yfpR9C8oOt+GZhyHaPjvkTX+caJYf8mrt/6qcP8AtSPnGifZVfzU/Sj6F5RPW/DMeSJr/ONF+yq/mp+lD5xov2VT85P0oemvKDrfhmRCa/zjRfsqn5qfpQj9NeUPrfhmPCEJiahJU4yIRgS+Z5yIz588e/GLBgE7dCpgmcVpdQqFThKg0pWyJpuNI9FotAT6f0N0QlFBqYsQCz7W5bhwnxyn0kybptaL170imoRdUgYDWF7cAZ3+vDpq6PPXBydVtWe96w9C6RpI1ErKlPauqxLHexuMNwnlT8mtW9/nK3/A3/lOE/KHpewJ/SIr/KFpmzU/oEylLills6Ix5FhI3dG0Kr0e6OzK6E6jFbjPYQcsrjlN/pjoTR9PpqdbLFHW2st8wb7N4M+Y9KdbdJ0hSlRl1SQbBQMRliMZy9H9YdIo+hUI549++Eufja6WTHgnF9S+D3GgfJsiNrVK5dQcgmqe8sfCbfWHpmjoNDUSwfVtTQZgn6TcNuOc+eVuvGmsLeVC8VUA988/pFd3Ys7FmOZJuTIfNGMaijRcUpSuR63qJ1mFCqyVm8yoblj9F75ngb4z2XWPqfQ0w+VV9SoQPPUBlcAeaWW+OG0HKfHJrdF9Y9J0catKqwX6p85e45dkiPKmqkrKlxtPqie76I+TmlTYNWqGqAbhQmqpt9bEkjhlLevfWRKVI0KbAuwsbfRE8VpfXPTKi6pq6o26oCzz9RyxJYkk5k4kynzRiqgifSlN+/R9t0ZgNAp/5CDvVQJ8SrekebeJmvU6zaS1JaBqeYoCiwAOqBYC44YXzmNI5uRTSS7FcXG4ybZ9Z+TFwdFsDiHcHhiD4ETxvXX+Mv8AP6mmR0R0zX0Zi1Fyt/SFgytbepw7ZPSvSb12DOBrC4wBF7m5wJ3y/Wi+Jx74/RD4ZesprWf2bHydMBpdjtpuBxN1PgDPddbsk/A3iJ8h0bSGpsroSrKbqymxBm/X63V6ihamo1gQDq6px32w2Svp+ZRxIn6rglyfaYVYec34vfKml1S5N7ZknDEYylpzSOmOiJEmRJLLDkO0+z2RYzbOXjj7YpjELJhCAwhCEAIhGFM/3whqjf3RUxWLCNcbpGtGMdlwHd3fAiWElcj3xYAMDJvFEmADPv8AjD49cSOuIt2j4+MopiYAJOYt8cYsm8YCwEdlviO2LEAQhCMAtCEmAC2hGkQAiEmSFvEBCDb8X2QaMcPZ74sZJEkCEdBt7ucRRN93Luka522PP35waJHZI114j1yRTvkQfV4xIyDM7v7QAZ1N8ohjKxG2Tr7wD6vCGAyVyYwCnePXJFM7MfjdCh2JCP5Pn3SIUwsrJhCEQwhCEYDKbGDLYyAJ1UNFZyFVSzZAAXJ7BHGLeES5JbOZVJnZovRzvfVGA9JiQqqN7McB7dk7ho9Ol/EId/qI3mg/fcf7VvzE49M6Td7LgFHooo1UXko28TjxmnRGP3EdUnovIo0hhaq/12BCKfuIcWPFsPuzOe2YGHtiE3kqZEpJ6RSTW2JLKdO8daVzwzvstvhUcZDLx4mJLuxt9kQzbB/fnEK3y7vdEk3ibsdEQll78ZJp4XvttjFQWUyYxSRqcu8RjIhHVLm14Wt++PqiokULH1re73xC/wDfbFjAsYXxHaN37RJKtbGMVuLjtG79obHohROzRa6AFXTXS99ZTqupyLK2R/CwIw2ZzlAth38OEVjBOgeTQr9GEqXoN5VALtYWqIPv08SB95brxmbHpVmRgyMVZTdWUkEHgRlNI6ZSrfx11H+3pqMT/wBymLA/iWx4NHh/gWUZUsIsLdvu+OM6tK6MemA5s9M+jUQ6yNw1vot91rHhOQmKmthaeiIsYyBACJIgYQKG123nvhEhCxURCEZUhsHgUCWJTnZovR5ZdZiET674DkoGLHgAeyX/AD5KeFEHW+0cDX/kXEIO9uMuMKVyIlO8Imn0cEAas2oMwgF6jDgp9EcWtwBi1+lLApTUIhwIBuzj775tywXhM6pWLEkm5OJJxJO8yuN8lYQlC8ssqMezZKo6nYeyKRIeS0Al1NL+07uMWnTv4nhHqVNgy8TvMEu7Bvshmq/RGXrvv58JSRFkhoN2CVCwnVV0RgqMbDXBYKTjqg6obkSDblynMRaJprY07AS+ubBRwv34+FolBNZgN5t3ydIe7E7L4ctglLCbJeWkVl5BcyJEixjhznJrizHdmORxESXVBdVbmp7Dceo+qNZTDTRRCEenTuQCQoJAuchc5m2NoihBLUYg3Ge/d741egyMyMNVlJVhxHHdylRbdDQtlrAWuvaN3HlKZKMQbjOOygi4HMbuXCN5ForhCWILY93vgUdOh6a9EnUa1/TUgMjD6rIcGHOdepQreiRo9T6rEmi5+6xxpng1xxEyZDGUpVh6IavPc6NM0R6TarqVOdjtGwgjBhxGE5536H0oyL5N1FSn9R7kDeUYYoeI9cv/AOGpVx0diW20WIDj8JyqDlY8I+lPXwHVW/kyZEaohBIIIIwIIsQeMWQMISYQKo7aWhM581bjMnAKo26zHBQOPCX61Knlaq+8g+TXkMC/bYcDKa3SDONXBU+iqjVUNsPE7Lm5nCxM1bitGKjJ7L9K0tnN2Yscsdg3ADADgMJzEwhMnJvZqopYQQhCAyZfSTWwyttOVuMSml/adwjVHwsMvE7zGlWWS32QVH+iMB4nefjCUxwb557/AHxWW0TdgsC3mj0ZoQdrubIoLuRmEGduJJCjiwnFSS58Zs6a/kaYpDB2s9TgbeYn8oOseLcJpxxX3PRE5PS2cHSOla7k2AGQUZKoFlUcAAB2TmDdvOJeF5EnbsuKpUdeigDWbKynjifNHjfsnOVvtHxzlzGycz6lHvY9057ypYSRMctsCh4d4ktSIzFsjjhgRcHHYQQe2Aml02uq6b/I6Pfn83piSo2mym6aRmanLv8AdLqagowve1m9h8ZVeWaO/nc8O/CONWJ6K+WHrMgmDYYRZIzXrjy2jioPTogJUG1qeVN+Orgh/lmPO7orTPJVAxGspBV12MjCzL3eu0OldC8lUIB1kIDo31kbFTztgeIMtq1fyJYdfBxiSpINxIlgW2J7vfIGTqA+ds3e7hEJvG1je8hhtHdGwFMWEIiggDuhCAGtT6UWoAukqXGQqAgVVGzzvpjg3YREr9FmxekwqptKjzl/Gma88RxmZOvRa70zrqxVslIwPE8tn9pakniXyZuLWV8Ea+rhq3tt3nb64Tt/4yPpaNQY7TZhc77KQB2Ql+3z+iPd4/Zjy1vOF9osDx3HnCExWjZ7KoQhEMI9NL/GwZwhGtieh6j7Bl48TKYQjkJEx1bYcvjKEIgNforRwoaqQGVLGx+kxNqYI3XxPBSNszNKrF2JJJJJJJzJJuSeN4Qm3JiKMoZkyiMoxhCYo1LtKOQ3AevE+smc8IRz2xR0h6PpDmJp9YvTT/Kof/BThCXH/W/4Il96/kyZIMITM2LK+d9+PfK4Qg9krRE29FXy9BqR9OiC6H7mTpfhmP5t8ITTj+5kcmrMnAcTv90gwhILCLeEIgBhtiwhAAkQhAospJfgLXJ4SHa5v2AbhsEmEOwu4sIQgM//2Q==");
    height: 100vh;
    background-size: cover;
    background-position: center;
}

.Name-Page {
    width: 400px;
    padding: 10% 0 0 0;
    margin: auto;
}

.form {
    position: relative;
    z-index: 1;
    background-color: white;
    max-width: 200px;
    margin: 0 auto 100px;
    padding: 25px;
    text-align: center
}

.form input {
    font-family: "Roboto", "sans-serif";
    outline: 1;
    background: rgb(226, 218, 218);
    width: 100%;
    border: 0;
    margin: 0 0 15px;
    padding: 20px;
    box-sizing: border-box;
    font-size: 14px
}

.form button {
    font-family: "Roboto", "sans-serif";
    outline: 0;
    background: #4CAF50;
    width: 100%;
    border: 0;
    padding: 10px;
    color: #ffffff;
    font-size: 10px;
    cursor: pointer;
}

.form p {
    font-family: "Roboto", "sans-serif";
    font-size: 60%;
    margin: 15px
}

.form button:hover {
    background: #43A047
}

.form .unknown-form {
    display: none
}


Ok, so this was all the CSS and HTML for this project. Now it is time for the main.js file which will be executed in the client’s browser. I have mostly examined all the code 👇👇 in the comments.

Main.js File for Client


const socket = io("/"); // require socket.io for the client side to emit events that will received by the server socket.io
const main__chat__window = document.getElementById("main__chat_window"); // Get the Div where are messages are going to be there
const videoGrids = document.getElementById("video-grids"); // This div will contain various more div in which the video element and name will appear
const myVideo = document.createElement("video"); // This video element will show us our own video
const chat = document.getElementById("chat"); // Get our main right div
    OtherUsername = ""; // It will hold our other user's name
chat.hidden = true; // Hide the chat window at first
myVideo.muted = true; // Sets The video's audeo to mute

window.onload = () => { // When Window load
    $(document).ready(function() {
        $("#getCodeModal").modal("show"); // Show our modal
    });
};

var peer = new Peer(undefined, { // Now with our peer server up an running, let's connect our cient peer js to ther server
    path: "/peerjs",
    host: "/",
    port: "3030",
});

let myVideoStream;
const peers = {};
var getUserMedia =
    navigator.getUserMedia ||
    navigator.webkitGetUserMedia ||
    navigator.mozGetUserMedia;

sendmessage = (text) => {
    if (event.key === "Enter" && text.value != "") { // When enter is pressed and the type message box is not empty
        socket.emit("messagesend", myname + ' : ' + text.value); // Emit a send message event and pass chat message with userName
        text.value = ""; // Empty chat message box
        main__chat_window.scrollTop = main__chat_window.scrollHeight; // Scroll down
    }
};

navigator.mediaDevices // Webrtc provides a standard api for accessing cameras and microphones connected to the device
    .getUserMedia({
        video: true,
        audio: true,
    }) // It returns a promise here 
    .then((stream) => { // If permission is granted, it gives us the video and the audio track
        myVideoStream = stream;
        addVideoStream(myVideo, stream, myname); // This function add the div which contains the video and the name. Basically it add our video to the screen

        socket.on("user-connected", (id, username) => { // When server emits the "user-connected" event for all the cleints in the room
            //console.log("userid:" + id);
            connectToNewUser(id, stream, username); // We run this function and pass user's id, stream and user's name as arguments(Explnation At function)
            socket.emit("tellName", myname); // Emit a tellName emit to tell other clients thir name
        });

        socket.on("user-disconnected", (id) => {
            console.log(peers);
            if (peers[id]) peers[id].close();
        });
    });
peer.on("call", (call) => { // When We get a call
    getUserMedia({ video: true, audio: true }, // Get our stream
        function(stream) {
            call.answer(stream); // Answer the call with our stream
            const video = document.createElement("video");  // Create a video element
            call.on("stream", function(remoteStream) { // Get other user's stream
                addVideoStream(video, remoteStream, OtherUsername); // And other user's stream to our window
            });
        },
        function(err) {
            console.log("Failed to get local stream", err);
        }
    );
});

peer.on("open", (id) => { // When ever user joins every user is given a unique id and its very imposrtant to know their id when communicating
    socket.emit("join-room", roomId, id, myname); 
});

socket.on("createMessage", (message) => { // THis function appends a message to the chat area when we or ther user sends message
    var ul = document.getElementById("messageadd");
    var li = document.createElement("li");
    li.className = "message";
    li.appendChild(document.createTextNode(message));
    ul.appendChild(li);
});

socket.on("AddName", (username) => { // Tell other user their name
    OtherUsername = username;
    console.log(username);
});

const RemoveUnusedDivs = () => { // This function is used to remove unused divs whenever if it is there
    //
    alldivs = videoGrids.getElementsByTagName("div"); // Get all divs in our video area
    for (var i = 0; i < alldivs.length; i++) { // loop through all the divs
        e = alldivs[i].getElementsByTagName("video").length; // Check if there is a video elemnt in each of the div
        if (e == 0) { // If no
            alldivs[i].remove // remove
        }
    }
};

const connectToNewUser = (userId, streams, myname) => {
    const call = peer.call(userId, streams); // This will call the other user id with our own stream
    const video = document.createElement("video"); 
    call.on("stream", (userVideoStream) => { // When other user answers the call they send their steam to this user
        //       console.log(userVideoStream);
        addVideoStream(video, userVideoStream, myname); // And that stream
    });
    call.on("close", () => { // When call closses
        video.remove();  // Remove that video element
        RemoveUnusedDivs(); // Remove all unused divs
    });
    peers[userId] = call;
};

const cancel = () => { // Hide our invite modalwhen we click cancel
    $("#getCodeModal").modal("hide");
};

const copy = async() => { // copy our Invitation link when we press the copy button
    const roomid = document.getElementById("roomid").innerText;
    await navigator.clipboard.writeText("http://localhost:3030/join/" + roomid);
};
const invitebox = () => { // SHow our model when we click
    $("#getCodeModal").modal("show");
};

const muteUnmute = () => { // Mute Audio
    const enabled = myVideoStream.getAudioTracks()[0].enabled; // Audio tracks are those tracks whose kind property is audio. Chck if array in empty or not
    if (enabled) { // If not Mute
        myVideoStream.getAudioTracks()[0].enabled = false; // Mute
        document.getElementById("mic").style.color = "red"; // Change color
    } else {
        document.getElementById("mic").style.color = "white"; // Change color
        myVideoStream.getAudioTracks()[0].enabled = true; // UnMute
    }
};

const VideomuteUnmute = () => {
    const enabled = myVideoStream.getVideoTracks()[0].enabled;
    if (enabled) { // If Video on
        myVideoStream.getVideoTracks()[0].enabled = false; // Turn off
        document.getElementById("video").style.color = "red"; // Change Color
    } else {
        document.getElementById("video").style.color = "white"; // Change Color
        myVideoStream.getVideoTracks()[0].enabled = true; // Turn On 
    }
};

const showchat = () => { // Show chat window or not
    if (chat.hidden == false) { 
        chat.hidden = true; // Dont Show
    } else {
        chat.hidden = false; // SHow
    }
};

const addVideoStream = (videoEl, stream, name) => { 
    videoEl.srcObject = stream; // Set the stream to the video element that we passed as arguments
    videoEl.addEventListener("loadedmetadata", () => { // When all the metadata has been loaded
        videoEl.play(); // Play the video
    });
    const h1 = document.createElement("h1"); // Create 1 h1 elemnt to display name
    const h1name = document.createTextNode(name); // Create a text node (text). Note: To display an proper h1 element with text, its important to create an h1 and a text node both 
    h1.appendChild(h1name); // append text to h1 element
    const videoGrid = document.createElement("div"); // Create a div 'videoGrid' inside the "videoGridS" div
    videoGrid.classList.add("video-grid"); // add a class to videoGrid div
    videoGrid.appendChild(h1); // append the h1 to the div "videoGrid"
    videoGrids.appendChild(videoGrid);  // append the name to the the div "videoGrid"
    videoGrid.append(videoEl); // append the video element to the the div "videoGrid"
    RemoveUnusedDivs(); // Remove all unsed divs
    let totalUsers = document.getElementsByTagName("video").length; // Get all video elemets
    if (totalUsers > 1) { // If more users than 1
        for (let index = 0; index < totalUsers; index++) { // loop through all videos
            document.getElementsByTagName("video")[index].style.width = // Set the width of each video to
                100 / totalUsers + "%"; // 👈👈
        }
    }
};


Test


I have explained all the above code in comments but if you have any other doubt, you can of course comment and ask. All this code is also available on my Github page. So, just clone the repo to test


So, that’s all for it. You can enhance this Zoom clone like whatever you want and test your own creativity. Like you can emit a function to change video when the user turns off his video.


Till then stay safe, stay healthy.

Thank You.


This article was first published here on medium

If you wanna see this zoom clone Runs, check on medium only.