├── .gitignore ├── public ├── img │ ├── video-bg.png │ ├── landing-page-girl.jpg │ ├── keyboard-box-fill.svg │ ├── video-add-line 1.svg │ ├── icon.svg │ ├── share.svg │ ├── video-call.svg │ ├── video-chat.svg │ └── undraw_video_call_kxyp.svg ├── js │ ├── index_page.js │ └── script.js ├── index.html └── css │ ├── style.css │ └── index-style.css ├── package.json ├── server.js ├── README.md ├── LICENSE └── views └── room.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /public/img/video-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhawal-kamdar/Speak-Video-Chat/HEAD/public/img/video-bg.png -------------------------------------------------------------------------------- /public/img/landing-page-girl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhawal-kamdar/Speak-Video-Chat/HEAD/public/img/landing-page-girl.jpg -------------------------------------------------------------------------------- /public/img/keyboard-box-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speak", 3 | "version": "1.0.0", 4 | "description": "Speak is a free video chatting website", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server", 8 | "dev": "nodemon server" 9 | }, 10 | "author": "Dhawal Kamdar", 11 | "license": "MIT", 12 | "dependencies": { 13 | "ejs": "^3.1.3", 14 | "express": "^4.17.1", 15 | "socket.io": "^2.3.0", 16 | "uuid": "^8.3.0" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/img/video-add-line 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/js/index_page.js: -------------------------------------------------------------------------------- 1 | // URL Copy To Clipboard 2 | document.getElementById("share-button").addEventListener("click", getURL); 3 | 4 | function getURL() { 5 | const c_url = window.location.href; 6 | copyToClipboard(c_url); 7 | alert("Url Copied to Clipboard,\nShare it with your Friends!\nUrl: " + c_url); 8 | } 9 | 10 | function copyToClipboard(text) { 11 | var dummy = document.createElement("textarea"); 12 | document.body.appendChild(dummy); 13 | dummy.value = text; 14 | dummy.select(); 15 | document.execCommand("copy"); 16 | document.body.removeChild(dummy); 17 | } 18 | 19 | // Invite Link Input 20 | function getInputValue() { 21 | var url = document.getElementById("invite-link-input").value; 22 | var code = url.split("/"); 23 | window.open(code[3]); 24 | } 25 | -------------------------------------------------------------------------------- /public/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const server = require("http").Server(app); 4 | const io = require("socket.io")(server); 5 | const { v4: uuidV4 } = require("uuid"); 6 | app.set("view engine", "ejs"); 7 | app.use(express.static("public")); 8 | 9 | app.get("/", (req, res) => { 10 | res.render("index"); 11 | }); 12 | 13 | app.get("/create-room/", (req, res) => { 14 | res.redirect(`/${uuidV4()}`); 15 | }); 16 | 17 | app.get("/:room", (req, res) => { 18 | res.render("room", { roomId: req.params.room }); 19 | }); 20 | 21 | io.on("connection", (socket) => { 22 | socket.on("join-room", (roomId, userId) => { 23 | socket.join(roomId); 24 | socket.to(roomId).broadcast.emit("user-connected", userId); 25 | 26 | socket.on("disconnect", () => { 27 | socket.to(roomId).broadcast.emit("user-disconnected", userId); 28 | }); 29 | }); 30 | }); 31 | 32 | const PORT = process.env.PORT || 3000; 33 | 34 | server.listen(PORT, () => console.log(`Server Started on ${PORT}`)); 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Speak Video Chat 2 | 3 | Speak is a free multi-room multi-user video chatting website. 4 | 5 | ## Demo 6 | 7 | https://speak-video-chat.onrender.com/ 8 | 9 | ## Developed using 10 | 11 | - Node.js 12 | - Express.js 13 | - Socket.IO 14 | - PeerJs 15 | 16 | ## How to install 17 | 18 | Make sure you have Node.js installed in your system. 19 | 20 | Clone this repo 21 | 22 | git clone https://github.com/dhawal-kamdar/Speak-Video-Chat.git 23 | 24 | Install PeerJS globally. 25 | 26 | npm i -g peerjs 27 | 28 | Now to install all required node modules 29 | 30 | npm i 31 | 32 | ## How to use 33 | 34 | Run Server 35 | 36 | node server 37 | 38 | Server Started on Port 3000. 39 | 40 | Run PeerJS Server in separate terminal. 41 | 42 | peerjs --port 3001 43 | 44 | PeerJS Server Started on Port 3001. 45 | 46 | ## Let's Start 47 | 48 | Open browser and goto http://localhost:3000/ 49 | 50 | **Important :** Allow the camera and audio permissions asked by the browser. 51 | 52 | Now copy the url and open in another tab. 53 | 54 | Enjoy the video chat! 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dhawal Kamdar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/img/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /views/room.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | 26 | 30 | 31 | 32 | Speak 33 | 34 |
35 |

Let's Speak!

36 |
Your View
37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /public/img/video-call.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | Speak 24 | 25 | 26 |
27 | 36 |
37 |
38 |
39 |
40 | video call svg image 44 |
45 |
46 |
47 |

Enjoy Free Video Chat.

48 |
49 |
50 |
51 | 59 |
60 |
61 | 66 | 67 |
68 |
69 |
70 |

It's Free, Try Now!

71 |
72 |
73 |
74 |
75 | girl on video call 76 |
77 |
78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Global Styles */ 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | font-size: 62.5%; 11 | --header1: calc(2.5rem + 1.2vw); 12 | --header2: calc(2rem + 1.2vw); 13 | --header3: calc(1.5rem + 1.2vw); 14 | --header4: calc(1rem + 1.2vw); 15 | --header5: calc(0.8rem + 1.2vw); 16 | --header6: calc(0.5rem + 1.2vw); 17 | --header7: calc(0.2rem + 1.2vw); 18 | scroll-behavior: smooth; 19 | } 20 | 21 | h1 { 22 | font-size: var(--header1); 23 | } 24 | 25 | h2 { 26 | font-size: var(--header2); 27 | } 28 | 29 | h3 { 30 | font-size: var(--header3); 31 | } 32 | 33 | h4 { 34 | font-size: var(--header4); 35 | } 36 | 37 | h5 { 38 | font-size: var(--header5); 39 | } 40 | 41 | h6 { 42 | font-size: var(--header6); 43 | } 44 | 45 | /* Video Section */ 46 | .video-section { 47 | min-height: 100vh; 48 | background: #e6f1f7; 49 | position: relative; 50 | } 51 | 52 | .video-section h2 { 53 | font-family: "Cookie", cursive; 54 | text-align: center; 55 | padding: 2rem; 56 | } 57 | 58 | .video-section h5 { 59 | font-family: "Montserrat", sans-serif; 60 | margin-left: 3rem; 61 | } 62 | 63 | #video-grid { 64 | display: grid; 65 | grid-template-columns: repeat(auto-fill, 300px); 66 | grid-auto-rows: 300px; 67 | grid-gap: 2rem; 68 | margin: 1rem 2rem 1rem 2rem; 69 | } 70 | 71 | video { 72 | width: 100%; 73 | height: 100%; 74 | object-fit: cover; 75 | margin: 0 1rem 0 1rem; 76 | } 77 | 78 | #block-display { 79 | display: none; 80 | } 81 | 82 | .button-area { 83 | display: flex; 84 | align-items: center; 85 | justify-content: flex-end; 86 | min-height: 10vh; 87 | position: fixed; 88 | bottom: 0; 89 | width: 100%; 90 | } 91 | 92 | #end-button { 93 | font-family: "Montserrat", sans-serif; 94 | font-size: var(--header7); 95 | margin-right: 1rem; 96 | padding: 1rem 2rem; 97 | background: #f44336; 98 | border: none; 99 | color: white; 100 | cursor: pointer; 101 | transition: background 0.3s ease-in-out; 102 | } 103 | 104 | #invite-button { 105 | font-family: "Montserrat", sans-serif; 106 | font-size: var(--header7); 107 | margin-right: 1rem; 108 | padding: 1rem 2rem; 109 | background: #0072b1; 110 | border: none; 111 | color: white; 112 | cursor: pointer; 113 | transition: background 0.3s ease-in-out; 114 | } 115 | 116 | #invite-button:hover { 117 | background: #2f94ca; 118 | } 119 | 120 | /* Media Query */ 121 | @media screen and (max-width: 1024px) { 122 | /* Video Section */ 123 | .video-section h2 { 124 | font-size: var(--header1); 125 | } 126 | 127 | .video-section h5 { 128 | font-size: var(--header3); 129 | } 130 | 131 | #video-grid { 132 | grid-template-columns: repeat(auto-fill, 250px); 133 | grid-auto-rows: 250px; 134 | justify-content: center; 135 | } 136 | 137 | video { 138 | margin: 0; 139 | } 140 | 141 | #end-button { 142 | font-size: var(--header4); 143 | } 144 | 145 | #invite-button { 146 | font-size: var(--header4); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | const socket = io("/"); 2 | const videoGrid = document.getElementById("video-grid"); 3 | // const myPeer = new Peer(undefined, { 4 | // host: "/", 5 | // port: "3001" 6 | // }); 7 | console.log("Connecting to PeerJS..."); 8 | const myPeer = new Peer(); 9 | const myVideo = document.createElement("video"); 10 | myVideo.muted = true; 11 | myVideo.setAttribute("playsinline", true); // Added for iOS 12 | const peers = {}; 13 | 14 | console.log("Requesting camera and microphone access..."); 15 | navigator.mediaDevices 16 | .getUserMedia({ 17 | video: true, 18 | audio: true, 19 | }) 20 | .then((stream) => { 21 | console.log("Camera and mic access granted."); 22 | addVideoStream(myVideo, stream); 23 | 24 | myPeer.on("call", (call) => { 25 | console.log("Incoming call from peer:", call.peer); 26 | call.answer(stream); 27 | const video = document.createElement("video"); 28 | video.setAttribute("playsinline", true); // Added for iOS 29 | call.on("stream", (userVideoStream) => { 30 | console.log("Received remote stream from:", call.peer); 31 | addVideoStream(video, userVideoStream); 32 | }); 33 | 34 | call.on("error", (err) => { 35 | console.error("Call error:", err); 36 | }); 37 | }); 38 | 39 | socket.on("user-connected", (userId) => { 40 | console.log("🟢 New user connected:", userId); 41 | connectToNewUser(userId, stream); 42 | }); 43 | }) 44 | .catch((err) => { 45 | console.error("Failed to get media stream:", err); 46 | alert("Error accessing camera/mic: " + err.message); 47 | }); 48 | 49 | socket.on("user-disconnected", (userId) => { 50 | console.log("🔴 User disconnected:", userId); 51 | if (peers[userId]) peers[userId].close(); 52 | }); 53 | 54 | myPeer.on("open", (id) => { 55 | console.log("My peer ID is:", id); 56 | socket.emit("join-room", ROOM_ID, id); 57 | }); 58 | 59 | function connectToNewUser(userId, stream) { 60 | console.log("Calling new user:", userId); 61 | const call = myPeer.call(userId, stream); 62 | const video = document.createElement("video"); 63 | video.setAttribute("playsinline", true); // Added for iOS 64 | call.on("stream", (userVideoStream) => { 65 | console.log("Stream received from new user:", userId); 66 | addVideoStream(video, userVideoStream); 67 | }); 68 | call.on("close", () => { 69 | console.log("Call closed with user:", userId); 70 | video.remove(); 71 | }); 72 | 73 | call.on("error", (err) => { 74 | console.error("Call error with user:", userId, err); 75 | }); 76 | 77 | peers[userId] = call; 78 | } 79 | 80 | // function addVideoStream(video, stream) { 81 | // video.srcObject = stream; 82 | // video.addEventListener("loadedmetadata", () => { 83 | // video.play(); 84 | // }); 85 | // videoGrid.append(video); 86 | // } 87 | 88 | function addVideoStream(video, stream) { 89 | console.log("Adding video stream to DOM"); 90 | video.srcObject = stream; 91 | 92 | // Essential for iOS Safari/Chrome 93 | video.setAttribute("playsinline", true); 94 | video.muted = true; // Only needed for local stream (safe to set always) 95 | 96 | video.addEventListener("loadedmetadata", () => { 97 | video.play().catch(e => console.error("video.play() failed:", e)); 98 | }); 99 | 100 | videoGrid.append(video); 101 | } 102 | 103 | 104 | // URL Copy To Clipboard 105 | document.getElementById("invite-button").addEventListener("click", getURL); 106 | 107 | function getURL() { 108 | const c_url = window.location.href; 109 | copyToClipboard(c_url); 110 | alert("Url Copied to Clipboard,\nShare it with your Friends!\nUrl: " + c_url); 111 | } 112 | 113 | function copyToClipboard(text) { 114 | var dummy = document.createElement("textarea"); 115 | document.body.appendChild(dummy); 116 | dummy.value = text; 117 | dummy.select(); 118 | document.execCommand("copy"); 119 | document.body.removeChild(dummy); 120 | } 121 | 122 | // End Call 123 | document.getElementById("end-button").addEventListener("click", endCall); 124 | 125 | function endCall() { 126 | window.location.href = "/"; 127 | } 128 | -------------------------------------------------------------------------------- /public/img/video-chat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/css/index-style.css: -------------------------------------------------------------------------------- 1 | /* Global Styles */ 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | font-size: 62.5%; 11 | --header1: calc(2.5rem + 1.3vw); 12 | --header2: calc(2rem + 1.3vw); 13 | --header3: calc(1.5rem + 1.3vw); 14 | --header4: calc(1rem + 1.3vw); 15 | --header5: calc(0.8rem + 1.3vw); 16 | --header6: calc(0.5rem + 1.3vw); 17 | --header7: calc(0.2rem + 1.3vw); 18 | scroll-behavior: smooth; 19 | } 20 | 21 | h1 { 22 | font-size: var(--header1); 23 | } 24 | 25 | h2 { 26 | font-size: var(--header2); 27 | } 28 | 29 | h3 { 30 | font-size: var(--header3); 31 | } 32 | 33 | h4 { 34 | font-size: var(--header4); 35 | } 36 | 37 | h5 { 38 | font-size: var(--header5); 39 | } 40 | 41 | h6 { 42 | font-size: var(--header6); 43 | } 44 | 45 | /* Nav Bar */ 46 | .main-head { 47 | min-height: 10vh; 48 | } 49 | 50 | .main-head nav { 51 | display: flex; 52 | align-items: center; 53 | padding-top: 0.6%; 54 | } 55 | 56 | .logo { 57 | display: flex; 58 | flex: 1 1 40rem; 59 | align-items: center; 60 | font-family: "Pattaya", sans-serif; 61 | } 62 | 63 | .logo img { 64 | width: 4.5rem; 65 | align-items: center; 66 | margin: 2rem 1rem 1rem 3rem; 67 | } 68 | 69 | .logo h1 { 70 | font-size: var(--header3); 71 | } 72 | 73 | .nav-links { 74 | flex: 1 1 40rem; 75 | text-align: end; 76 | margin-right: 3rem; 77 | } 78 | 79 | .nav-links h2 { 80 | font-family: "Montserrat", sans-serif; 81 | font-size: var(--header7); 82 | } 83 | 84 | .nav-links h2:hover { 85 | color: #0072b1; 86 | } 87 | 88 | #share-button { 89 | background: transparent; 90 | border: none; 91 | } 92 | 93 | /* Hero Section */ 94 | .hero { 95 | display: flex; 96 | min-height: 90vh; 97 | flex-wrap: wrap; 98 | } 99 | 100 | .hero-left { 101 | flex: 1 1 40rem; 102 | } 103 | 104 | .hero-left-svg { 105 | text-align: center; 106 | margin-top: 6%; 107 | } 108 | 109 | .hero-left-svg img { 110 | height: 22vw; 111 | } 112 | 113 | .hero-left-text-area { 114 | display: flex; 115 | flex-direction: column; 116 | align-items: center; 117 | justify-content: center; 118 | margin: 6%; 119 | } 120 | 121 | .hero-left-button-area { 122 | width: 100%; 123 | display: flex; 124 | margin: 3rem; 125 | align-items: center; 126 | justify-content: space-evenly; 127 | flex-wrap: wrap; 128 | } 129 | 130 | .text-one { 131 | font-size: var(--header3); 132 | font-family: "Montserrat", sans-serif; 133 | } 134 | 135 | .button-one button { 136 | font-family: "Montserrat", sans-serif; 137 | display: flex; 138 | background: #0072b1; 139 | padding: 0.8rem 2rem; 140 | align-items: center; 141 | justify-content: center; 142 | border: none; 143 | border-radius: 0.5rem; 144 | cursor: pointer; 145 | } 146 | 147 | .button-image { 148 | padding-right: 1rem; 149 | } 150 | 151 | .button-one a { 152 | text-decoration: none; 153 | color: #ffffff; 154 | font-size: var(--header7); 155 | } 156 | 157 | .button-one button:hover { 158 | background: #0a7fbe; 159 | } 160 | 161 | .input-one { 162 | position: relative; 163 | display: flex; 164 | align-items: center; 165 | justify-content: center; 166 | } 167 | 168 | .input-one input { 169 | font-family: "Montserrat", sans-serif; 170 | padding: 1.2rem 1.2rem; 171 | border-color: #0072b1; 172 | border-width: 0.2rem; 173 | border-radius: 0.5rem; 174 | font-size: 1.5rem; 175 | } 176 | 177 | #join-button { 178 | font-size: 2rem; 179 | font-weight: 600; 180 | position: absolute; 181 | right: 0; 182 | margin-right: 1rem; 183 | background: #ffffff; 184 | color: #0072b1; 185 | border: none; 186 | cursor: pointer; 187 | } 188 | 189 | .text-two { 190 | font-size: var(--header4); 191 | font-family: "Montserrat", sans-serif; 192 | } 193 | 194 | .text-two span { 195 | color: #0072b1; 196 | } 197 | 198 | .hero-right { 199 | flex: 1 1 40rem; 200 | } 201 | 202 | .hero-right img { 203 | height: 100%; 204 | width: 100%; 205 | object-fit: cover; 206 | } 207 | 208 | /* Media Query for 1920 x 1080 */ 209 | @media screen and (min-width: 1919px) { 210 | html { 211 | font-size: 90%; 212 | } 213 | 214 | .button-one button { 215 | padding: 1rem 2rem; 216 | } 217 | } 218 | 219 | @media screen and (max-width: 955px) { 220 | .button-one button { 221 | margin: 2rem; 222 | } 223 | 224 | .input-one input { 225 | margin-left: 0rem; 226 | } 227 | } 228 | 229 | /* Media Query for mobile */ 230 | @media screen and (max-width: 799px) { 231 | /* Nav Bar */ 232 | .main-head nav { 233 | padding-top: 0.4%; 234 | } 235 | 236 | .logo img { 237 | width: 3.5rem; 238 | margin: 2rem 1rem 1rem 2rem; 239 | } 240 | 241 | .logo h1 { 242 | font-size: var(--header2); 243 | } 244 | 245 | .nav-links { 246 | margin-right: 2rem; 247 | } 248 | 249 | .nav-links h2 { 250 | font-size: var(--header3); 251 | } 252 | 253 | /* Hero Section */ 254 | .hero-left-svg { 255 | margin-top: 2%; 256 | } 257 | 258 | .hero-left-svg img { 259 | height: 38vw; 260 | } 261 | 262 | .hero-left-text-area { 263 | margin: 0%; 264 | } 265 | 266 | .hero-left-button-area { 267 | margin: 1rem; 268 | } 269 | 270 | .button-one button { 271 | padding: 0.5rem 2rem; 272 | } 273 | 274 | .button-one a { 275 | font-size: var(--header4); 276 | } 277 | 278 | .input-one input { 279 | margin-left: 0rem; 280 | font-size: var(--header4); 281 | padding: 0.8rem 1.5rem; 282 | font-size: var(--header4); 283 | } 284 | 285 | .text-one { 286 | margin-top: 2rem; 287 | } 288 | 289 | .text-one h3 { 290 | font-size: var(--header2); 291 | } 292 | 293 | .text-two { 294 | position: absolute; 295 | right: 2rem; 296 | bottom: 2rem; 297 | background: #ffffffa9; 298 | padding: 0.8rem 1rem; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /public/img/undraw_video_call_kxyp.svg: -------------------------------------------------------------------------------- 1 | video call --------------------------------------------------------------------------------