├── .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 |
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 |
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 |
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 |

44 |
45 |
46 |
47 |
Enjoy Free Video Chat.
48 |
49 |
69 |
70 |
It's Free, Try Now!
71 |
72 |
73 |
74 |
75 |

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 |
--------------------------------------------------------------------------------