├── .env.example
├── client
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── screens
│ │ ├── RoomScreen
│ │ │ ├── .fuse_hidden00011a4d00000002
│ │ │ ├── RoomScreen.css
│ │ │ ├── RoomService.js
│ │ │ ├── RoomUtils.js
│ │ │ └── RoomScreen.js
│ │ ├── Warning.js
│ │ ├── DashboardScreen.js
│ │ ├── LoginScreen.js
│ │ └── RegisterScreen.js
│ ├── index.js
│ ├── components
│ │ ├── Video.js
│ │ └── Nav.js
│ ├── hooks
│ │ └── useAuthentication.js
│ ├── App.js
│ ├── App.css
│ ├── routing
│ │ ├── PCRoute.js
│ │ └── Routes.js
│ └── index.css
├── .env.example
└── package.json
├── screenshots
├── Meeting1.png
└── Meeting2.png
├── .gitignore
├── package.json
├── readme.md
├── models
└── UserModal.js
├── routes
└── user.js
└── server.js
/.env.example:
--------------------------------------------------------------------------------
1 | SECRET=
2 | DATABASE=
3 | CORS_ORIGIN=http://localhost:8000
4 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/screenshots/Meeting1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdjahidhasan009/Meeting/HEAD/screenshots/Meeting1.png
--------------------------------------------------------------------------------
/screenshots/Meeting2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdjahidhasan009/Meeting/HEAD/screenshots/Meeting2.png
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdjahidhasan009/Meeting/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdjahidhasan009/Meeting/HEAD/client/public/logo192.png
--------------------------------------------------------------------------------
/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdjahidhasan009/Meeting/HEAD/client/public/logo512.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /client/.env
2 | /client/build
3 | /client/node_modules/
4 |
5 | public
6 | node_modules/
7 | .env
8 | .idea
9 |
--------------------------------------------------------------------------------
/client/src/screens/RoomScreen/.fuse_hidden00011a4d00000002:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mdjahidhasan009/Meeting/HEAD/client/src/screens/RoomScreen/.fuse_hidden00011a4d00000002
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/client/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL=http://localhost:8000
2 | REACT_APP_GOOGLE_STUN_SERVER=stun:stun.l.google.com:19302
3 |
4 | REACT_APP_TURN_SERVER1_NAME=
5 | REACT_APP_TURN_SERVER1_USERNAME=
6 | REACT_APP_TURN_SERVER1_PASSWORD=
7 |
8 | REACT_APP_TURN_SERVER2_NAME=
9 | REACT_APP_TURN_SERVER2_USERNAME=
10 | REACT_APP_TURN_SERVER2_PASSWORD=
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Meeting",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "bcryptjs": "^2.4.3",
8 | "cors": "^2.8.5",
9 | "dotenv": "^8.2.0",
10 | "express": "^4.17.1",
11 | "jsonwebtoken": "^8.5.1",
12 | "mongoose": "^5.10.11",
13 | "path": "^0.12.7",
14 | "simple-peer": "^9.11.1",
15 | "socket.io": "^2.3.0"
16 | },
17 | "scripts": {
18 | "start": "PROD=true node server.js"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/components/Video.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 |
3 | const Video = (props) => {
4 | const ref = useRef();
5 |
6 | useEffect(() => {
7 | // if (props.peer && props.peer.peer) {
8 | props.peer.peer.on("stream", stream => {
9 | ref.current.srcObject = stream;
10 | });
11 | // }
12 | //eslint-disable-next-line
13 | }, []);
14 |
15 | return (
16 |
17 | );
18 | }
19 |
20 | export default Video;
21 |
--------------------------------------------------------------------------------
/client/src/hooks/useAuthentication.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 |
4 | const useAuthenticated = () => {
5 | const [ isAuthenticated, setIsAuthenticated ] = useState(true);
6 | const location = useLocation();
7 |
8 | useEffect(() => {
9 | const token = localStorage.getItem('Token');
10 | if(token) setIsAuthenticated(true);
11 | else setIsAuthenticated(false);
12 | //eslint-disable-next-line
13 | }, [ location.pathname ]);
14 |
15 | return isAuthenticated ;
16 | }
17 |
18 | export default useAuthenticated;
19 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {BrowserRouter, Route, Switch} from "react-router-dom";
3 |
4 | import Warning from "./screens/Warning";
5 |
6 | import Navbar from './components/Nav';
7 | import Routes from "./routing/Routes";
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [Video Preview at youtube](https://www.youtube.com/watch?v=Kp6jooQFkXg)
2 |
3 | # Features
4 | - Users can create a room and join and do video call with each
5 | other. Can mute, unmute audio also hide or show their video.
6 | - User can share their own screen.
7 | - Users can chat along with video call, user authentication.
8 |
9 | ## Screenshorts
10 |
11 |
13 |
14 |
15 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/src/screens/Warning.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import axios from "axios";
3 | import M from 'materialize-css';
4 | import { useHistory } from 'react-router-dom';
5 |
6 | const Warning = () => {
7 |
8 | return (
9 |
10 |
11 |
12 |
Warning
13 |
14 |
This site only for PC device!
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default Warning;
23 |
--------------------------------------------------------------------------------
/models/UserModal.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | require: 'Name is required!'
8 | },
9 | username: {
10 | type: String,
11 | require: 'Username is required!',
12 | unique: true
13 | },
14 | email: {
15 | type: String,
16 | require: 'Email is required!'
17 | },
18 | password: {
19 | type: String,
20 | require: 'Password is required!'
21 | }
22 | }
23 | );
24 |
25 | module.exports = mongoose.model('User', userSchema);
26 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/routing/PCRoute.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import {Redirect, Route} from "react-router-dom";
3 |
4 | const PCRoute = ({ component: Component, ...rest }) => {
5 | const [ isVisitingFromPC, setIsVisitingFromPC ] = useState(true);
6 | useEffect(() => {
7 | if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
8 | setIsVisitingFromPC(false);
9 | }
10 | }, []);
11 | return(
12 | !isVisitingFromPC ?
15 | (
16 |
17 | ) : (
18 |
19 | )
20 |
21 | }
22 | />
23 | )
24 | }
25 |
26 | export default PCRoute;
27 |
--------------------------------------------------------------------------------
/client/src/routing/Routes.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Switch } from "react-router-dom";
3 |
4 | import PCRoute from "./PCRoute";
5 | import DashboardScreen from "../screens/DashboardScreen";
6 | import LoginScreen from "../screens/LoginScreen";
7 | import RegisterScreen from "../screens/RegisterScreen";
8 | import RoomScreen from "../screens/RoomScreen/RoomScreen";
9 |
10 | const Routes = () => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default Routes;
24 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "axios": "^0.21.0",
10 | "materialize-css": "^1.0.0-rc.2",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-router-dom": "^5.1.2",
14 | "react-scripts": "3.4.1",
15 | "simple-peer": "9.11.1",
16 | "socket.io-client": "^2.3.0",
17 | "styled-components": "^5.1.0",
18 | "uuid": "^7.0.3"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | html, body {
16 | height: 100%;
17 | margin: 0;
18 | }
19 |
20 | #root {
21 | min-height: 500px;
22 | height: 100%;
23 | }
24 |
25 | main {
26 | height: calc(100% - 64px); /*Navbar height is 64px*/
27 | }
28 |
29 | .p0 {
30 | padding: 0 !important;
31 | }
32 |
33 | nav {
34 | background-color: #0087ffe6;
35 | }
36 |
37 | .container {
38 | min-height: 100%;
39 | display: flex;
40 | justify-content: center;
41 | align-items: center;
42 | }
43 |
44 | .card-container {
45 | display: flex;
46 | flex-direction: column;
47 | justify-content: center;
48 | align-items: center;
49 | }
50 |
51 | .card {
52 | display: inline-block;
53 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
54 | border-radius: 4px;
55 | padding: 1rem;
56 | }
57 |
58 | .card__header {
59 | background: #0087ffe6;
60 | padding: 0.75rem 1.5rem;
61 | border-radius: 2px;
62 | text-transform: uppercase;
63 | font-size: 1.1rem;
64 | font-weight: bold;
65 | display: inline-block;
66 | position: absolute;
67 | top: -1rem;
68 | left: -0.75rem;
69 | }
70 |
71 | .card__body {
72 | margin-top: 2rem;
73 | }
74 |
75 | button {
76 | background: #0acd1d;
77 | border: none;
78 | padding: 10px 0;
79 | margin-top: 10px;
80 | border-radius: 2px;
81 | width: 100%;
82 | text-transform: uppercase;
83 | }
84 |
--------------------------------------------------------------------------------
/client/src/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import M from "materialize-css";
4 |
5 | import useAuthenticated from "../hooks/useAuthentication";
6 |
7 | const Navbar = () => {
8 | let isAuthenticated = useAuthenticated();
9 |
10 | useEffect(() => {
11 | document.addEventListener('DOMContentLoaded', function() {
12 | let elems = document.querySelectorAll('.sidenav');
13 | M.Sidenav.init(elems);
14 | });
15 | }, []);
16 |
17 | return (
18 | <>
19 |
20 |
21 |
Meeting
22 |
23 |
24 |
25 |
26 | {isAuthenticated ? (
27 | Dashboard
28 | ) : (
29 | <>
30 | Login
31 | Register
32 | >
33 | )}
34 |
35 |
36 |
37 |
38 |
39 | {isAuthenticated ? (
40 | Dashboard
41 | ) : (
42 | <>
43 | Login
44 | Register
45 | >
46 | )}
47 |
48 | >
49 | )
50 | }
51 |
52 | export default Navbar;
53 |
--------------------------------------------------------------------------------
/client/src/screens/DashboardScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { v1 as uuid } from "uuid";
3 | import useAuthentication from "../hooks/useAuthentication";
4 |
5 | import M from 'materialize-css';
6 |
7 | const DashboardScreen = (props) => {
8 | let isAuthenticated = useAuthentication();
9 | const createRoom = () => {
10 | const id = uuid();
11 | props.history.push(`/room/${id}`);
12 | };
13 |
14 | useEffect(() => {
15 | if(!isAuthenticated) {
16 | M.toast({ html: 'Login first', classes:'red' });
17 | props.history.push('/login');
18 | }
19 | //eslint-disable-next-line
20 | }, [isAuthenticated]);
21 |
22 | const logout = () => {
23 | localStorage.removeItem("Token");
24 | props.history.push('/login');
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
Note
32 |
33 |
In this project I am using free stun and turn servers. As turn server transfer media(video) it is expensive that's why free turn server sometime can not transfer media with two client in different network. But it works(video transmission) in same network(router) . And chat message works always without any issue.
34 |
35 |
36 |
37 | Create room
38 | Logout
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default DashboardScreen;
46 |
--------------------------------------------------------------------------------
/routes/user.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 | const bcrypt = require('bcryptjs');
3 | const jwt = require('jsonwebtoken');
4 | const User = require('../models/UserModal');
5 |
6 | router.post('/register', async (req, res) => {
7 | try {
8 | const { name, username, email, password } = req.body;
9 | if(password.length < 6) return res.status(400).json({ message: 'Password must be at least 6 character long' });;
10 |
11 | let userExists = await User.findOne({ email });
12 | if(userExists) return res.status(400).json({ message: "User already exits with this email!" });
13 | userExists = await User.findOne({ username });
14 | if(userExists) return res.status(400).json({ message: 'User already exits with this username!' });
15 |
16 | const user = new User({
17 | name, username, email, password
18 | });
19 | const salt = await bcrypt.genSalt(10);
20 | user.password = await bcrypt.hash(password, salt);
21 | await user.save();
22 | res.json({
23 | message: 'User created!'
24 | })
25 | } catch (error) {
26 | console.error(error);
27 | }
28 | });
29 |
30 | router.post('/login', async (req, res) => {
31 | try {
32 | const { email, password } = req.body;
33 |
34 | const user = await User.findOne({ email });
35 | if(!user) return res.status(400).json({ message: 'Email or password does not match' });
36 |
37 | const isMatch = await bcrypt.compare(password, user.password);
38 | if(!isMatch) return res.status(400).json({ message: 'Email or password does not match' });
39 |
40 | const token = await jwt.sign(user.id, process.env.SECRET);
41 | res.json(token);
42 | } catch (error) {
43 | console.error(error);
44 | }
45 | });
46 |
47 | module.exports = router;
48 |
--------------------------------------------------------------------------------
/client/src/screens/RoomScreen/RoomScreen.css:
--------------------------------------------------------------------------------
1 | .fa-paper-plane {
2 | color: white;
3 | margin-top: 25px;
4 | }
5 |
6 | #video-grid {
7 | display: grid;
8 | grid-template-columns: repeat(3, 1fr);
9 | }
10 |
11 | video {
12 | object-fit: fill;
13 | width: 300px;
14 | height: 220px;
15 | padding: 2px;
16 | }
17 |
18 | .videos__users-video {
19 | height: calc(100% - 67px);
20 | flex-grow: 1;
21 | background-color: black;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | }
26 |
27 | .videos__controls {
28 | height: 67px;
29 | display: flex;
30 | background-color: #1C1E20;
31 | color: #D2D2D2;
32 | padding: 5px;
33 | justify-content: space-between;
34 | }
35 |
36 | .control {
37 | display: flex;
38 | }
39 |
40 | .control__btn-container {
41 | display: flex;
42 | flex-direction: column;
43 | align-items: center;
44 | padding: 8px 10px;
45 | min-width: 80px;
46 | cursor: pointer;
47 | }
48 |
49 | .control__btn-container i {
50 | font-size: 24px;
51 | }
52 |
53 | .control__btn-container:hover {
54 | background-color: #343434;
55 | border-radius: 5px;
56 | }
57 |
58 | .leave_meeting {
59 | margin-top: 12px;
60 | color: red;
61 | }
62 |
63 | .chat {
64 | position: relative;
65 | background-color: #242324;
66 | border-left: 1px solid #3D3D42;
67 | }
68 |
69 | .chat__header {
70 | color: #F5F5F5;
71 | text-align: center;
72 | }
73 |
74 | .chat__msg-container {
75 | flex-grow: 1;
76 | overflow-y: scroll;
77 | height: calc(100% - 110px);
78 | }
79 |
80 | .chat__msg-send-container {
81 | position: absolute;
82 | bottom: 0;
83 | padding: 22px 12px;
84 | display: flex;
85 | }
86 |
87 | .chat__msg-send-container input {
88 | flex-grow: 1;
89 | background-color: transparent;
90 | border: none;
91 | color: #F5F5F5;
92 | }
93 |
94 | .messages {
95 | color: white;
96 | list-style: none;
97 | margin: 0;
98 | }
99 |
100 | .messages p {
101 | overflow-wrap: break-word;
102 | }
103 |
104 | .unmute, .stop {
105 | color: #CC3B33;
106 | }
107 |
108 | .room,
109 | .room .col {
110 | height: 100%;
111 | }
112 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
32 | React App
33 |
34 |
35 | You need to enable JavaScript to run this app.
36 |
37 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/client/src/screens/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import axios from "axios";
3 | import M from 'materialize-css';
4 | import { useHistory } from 'react-router-dom';
5 |
6 | const LoginScreen = (props) => {
7 | const emailRef = React.createRef();
8 | const passwordRef = React.createRef();
9 | const history = useHistory();
10 |
11 | const loginUser = (e) => {
12 | e.preventDefault();
13 | const email = emailRef.current.value;
14 | const password = passwordRef.current.value;
15 |
16 | axios
17 | .post(process.env.REACT_APP_BASE_URL + "/user/login", {
18 | email,
19 | password,
20 | })
21 | // }, {withCredentials: true})
22 | .then((response) => {
23 | localStorage.setItem("Token", response.data);
24 | M.toast({html: 'Login Successful', classes: 'green'});
25 | props.history.push("/");
26 | })
27 | .catch((err) => {
28 | if(err.response && err?.response?.data?.message)
29 | M.toast({html: err?.response?.data?.message, classes: 'red'});
30 | else
31 | M.toast({ html: 'Error:' + "Internal Server Error", classes:'red'});
32 | });
33 | };
34 |
35 | return (
36 |
69 | );
70 | };
71 |
72 | export default LoginScreen;
73 |
--------------------------------------------------------------------------------
/client/src/screens/RegisterScreen.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import axios from "axios";
3 | import { useHistory } from "react-router-dom";
4 |
5 | import M from "materialize-css";
6 |
7 | const RegisterScreen = (props) => {
8 | const nameRef = React.createRef();
9 | const usernameRef = React.createRef();
10 | const emailRef = React.createRef();
11 | const passwordRef = React.createRef();
12 | const history = useHistory();
13 |
14 | const registerUser = (props) => {
15 | const name = nameRef.current.value;
16 | const username = usernameRef.current.value;
17 | const email = emailRef.current.value;
18 | const password = passwordRef.current.value;
19 |
20 | axios
21 | .post(process.env.REACT_APP_BASE_URL + "/user/register", {
22 | name,
23 | username,
24 | email,
25 | password,
26 | })
27 | .then((response) => {
28 | M.toast({html: 'User created, Login first!', classes: 'green'});
29 | history.push("/login");
30 | })
31 | .catch((err) => {
32 | console.log(err)
33 | if(err.response && err.response.data) console.log(err.response.data.message);
34 | M.toast({html: err.response.data.message, classes: 'red'});
35 | });
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
Register
43 |
75 |
76 | Password
77 |
84 |
85 |
Register
86 |
history.push('/login')}>Login
87 |
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default RegisterScreen;
95 |
--------------------------------------------------------------------------------
/client/src/screens/RoomScreen/RoomService.js:
--------------------------------------------------------------------------------
1 | import io from "socket.io-client";
2 | import Peer from "simple-peer";
3 | import {addPeer, createPeer} from "./RoomUtils";
4 |
5 | const BASE_URL = process.env.REACT_APP_BASE_URL;
6 |
7 | const RoomService = {
8 | connectToSocketAndWebcamStream: async (token) => {
9 | const socket = io.connect(BASE_URL, {
10 | query: {
11 | token: token
12 | }
13 | });
14 |
15 | const webcamStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
16 |
17 | return { socket, webcamStream };
18 | },
19 |
20 |
21 | setupSocketListeners: (socket, webcamStream, setPeers, screenCaptureStream, currentPeers, setMessages, roomId) => {
22 | socket.emit("joinRoom", roomId); //sending to the server that a user joined to a room
23 |
24 | //server send array of socket id's of other user of same room so that new user can connect with other user via
25 | //simple-peer for video transmission and message will be served using socket io
26 | socket.on("usersInRoom", users => { //triggered in server and here receiving it
27 | const tempPeers = [];
28 | console.log('usersInRoom', users, webcamStream);
29 | users.forEach(otherUserSocketId => {
30 | //creating connection between two user via simple-peer for video
31 | const peer = createPeer(otherUserSocketId, socket.id, webcamStream, socket);
32 | currentPeers.push({
33 | peerId: otherUserSocketId,
34 | peer
35 | });
36 | tempPeers.push({
37 | peerId: otherUserSocketId,
38 | peer
39 | });
40 | })
41 | setPeers(tempPeers);
42 | })
43 |
44 | //a new user joined at same room send signal,callerId(simple-peer) and stream to server and server give it to
45 | //us to create peer between two peer and connect
46 | socket.on("userJoined", payload => {
47 | let peer;
48 | if(screenCaptureStream) peer = addPeer(payload.signal, payload.callerId, screenCaptureStream, socket);
49 | else peer = addPeer(payload.signal, payload.callerId, webcamStream, socket);
50 | currentPeers.push({
51 | peerId: payload.callerId,
52 | peer
53 | });
54 | const peerObj = {
55 | peer,
56 | peerId: payload.callerId,
57 | };
58 |
59 | setPeers(users => [...users, peerObj]);
60 | });
61 |
62 | //receiving signal of other peer who is trying to connect and adding its signal at peersRef
63 | socket.on("takingReturnedSignal", payload => {
64 | const item = currentPeers.find(p => p.peerId === payload.id);
65 | item.peer.signal(payload.signal);
66 | });
67 |
68 | //receiving message of an user and adding this at message state
69 | socket.on('receiveMessage', payload => {
70 | setMessages(messages => [...messages, payload]);
71 | });
72 |
73 | //user left and server send its peerId to disconnect from that peer
74 | socket.on('userLeft', id => {
75 | const peerObj = currentPeers.find(p => p.peerId === id);
76 | if(peerObj?.peer) peerObj.peer.destroy(); //cancel connection with disconnected peer
77 | const peers = currentPeers.filter(p => p.peerId !== id);
78 | currentPeers = peers;
79 | setPeers(peers);
80 | });
81 |
82 | socket.on('disconnect', (payload) => {
83 | //destroying previous stream(webcam stream)
84 | const previousWebcamStream = webcamStream;
85 | const previousWebcamStreamTracks = previousWebcamStream.getTracks();
86 | previousWebcamStreamTracks.forEach(track => {
87 | track.stop();
88 | });
89 |
90 | //destroying previous stream(screen capture stream)
91 | const previousScreenCaptureStream = screenCaptureStream;
92 | if(previousScreenCaptureStream) {
93 | const previousScreenCaptureStreamTracks = previousScreenCaptureStream.getTracks();
94 | previousScreenCaptureStreamTracks.forEach(track => {
95 | track.stop();
96 | });
97 | }
98 | });
99 | },
100 | };
101 |
102 | export default RoomService;
103 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const express = require("express");
3 | const http = require("http");
4 | const app = express();
5 | const server = http.createServer(app);
6 | const mongoose = require('mongoose');
7 | const jwt = require('jsonwebtoken');
8 | const cors = require('cors');
9 | const path = require('path');
10 |
11 | const User = require('./models/UserModal');
12 |
13 | const PORT = 8000;
14 |
15 | app.use(express.json());
16 | app.use(express.urlencoded({ extended: true }));
17 | app.use(cors({
18 | origin: process.env.CORS_ORIGIN
19 | }));
20 |
21 | //For deploy only
22 | // app.use(express.static(path.join('public')));
23 |
24 | app.use('/user', require('./routes/user'));
25 |
26 | //Database connection
27 | mongoose.connect(process.env.DATABASE, {
28 | useUnifiedTopology: true,
29 | useNewUrlParser: true
30 | });
31 | mongoose.connection.on('error', error => {
32 | console.err('Mongoose Connection Error: ' + error.message);
33 | })
34 | mongoose.connection.once('open', ()=> {
35 | console.log('Mongodb connected!');
36 | })
37 |
38 | //for deploy only
39 |
40 | // app.use((req, res, next) => {
41 | // res.sendFile(path.resolve(__dirname, 'public', 'index.html'));
42 | // })
43 |
44 | server.listen(process.env.PORT || PORT, () => {
45 | console.log('server is running on port ' + PORT)
46 | });
47 |
48 |
49 | //Socket
50 | const socket = require("socket.io");
51 | const io = socket(server);
52 |
53 | const usersInRoom = {}; //all user(socket id) connected to a chatroom
54 | const socketToRoom = {}; //roomId in which a socket id is connected
55 |
56 | //verifying token
57 | io.use(async (socket, next) => {
58 | try {
59 | const token = socket.handshake.query.token;
60 | const payload = await jwt.verify(token, process.env.SECRET);
61 | socket.userId = payload;
62 | const user = await User.findOne({ _id: socket.userId }).select('-password');
63 | socket.username = user.username;
64 | socket.name = user.name;
65 | next();
66 | } catch (error) {
67 | console.log(error);
68 | }
69 | });
70 |
71 | io.on('connection', socket => {
72 | console.log('Some one joined socketId: ' + socket.id);
73 | socket.on("joinRoom", roomId=> {
74 | // console.log('Joined roomId: ' + roomId + " socketId: " + socket.id + ' userId: ' + socket.userId);
75 | if (usersInRoom[roomId]) {
76 | usersInRoom[roomId].push(socket.id);
77 | } else {
78 | usersInRoom[roomId] = [socket.id];
79 | }
80 | socketToRoom[socket.id] = roomId;
81 | const usersInThisRoom = usersInRoom[roomId].filter(id => id !== socket.id);
82 | socket.join(roomId); //for message
83 | socket.emit("usersInRoom", usersInThisRoom); //sending all socket id already joined user in this room
84 | });
85 |
86 | //client send this signal to sever and sever will send to other user of peerId(callerId is peer id)
87 | socket.on("sendingSignal", payload => {
88 | console.log('console.log before sending userJoined', payload.callerId);
89 | io.to(payload.userIdToSendSignal).emit('userJoined', { signal: payload.signal, callerId: payload.callerId });
90 | });
91 |
92 | //client site receive signal of other peer and it sending its own signal for other member
93 | socket.on("returningSignal", payload => {
94 | io.to(payload.callerId).emit('takingReturnedSignal', { signal: payload.signal, id: socket.id });
95 | });
96 |
97 | //from client send message to send all other connected user of same room
98 | socket.on('sendMessage', payload => {
99 | //sending message to all other connected user at same room
100 | io.to(payload.roomId).emit('receiveMessage', { message: payload.message, name:socket.name, username: socket.username });
101 | });
102 |
103 | //someone left room
104 | socket.on('disconnect', () => {
105 | const roomId = socketToRoom[socket.id];
106 | let socketsIdConnectedToRoom = usersInRoom[roomId];
107 | if (socketsIdConnectedToRoom) {
108 | socketsIdConnectedToRoom = socketsIdConnectedToRoom.filter(id => id !== socket.id);
109 | usersInRoom[roomId] = socketsIdConnectedToRoom;
110 | }
111 | socket.leave(roomId); //for message group(socket)
112 | socket.broadcast.emit("userLeft", socket.id); //sending socket id to all other connected user of same room without its own
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/client/src/screens/RoomScreen/RoomUtils.js:
--------------------------------------------------------------------------------
1 | import Peer from "simple-peer";
2 |
3 | export const addPeer = (incomingSignal, callerId, stream, socket) => {
4 | const peer = new Peer({
5 | initiator: false,
6 | trickle: false,
7 | stream
8 | });
9 |
10 | //other peer give its signal in signal object and this peer returning its own signal
11 | peer.on("signal", signal => {
12 | socket.emit("returningSignal", { signal, callerId: callerId });
13 | });
14 | peer.signal(incomingSignal);
15 | return peer;
16 | };
17 |
18 | export const startWebCamVideo = async (peers, userVideo, webcamStream, screenCaptureStream) => {
19 | const newWebcamStream = await getWebcamStream(); //getting webcam video and audio
20 | const videoStreamTrack = newWebcamStream.getVideoTracks()[0]; //taking video track of stream
21 | const audioStreamTrack = newWebcamStream.getAudioTracks()[0]; //taking audio track of stream
22 | //replacing all video track of all peer connected to this peer
23 | peers.map(peer => {
24 | //replacing video track
25 | peer.peer.replaceTrack(
26 | peer.peer.streams[0].getVideoTracks()[0],
27 | videoStreamTrack,
28 | peer.peer.streams[0]
29 | );
30 | //replacing audio track
31 | peer.peer.replaceTrack(
32 | peer.peer.streams[0].getAudioTracks()[0],
33 | audioStreamTrack,
34 | peer.peer.streams[0]
35 | );
36 | });
37 | userVideo.srcObject = newWebcamStream;
38 | webcamStream = newWebcamStream;
39 | screenCaptureStream = null;
40 | };
41 |
42 | export const shareScreen = async (peers, screenCaptureStream, webcamStream, currentPeers, userVideo, setIsAudioMuted, setIsVideoMuted) => {
43 | //getting screen video
44 | screenCaptureStream.current = await navigator.mediaDevices.getDisplayMedia({ cursor: true });
45 | //taking video track of stream
46 | const screenCaptureVideoStreamTrack = screenCaptureStream.current.getVideoTracks()[0];
47 | debugger
48 | //replacing video track of each peer connected with getDisplayMedia video track and audio will remain as it is
49 | //as all browser does not return audio track with getDisplayMedia
50 | currentPeers.map(peer => (
51 | peer.peer.replaceTrack(
52 | peer.peer.streams[0].getVideoTracks()[0],
53 | screenCaptureVideoStreamTrack,
54 | peer.peer.streams[0]
55 | )
56 | ))
57 | //destroying previous stream video track
58 | const previousWebcamStream = userVideo.srcObject;
59 | const previousWebcamStreamTracks = previousWebcamStream.getTracks();
60 | previousWebcamStreamTracks.forEach(function(track) {
61 | if(track.kind === 'video') track.stop();
62 | });
63 | userVideo.srcObject = screenCaptureStream.current;
64 |
65 | //When user will stop share then own video(of webcam) will appears
66 | screenCaptureStream.current.getVideoTracks()[0].addEventListener('ended', () => {
67 | startWebCamVideo(peers, userVideo, webcamStream, screenCaptureStream);
68 | setIsAudioMuted(false);
69 | setIsVideoMuted(false);
70 | });
71 | }
72 |
73 | export const muteOrUnmuteAudio = (webcamStream, isAudioMuted, setIsAudioMuted) => {
74 | if(!webcamStream) return;
75 |
76 | if(!isAudioMuted) {
77 | webcamStream.getAudioTracks()[0].enabled = false;
78 | setIsAudioMuted(true);
79 | } else {
80 | webcamStream.getAudioTracks()[0].enabled = true;
81 | setIsAudioMuted(false);
82 | }
83 | };
84 |
85 | export const playOrStopVideo = (userVideo, isVideoMuted, setIsVideoMuted) => {
86 | if(!userVideo) return;
87 |
88 | if(!isVideoMuted) {
89 | userVideo.getVideoTracks()[0].enabled = false;
90 | setIsVideoMuted(true);
91 | } else {
92 | userVideo.getVideoTracks()[0].enabled = true;
93 | setIsVideoMuted(false);
94 | }
95 | };
96 |
97 | export const sendMessage = (e, socket, roomId, message) => {
98 | e.preventDefault();
99 | //sending message text with roomId to sever it will send message along other data to all connected user of current room
100 | if(socket) {
101 | socket.emit('sendMessage', {
102 | roomId,
103 | message: message.value
104 | })
105 | message.value = "";
106 | }
107 | }
108 |
109 | export const createPeer = (userIdToSendSignal, mySocketId, stream, socket) => {
110 | //if initiator is true then newly created peer will send a signal to other peer it those two peers accept signal
111 | // then connection will be established between those two peers
112 | //trickle for enable/disable trickle ICE candidates
113 | const peer = new Peer({
114 | initiator: true,
115 | trickle: false,
116 | config: {
117 | iceServers: [
118 | {
119 | urls: process.env.REACT_APP_GOOGLE_STUN_SERVER
120 | },
121 | {
122 | urls: process.env.REACT_APP_TURN_SERVER1_NAME,
123 | username: process.env.REACT_APP_TURN_SERVER1_USERNAME,
124 | credential: process.env.REACT_APP_TURN_SERVER1_PASSWORD
125 | },
126 | {
127 | urls: process.env.REACT_APP_TURN_SERVER2_NAME,
128 | username: process.env.REACT_APP_TURN_SERVER2_USERNAME,
129 | credential: process.env.REACT_APP_TURN_SERVER2_PASSWORD
130 | }
131 | ]
132 | },
133 | stream //My own stream of video and audio
134 | });
135 |
136 | //sending signal to second peer and if that receive than other(second) peer also will send an signal to this peer
137 | peer.on("signal", signal => {
138 | socket.emit("sendingSignal", { userIdToSendSignal: userIdToSendSignal, callerId: mySocketId, signal });
139 | })
140 | return peer;
141 | };
142 |
143 | export const getWebcamStream = async () => {
144 | return await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
145 | };
--------------------------------------------------------------------------------
/client/src/screens/RoomScreen/RoomScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState, createRef } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import M from "materialize-css";
4 |
5 | import Video from '../../components/Video';
6 | import useAuthenticated from "../../hooks/useAuthentication";
7 | import "./RoomScreen.css";
8 | import RoomService from "./RoomService";
9 | import {muteOrUnmuteAudio, playOrStopVideo, sendMessage, shareScreen, stopAllVideoAudioMedia} from "./RoomUtils";
10 |
11 | const RoomScreen = (props) => {
12 | const isAuthenticated = useAuthenticated();
13 | const [ peers, setPeers ] = useState([]); //state for rendering and also have stream of peers
14 | const socketRef = useRef(); //own socket
15 | const userVideoRef = useRef(); //for display own video
16 | const messageRef = createRef(); //message input
17 | const peersRef = useRef([]); //collection of peers who are currently connect to a room
18 | const screenCaptureStream = useRef(); //screen capture stream
19 | const roomId = props.match.params.roomId; //joined room id
20 | const [ isVideoMuted, setIsVideoMuted ] = useState(false);
21 | const [ isAudioMuted, setIsAudioMuted ] = useState(false);
22 | const [webcamStream, setWebCamStream ] = useState(null); //own webcam stream
23 | const [ messages, setMessages ] = useState([]); //all messages state after joining the room
24 | const history = useHistory();
25 | const currentPeers = useRef([]);
26 |
27 | useEffect(() => {
28 | if(!isAuthenticated) {
29 | M.toast({ html: 'Login first', classes:'red' });
30 | props.history.push('/login');
31 | }
32 | //eslint-disable-next-line
33 | }, [isAuthenticated]);
34 |
35 | useEffect(() => {
36 | RoomService.connectToSocketAndWebcamStream(localStorage.getItem('Token'))
37 | .then(({ socket, webcamStream }) => {
38 | socketRef.current = socket;
39 |
40 | setWebCamStream(webcamStream);
41 | userVideoRef.current.srcObject = webcamStream;
42 |
43 | // if(!webcamStream.getAudioTracks()[0].enabled) webcamStream.getAudioTracks()[0].enabled = true;
44 |
45 | // const audioTracks = webcamStream.getAudioTracks();
46 | // if (audioTracks.length > 0 && !audioTracks[0].enabled) {
47 | // audioTracks[0].enabled = true;
48 | // // Update the state to re-render the component
49 | // setWebCamStream(webcamStream);
50 | // }
51 | RoomService.setupSocketListeners(socketRef.current, webcamStream, setPeers, screenCaptureStream.current, peersRef.current, setMessages, roomId);
52 | });
53 |
54 | return async () => {
55 | socketRef.current.disconnect();
56 | await stopAllVideoAudioMedia();
57 | }
58 | //eslint-disable-next-line
59 | }, []);
60 |
61 | //Stopping webcam and screen media and audio also
62 | const stopAllVideoAudioMedia = async () => {
63 | debugger
64 | //destroying previous stream(screen capture stream)
65 | const previousScreenCaptureStream = screenCaptureStream.current;
66 | if(previousScreenCaptureStream) {
67 | const previousScreenCaptureStreamTracks = previousScreenCaptureStream.getTracks();
68 | previousScreenCaptureStreamTracks.forEach(track => {
69 | track.stop();
70 | });
71 | }
72 |
73 | //destroying previous stream(webcam stream)
74 | const previousWebcamStream = webcamStream;
75 | if(previousWebcamStream) {
76 | const previousWebcamStreamTracks = previousWebcamStream.getTracks();
77 | previousWebcamStreamTracks.forEach(track => {
78 | track.stop();
79 | });
80 | }
81 | }
82 |
83 |
84 | const handleOnClickAudioToggle = () => {
85 | muteOrUnmuteAudio(webcamStream, isAudioMuted, setIsAudioMuted);
86 | }
87 |
88 | const handlePlayOrStopVideo = () => {
89 | playOrStopVideo(webcamStream, isVideoMuted, setIsVideoMuted);
90 | }
91 |
92 | const handleShareScreen = async () => {
93 | await shareScreen(peers, screenCaptureStream, webcamStream, peers, userVideoRef.current, setIsAudioMuted, setIsVideoMuted);
94 | }
95 |
96 | const handleSendMessage = (e) => {
97 | sendMessage(e, socketRef.current, roomId, messageRef.current);
98 | }
99 |
100 | const leaveMeeting = () => {
101 | history.push('/');
102 | };
103 |
104 | return (
105 |
106 |
107 |
108 |
109 |
110 | {console.log('peers', peers)}
111 | {peers.map((peer) => (
112 |
113 | ))}
114 |
115 |
116 |
117 |
118 |
119 |
120 | {isAudioMuted
121 | ?
122 | :
123 | }
124 | {isAudioMuted
125 | ? Unmute
126 | : Mute
127 | }
128 |
129 |
130 | {isVideoMuted
131 | ?
132 | :
133 | }
134 | {isVideoMuted
135 | ? Play Video
136 | : Stop Video
137 | }
138 |
139 |
140 |
141 |
142 |
143 | Share Screen
144 |
145 |
146 |
147 |
148 | Leave Meeting
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
Chat
158 |
159 |
166 |
170 |
171 |
172 |
173 | );
174 | };
175 |
176 | export default RoomScreen;
177 |
--------------------------------------------------------------------------------