├── .gitignore ├── README.md ├── babel.config.json ├── nodemon.json ├── package-lock.json ├── package.json └── src ├── public └── js │ └── app.js ├── server.js └── views └── home.pug /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Noom 2 | 3 | Zoom Clone using NodeJS, WebRTC and Websockets. 4 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["src/public/*"], 3 | "exec": "babel-node src/server.js" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noom", 3 | "version": "1.0.0", 4 | "description": "Zoom Clone using NodeJS, WebRTC and Websockets.", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon" 8 | }, 9 | "devDependencies": { 10 | "@babel/cli": "^7.14.5", 11 | "@babel/core": "^7.14.6", 12 | "@babel/node": "^7.14.7", 13 | "@babel/preset-env": "^7.14.7", 14 | "nodemon": "^2.0.12" 15 | }, 16 | "dependencies": { 17 | "@socket.io/admin-ui": "^0.2.0", 18 | "express": "^4.17.1", 19 | "localtunnel": "^2.0.1", 20 | "pug": "^3.0.2", 21 | "socket.io": "^4.1.3", 22 | "ws": "^7.5.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/public/js/app.js: -------------------------------------------------------------------------------- 1 | const socket = io(); 2 | 3 | const myFace = document.getElementById("myFace"); 4 | const muteBtn = document.getElementById("mute"); 5 | const cameraBtn = document.getElementById("camera"); 6 | const camerasSelect = document.getElementById("cameras"); 7 | const call = document.getElementById("call"); 8 | 9 | call.hidden = true; 10 | 11 | let myStream; 12 | let muted = false; 13 | let cameraOff = false; 14 | let roomName; 15 | let myPeerConnection; 16 | let myDataChannel; 17 | 18 | async function getCameras() { 19 | try { 20 | const devices = await navigator.mediaDevices.enumerateDevices(); 21 | const cameras = devices.filter((device) => device.kind === "videoinput"); 22 | const currentCamera = myStream.getVideoTracks()[0]; 23 | cameras.forEach((camera) => { 24 | const option = document.createElement("option"); 25 | option.value = camera.deviceId; 26 | option.innerText = camera.label; 27 | if (currentCamera.label === camera.label) { 28 | option.selected = true; 29 | } 30 | camerasSelect.appendChild(option); 31 | }); 32 | } catch (e) { 33 | console.log(e); 34 | } 35 | } 36 | 37 | async function getMedia(deviceId) { 38 | const initialConstrains = { 39 | audio: true, 40 | video: { facingMode: "user" }, 41 | }; 42 | const cameraConstraints = { 43 | audio: true, 44 | video: { deviceId: { exact: deviceId } }, 45 | }; 46 | try { 47 | myStream = await navigator.mediaDevices.getUserMedia( 48 | deviceId ? cameraConstraints : initialConstrains 49 | ); 50 | myFace.srcObject = myStream; 51 | if (!deviceId) { 52 | await getCameras(); 53 | } 54 | } catch (e) { 55 | console.log(e); 56 | } 57 | } 58 | 59 | function handleMuteClick() { 60 | myStream 61 | .getAudioTracks() 62 | .forEach((track) => (track.enabled = !track.enabled)); 63 | if (!muted) { 64 | muteBtn.innerText = "Unmute"; 65 | muted = true; 66 | } else { 67 | muteBtn.innerText = "Mute"; 68 | muted = false; 69 | } 70 | } 71 | function handleCameraClick() { 72 | myStream 73 | .getVideoTracks() 74 | .forEach((track) => (track.enabled = !track.enabled)); 75 | if (cameraOff) { 76 | cameraBtn.innerText = "Turn Camera Off"; 77 | cameraOff = false; 78 | } else { 79 | cameraBtn.innerText = "Turn Camera On"; 80 | cameraOff = true; 81 | } 82 | } 83 | 84 | async function handleCameraChange() { 85 | await getMedia(camerasSelect.value); 86 | if (myPeerConnection) { 87 | const videoTrack = myStream.getVideoTracks()[0]; 88 | const videoSender = myPeerConnection 89 | .getSenders() 90 | .find((sender) => sender.track.kind === "video"); 91 | videoSender.replaceTrack(videoTrack); 92 | } 93 | } 94 | 95 | muteBtn.addEventListener("click", handleMuteClick); 96 | cameraBtn.addEventListener("click", handleCameraClick); 97 | camerasSelect.addEventListener("input", handleCameraChange); 98 | 99 | // Welcome Form (join a room) 100 | 101 | const welcome = document.getElementById("welcome"); 102 | const welcomeForm = welcome.querySelector("form"); 103 | 104 | async function initCall() { 105 | welcome.hidden = true; 106 | call.hidden = false; 107 | await getMedia(); 108 | makeConnection(); 109 | } 110 | 111 | async function handleWelcomeSubmit(event) { 112 | event.preventDefault(); 113 | const input = welcomeForm.querySelector("input"); 114 | await initCall(); 115 | socket.emit("join_room", input.value); 116 | roomName = input.value; 117 | input.value = ""; 118 | } 119 | 120 | welcomeForm.addEventListener("submit", handleWelcomeSubmit); 121 | 122 | // Socket Code 123 | 124 | socket.on("welcome", async () => { 125 | myDataChannel = myPeerConnection.createDataChannel("chat"); 126 | myDataChannel.addEventListener("message", (event) => console.log(event.data)); 127 | console.log("made data channel"); 128 | const offer = await myPeerConnection.createOffer(); 129 | myPeerConnection.setLocalDescription(offer); 130 | console.log("sent the offer"); 131 | socket.emit("offer", offer, roomName); 132 | }); 133 | 134 | socket.on("offer", async (offer) => { 135 | myPeerConnection.addEventListener("datachannel", (event) => { 136 | myDataChannel = event.channel; 137 | myDataChannel.addEventListener("message", (event) => 138 | console.log(event.data) 139 | ); 140 | }); 141 | console.log("received the offer"); 142 | myPeerConnection.setRemoteDescription(offer); 143 | const answer = await myPeerConnection.createAnswer(); 144 | myPeerConnection.setLocalDescription(answer); 145 | socket.emit("answer", answer, roomName); 146 | console.log("sent the answer"); 147 | }); 148 | 149 | socket.on("answer", (answer) => { 150 | console.log("received the answer"); 151 | myPeerConnection.setRemoteDescription(answer); 152 | }); 153 | 154 | socket.on("ice", (ice) => { 155 | console.log("received candidate"); 156 | myPeerConnection.addIceCandidate(ice); 157 | }); 158 | 159 | // RTC Code 160 | 161 | function makeConnection() { 162 | myPeerConnection = new RTCPeerConnection({ 163 | iceServers: [ 164 | { 165 | urls: [ 166 | "stun:stun.l.google.com:19302", 167 | "stun:stun1.l.google.com:19302", 168 | "stun:stun2.l.google.com:19302", 169 | "stun:stun3.l.google.com:19302", 170 | "stun:stun4.l.google.com:19302", 171 | ], 172 | }, 173 | ], 174 | }); 175 | myPeerConnection.addEventListener("icecandidate", handleIce); 176 | myPeerConnection.addEventListener("addstream", handleAddStream); 177 | myStream 178 | .getTracks() 179 | .forEach((track) => myPeerConnection.addTrack(track, myStream)); 180 | } 181 | 182 | function handleIce(data) { 183 | console.log("sent candidate"); 184 | socket.emit("ice", data.candidate, roomName); 185 | } 186 | 187 | function handleAddStream(data) { 188 | const peerFace = document.getElementById("peerFace"); 189 | peerFace.srcObject = data.stream; 190 | } 191 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import SocketIO from "socket.io"; 3 | import express from "express"; 4 | 5 | const app = express(); 6 | 7 | app.set("view engine", "pug"); 8 | app.set("views", __dirname + "/views"); 9 | app.use("/public", express.static(__dirname + "/public")); 10 | app.get("/", (_, res) => res.render("home")); 11 | app.get("/*", (_, res) => res.redirect("/")); 12 | 13 | const httpServer = http.createServer(app); 14 | const wsServer = SocketIO(httpServer); 15 | 16 | wsServer.on("connection", (socket) => { 17 | socket.on("join_room", (roomName) => { 18 | socket.join(roomName); 19 | socket.to(roomName).emit("welcome"); 20 | }); 21 | socket.on("offer", (offer, roomName) => { 22 | socket.to(roomName).emit("offer", offer); 23 | }); 24 | socket.on("answer", (answer, roomName) => { 25 | socket.to(roomName).emit("answer", answer); 26 | }); 27 | socket.on("ice", (ice, roomName) => { 28 | socket.to(roomName).emit("ice", ice); 29 | }); 30 | }); 31 | 32 | const handleListen = () => console.log(`Listening on http://localhost:3000`); 33 | httpServer.listen(3000, handleListen); 34 | -------------------------------------------------------------------------------- /src/views/home.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="UTF-8") 5 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 6 | meta(name="viewport", content="width=device-width, initial-scale=1.0") 7 | title Noom 8 | link(rel="stylesheet", href="https://unpkg.com/mvp.css") 9 | body 10 | header 11 | h1 Noom 12 | main 13 | div#welcome 14 | form 15 | input(placeholder="room name", required, type="text") 16 | button Enter room 17 | div#call 18 | div#myStream 19 | video#myFace(autoplay,playsinline, width="400", height="400") 20 | button#mute Mute 21 | button#camera Turn Camera Off 22 | select#cameras 23 | video#peerFace(autoplay,playsinline, width="400", height="400") 24 | script(src="/socket.io/socket.io.js") 25 | script(src="/public/js/app.js") --------------------------------------------------------------------------------