├── .gitignore
├── app
├── public
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── robots.txt
│ ├── manifest.json
│ └── index.html
├── src
│ ├── setupTests.js
│ ├── App.test.js
│ ├── App.js
│ ├── index.css
│ ├── reportWebVitals.js
│ ├── core
│ │ └── Menu.js
│ ├── webrtc
│ │ └── webrtc.js
│ ├── MainRouter.js
│ ├── index.js
│ ├── App.css
│ ├── components
│ │ ├── Video.jsx
│ │ └── chat
│ │ │ ├── TextInput.js
│ │ │ ├── ChatLog.js
│ │ │ └── Message.js
│ ├── room
│ │ ├── api-room.js
│ │ ├── Create.js
│ │ └── Room.js
│ └── logo.svg
├── .gitignore
├── package.json
└── README.md
├── README.md
├── src
├── db
│ └── pscale.js
├── options.js
├── room.js
├── routes
│ └── room.routes.js
├── server.js
├── express.js
├── ssl
│ ├── server.crt
│ └── server.key
├── controllers
│ └── room.controller.js
└── socket.js
├── .eslintrc.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | node_modules
3 | .env
4 |
--------------------------------------------------------------------------------
/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethand91/simple-meeting/HEAD/app/public/favicon.ico
--------------------------------------------------------------------------------
/app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethand91/simple-meeting/HEAD/app/public/logo192.png
--------------------------------------------------------------------------------
/app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethand91/simple-meeting/HEAD/app/public/logo512.png
--------------------------------------------------------------------------------
/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | A simple 1 to 1 metting application created with React, Nodejs, PlanetScale, WebRTC.
4 |
5 | This project was created for my PlanetScale X Hashnode Hackathon
6 |
--------------------------------------------------------------------------------
/src/db/pscale.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const mysql = require('mysql2');
3 |
4 | const connection = mysql.createConnection(process.env.DATABASE_URL);
5 |
6 | module.exports = connection;
7 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const options = {
4 | key: fs.readFileSync('./src/ssl/server.key'),
5 | cert: fs.readFileSync('./src/ssl/server.crt')
6 | };
7 |
8 | module.exports = options;
9 |
--------------------------------------------------------------------------------
/app/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/app/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/app/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter } from 'react-router-dom';
2 |
3 | import './App.css';
4 | import MainRouter from './MainRouter';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": false,
4 | "node": true,
5 | "commonjs": true,
6 | "es2021": true
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "ecmaVersion": "latest"
11 | },
12 | "rules": {
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/room.js:
--------------------------------------------------------------------------------
1 | class Room {
2 | constructor () {
3 | this.users = new Set();
4 | }
5 |
6 | addUser (socketId) {
7 | this.users.add(socketId);
8 | }
9 |
10 | removeUser (socketId) {
11 | this.users.delete(socketId);
12 | }
13 |
14 | count () {
15 | return this.users.size;
16 | }
17 | }
18 |
19 | module.exports = Room
20 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/src/routes/room.routes.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const roomCtrl = require('./../controllers/room.controller');
4 |
5 | const router = express.Router();
6 |
7 | router.route('/api/rooms')
8 | .post(roomCtrl.create);
9 |
10 | router.route('/api/rooms/:roomId')
11 | .get(roomCtrl.login)
12 | .delete(roomCtrl.remove);
13 |
14 | router.param('roomId', roomCtrl.roomById);
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/app/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/app/src/core/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AppBar from '@material-ui/core/AppBar';
3 | import Toolbar from '@material-ui/core/Toolbar';
4 | import Typography from '@material-ui/core/Typography';
5 |
6 | const Menu = () => (
7 |
8 |
9 |
10 | Simple Meeting
11 |
12 |
13 |
14 | );
15 |
16 | export default Menu;
17 |
--------------------------------------------------------------------------------
/app/src/webrtc/webrtc.js:
--------------------------------------------------------------------------------
1 | const getLocalMediaStream = async () =>
2 | await navigator.mediaDevices.getUserMedia({ audio: true, video: { width: 640, height: 480 } });
3 |
4 | const initializePeerConnection = () => {
5 | const config = { iceServers: [{ urls: [ 'stun:stun1.l.google.com:19302' ] }] };
6 | const peerConnection = new RTCPeerConnection(config);
7 |
8 | return peerConnection;
9 | };
10 |
11 | export {
12 | getLocalMediaStream,
13 | initializePeerConnection
14 | };
15 |
--------------------------------------------------------------------------------
/app/src/MainRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Routes } from 'react-router-dom';
3 |
4 | import Menu from './core/Menu';
5 | import Create from './room/Create';
6 | import Room from './room/Room';
7 |
8 | const MainRouter = () => {
9 | return (
10 |
11 |
12 |
13 | }/>
14 | }/>
15 |
16 |
17 | );
18 | };
19 |
20 | export default MainRouter;
21 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | const app = require('./express');
2 | const socket = require('./socket')(app);
3 |
4 | const connection = require('./db/pscale');
5 |
6 | const PORT = process.env.PORT || 3000;
7 |
8 | try {
9 | connection.connect();
10 | console.log('database connection established');
11 |
12 | app.listen(PORT, error => {
13 | if (error) {
14 | console.error(error);
15 | process.exit(1);
16 | }
17 |
18 | console.log(`server started on port ${PORT}`);
19 | });
20 | } catch (error) {
21 | console.error('app failed to start', error);
22 | process.exit(1);
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple-meeting",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/server.js",
6 | "scripts": {
7 | "start": "node src/server.js",
8 | "heroku-postbuild": "cd app && npm i && npm run build",
9 | "lint": "eslint ./src/**/*.js",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "body-parser": "^1.20.0",
17 | "compression": "^1.7.4",
18 | "cors": "^2.8.5",
19 | "dotenv": "^16.0.1",
20 | "express": "^4.18.1",
21 | "helmet": "^5.1.0",
22 | "mysql2": "^2.3.3",
23 | "socket.io": "^4.5.1",
24 | "uuid": "^8.3.2"
25 | },
26 | "devDependencies": {
27 | "eslint": "^8.20.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/express.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const http = require('http');
4 | const bodyParser = require('body-parser');
5 | const compress = require('compression');
6 | const cors = require('cors');
7 | const helmet = require('helmet');
8 |
9 | const options = require('./options');
10 | const roomRoutes = require('./routes/room.routes');
11 |
12 | const app = express();
13 |
14 | app.use(express.static(path.join(__dirname, "./../app/build")));
15 |
16 | app.use(bodyParser.json());
17 | app.use(compress());
18 | app.use(helmet());
19 | app.use(cors());
20 |
21 | app.use('/', roomRoutes);
22 |
23 | app.get('*', (req, res) => {
24 | res.sendFile(path.join(__dirname + './../app/build/index.html'));
25 | });
26 |
27 | const server = http.createServer(app);
28 |
29 | module.exports = server;
30 |
--------------------------------------------------------------------------------
/app/src/components/Video.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 |
4 | const useStyles = makeStyles(theme => ({
5 | localVideo: {
6 | 'z-index': 1,
7 | background: 'clear',
8 | position: 'absolute',
9 | 'object-fit': 'cover',
10 | top: 0,
11 | left: 0
12 | },
13 | remoteVideo: {
14 | width: '100vw',
15 | height: '90vh',
16 | background: 'clear',
17 | position: 'absolute',
18 | 'object-fit': 'cover',
19 | top: 0,
20 | left: 0,
21 | }
22 | }));
23 |
24 | export const Video = forwardRef ((props, ref) => {
25 | const classes = useStyles();
26 |
27 | return (
28 |
36 | );
37 | });
38 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.12.4",
7 | "@material-ui/icons": "^4.11.3",
8 | "@testing-library/jest-dom": "^5.16.4",
9 | "@testing-library/react": "^13.3.0",
10 | "@testing-library/user-event": "^13.5.0",
11 | "axios": "^0.27.2",
12 | "react": "^18.2.0",
13 | "react-copy-to-clipboard": "^5.1.0",
14 | "react-dom": "^18.2.0",
15 | "react-router-dom": "^6.3.0",
16 | "react-scripts": "5.0.1",
17 | "socket.io-client": "^4.5.1",
18 | "web-vitals": "^2.1.4"
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": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/ssl/server.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDazCCAlOgAwIBAgIUXZSfn8NSSe5A8yfdjKCtVXbqxY4wDQYJKoZIhvcNAQEL
3 | BQAwRTELMAkGA1UEBhMCSlAxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA3MjQwMzEwNDhaFw0yMzA3
5 | MjQwMzEwNDhaMEUxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
7 | AQUAA4IBDwAwggEKAoIBAQC6rXlRlve8ZWCn9rDpqzn+LE8A1IO6tQs3f9nZyYeB
8 | wytun2J8tQv2cQKuYN+fcNCjyYBz06qGV6OrxUQ21RA73re/KiwqlWjHqdy1LwET
9 | quUCvS7QDzDSIKP4D0n6SrcFnz59heGx6+r9uUkds+DNU7xrmQmfLevqhim5ePiU
10 | flg3jwSKOXUTR6C+shV98BMXVoegHRvfAUFzlQsBeQKnDPEPAwIcUZQ07JezXlFQ
11 | lyh6AjHayyxTF8QUWiM8AOev2iBxTdMQf5LsIExQC5un9O6bk/RaxbME6A8fIsNG
12 | 9Btk2HjFAu10RErPY0cWpBDf2TLwwJjGxHOLuA9SKtn9AgMBAAGjUzBRMB0GA1Ud
13 | DgQWBBSMWJLTIBV4CTH/Ls1ClRpuaW2rEzAfBgNVHSMEGDAWgBSMWJLTIBV4CTH/
14 | Ls1ClRpuaW2rEzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCR
15 | 3r+MzyNmYvG7dGSP5KC0cs90Q4AceZrk/l+0zOlVfO3X/2+xVUjwWXwimq9wlP3I
16 | PiLuRyTgt6/acsADFLrVt3iso4hTcVAiOOlpdpHeLQL4e6zpYmbdbZE/AONa2c58
17 | KzQj/fig00lZ8OU4Pse/XOwJL8xxI23h1slf9HETEBVwfLxPQJDJnT3Q2KUgtacx
18 | ITuFN8krpf+787fjdW1dDVZLxPmjWmb+xbF2Vx8r7UagZHkw+8LKexkczs1tC0rO
19 | 1O/9PrOVc0TjDHuzVjkDt+yAaKL53fwCG59xzMNntupu3ETMfIqFDWWgw6RpYU2m
20 | Q6H35ov1b1pK4iKuo07j
21 | -----END CERTIFICATE-----
22 |
--------------------------------------------------------------------------------
/app/src/room/api-room.js:
--------------------------------------------------------------------------------
1 | const create = async () => {
2 | try {
3 | const response = await fetch(`https://${window.location.hostname}/api/rooms/`, {
4 | method: 'POST',
5 | headers: {
6 | 'Accept': 'application/json',
7 | 'Content-Type': 'application/json'
8 | }
9 | });
10 |
11 | return await response.json();
12 | } catch (error) {
13 | console.error(error);
14 | }
15 | };
16 |
17 | const login = async (roomId, signal) => {
18 | try {
19 | const response = await fetch(`https://${window.location.hostname}/api/rooms/${roomId}`, {
20 | method: 'GET',
21 | signal,
22 | headers: {
23 | 'Accept': 'application/json',
24 | 'Content-Type': 'application/json'
25 | }
26 | });
27 |
28 | return await response.json();
29 | } catch (error) {
30 | console.error(error);
31 | }
32 | };
33 |
34 | const remove = async (roomId) => {
35 | try {
36 | const response = await fetch(`https://${window.location.hostname}/api/rooms/${roomId}`, {
37 | method: 'DELETE',
38 | headers: {
39 | 'Accept': 'application/json',
40 | 'Content-Type': 'application/json'
41 | }
42 | });
43 |
44 | return await response.json();
45 | } catch (error) {
46 | console.error(error);
47 | }
48 | };
49 |
50 | export {
51 | create,
52 | login,
53 | remove
54 | };
55 |
--------------------------------------------------------------------------------
/src/controllers/room.controller.js:
--------------------------------------------------------------------------------
1 | const { v4: uuidv4 } = require('uuid');
2 |
3 | const connection = require('./../db/pscale');
4 |
5 | const create = async (req, res) => {
6 | try {
7 | const key = uuidv4();
8 |
9 | await connection.promise().query('INSERT INTO room (`key`) VALUES (?)', [key]);
10 |
11 | return res.status(200).json({ key });
12 | } catch (error) {
13 | console.error(error);
14 | return res.status(400).json({ error: 'failed to create room' });
15 | }
16 | };
17 |
18 | const login = async (req, res) => {
19 | try {
20 | return res.status(200);
21 | } catch (error) {
22 | console.error(error);
23 | }
24 | };
25 |
26 | const remove = async (req, res) => {
27 | try {
28 | const room = req.room;
29 |
30 | await connection.promise().query(`delete from room where id=${room.id}`);
31 |
32 | return res.status(200);
33 | } catch (error) {
34 | return res.status(400).json({ error: 'failed to delete room' });
35 | }
36 | };
37 |
38 | const roomById = async (req, res, next, key) => {
39 | try {
40 | const result = await connection.promise().query('SELECT * FROM room WHERE room.`key` = ? limit 1', [key]);
41 |
42 | if (!result[0][0]) throw new Error('room not found');
43 |
44 | req.room = result[0][0];
45 | next();
46 | } catch (error) {
47 | console.error(error);
48 | return res.status(404).json({ error: 'room could not be found' });
49 | }
50 | };
51 |
52 | module.exports = {
53 | create,
54 | login,
55 | remove,
56 | roomById
57 | };
58 |
--------------------------------------------------------------------------------
/app/src/components/chat/TextInput.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import TextField from '@material-ui/core/TextField';
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import SendIcon from '@material-ui/icons/Send';
5 | import Button from '@material-ui/core/Button';
6 |
7 | const useStyles = makeStyles(theme => ({
8 | form: {
9 | display: 'flex',
10 | justifyContent: 'center',
11 | width: '95%',
12 | margin: `${theme.spacing(0)} auto`
13 | },
14 | text: {
15 | width: '100%'
16 | }
17 | }));
18 |
19 | export const TextInput = (props) => {
20 | const classes = useStyles();
21 | const [ message, setMessage ] = useState('');
22 | const textInput = useRef(undefined);
23 |
24 | const handleTextChange = event => {
25 | setMessage(event.target.value);
26 | };
27 |
28 | const handleButtonClick = () => {
29 | setMessage('');
30 | textInput.current.value = '';
31 | props.handleSendNewChatMessage(message);
32 | };
33 |
34 | return (
35 | <>
36 |
53 | >
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/socket.js:
--------------------------------------------------------------------------------
1 | const socket = require('socket.io');
2 |
3 | const Room = require('./room');
4 |
5 | module.exports = function (server) {
6 | const io = socket(server, {
7 | cors: {
8 | origin: '*'
9 | }
10 | });
11 |
12 | const rooms = new Map();
13 |
14 | io.on('connection', (socket) => {
15 | console.log('new connection', socket.id);
16 |
17 | socket.on('message', data => {
18 | socket.broadcast.emit('message', data);
19 | });
20 |
21 | socket.on('init', roomKey => {
22 | socket.roomKey = roomKey;
23 |
24 | if (!rooms.has(roomKey)) {
25 | const room = new Room();
26 | room.addUser(socket.id);
27 | rooms.set(roomKey, room);
28 |
29 | console.log('new room created');
30 |
31 | return;
32 | }
33 |
34 | if (rooms.get(roomKey) === socket.id) return;
35 |
36 | if (Array.from(rooms.values()).length === 2) {
37 | socket.disconnect();
38 |
39 | return;
40 | }
41 |
42 | const room = rooms.get(roomKey);
43 | room.addUser(socket.id);
44 | socket.broadcast.emit('offer');
45 | });
46 |
47 | socket.once('disconnect', () => {
48 | console.log('disconnect', socket.roomKey);
49 | if (rooms.has(socket.roomKey)) {
50 | const room = rooms.get(socket.roomKey);
51 |
52 | room.removeUser(socket.id);
53 |
54 | if (room.count() === 0) {
55 | rooms.delete(socket.roomKey);
56 |
57 | console.log('room was deleted', socket.roomKey);
58 | }
59 | }
60 | });
61 |
62 | socket.once('error', error => {
63 | console.error(error);
64 | });
65 | });
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/components/chat/ChatLog.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import { Paper } from '@material-ui/core';
4 |
5 | import { TextInput } from './TextInput';
6 | import { MessageLeft, MessageRight } from './Message';
7 |
8 | const useStyles = makeStyles(theme => ({
9 | paper: {
10 | height: '100%',
11 | maxWidth: '400px',
12 | minWidth: '400px',
13 | display: 'flex',
14 | alignItems: 'center',
15 | flexDirection: 'column',
16 | },
17 | container: {
18 | height: '90vh',
19 | display: 'flex-end',
20 | alignItems: 'center',
21 | },
22 | messageBody: {
23 | width: '100%',
24 | height: '100%',
25 | margin: 10,
26 | overFlowY: 'scroll',
27 | }
28 | }));
29 |
30 | export const ChatLog = props => {
31 | const classes = useStyles();
32 |
33 | useEffect(() => {
34 |
35 | }, []);
36 |
37 | return (
38 |
39 |
40 |
41 | {
42 | props.messages.map((data, i) => {
43 | {
44 | return data.isLocal ? (
45 |
49 | ) : (
50 |
54 | )
55 | }
56 | })
57 | }
58 |
59 |
62 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/ssl/server.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6rXlRlve8ZWCn
3 | 9rDpqzn+LE8A1IO6tQs3f9nZyYeBwytun2J8tQv2cQKuYN+fcNCjyYBz06qGV6Or
4 | xUQ21RA73re/KiwqlWjHqdy1LwETquUCvS7QDzDSIKP4D0n6SrcFnz59heGx6+r9
5 | uUkds+DNU7xrmQmfLevqhim5ePiUflg3jwSKOXUTR6C+shV98BMXVoegHRvfAUFz
6 | lQsBeQKnDPEPAwIcUZQ07JezXlFQlyh6AjHayyxTF8QUWiM8AOev2iBxTdMQf5Ls
7 | IExQC5un9O6bk/RaxbME6A8fIsNG9Btk2HjFAu10RErPY0cWpBDf2TLwwJjGxHOL
8 | uA9SKtn9AgMBAAECggEAKccnDo511RDvJ8f64eCzMuIhsDLfZEqePwZ679W9YNoX
9 | /0LmXLh7++Rg0DvX8CvkVbOD7ughEr3MYGXelVLrtipq/vpmNgWIFJ88SMRDPm6R
10 | oCJMtr2flHc/mwV47e4ItdZkutzanOIKjkwIRAB8wBcMZRwz3g797FeoQN0E7N5T
11 | 7UkLaM/WQduREJhs7kYzAde/H8NnaLmdtGT2yb5UVhMDg1614X66fBbJ4SPW0+Ez
12 | iFCrJCSZIQD9Q+r9NS9Nqh6AAD4somSgqJsY0zCugzNcVGS1k1q9wJ1OcPzbbSSp
13 | gtnUg0vn8uK92VfeGxMSaJP9+JwhoYyibH/fNMFHIQKBgQDeILKZDZhSJ/MuVj5S
14 | +3ev17rAiXuoY9R3TkK6QRkDvSqsDsZeQ46+AEDVkOAOf8wOaUzPVhEbldRZV3Vb
15 | QXQISLy4N0Bl33geRB25Htz3YuT9knhHN97JOEEIv56JlkoQIEVl7sqj9R3cyH4A
16 | wUkdYEJVmoiqCL/9zqzfSejH2QKBgQDXJOUHtDdzSJKwk610O6gRSHZJaN2K7/FZ
17 | yxJpoPSZV0NN6c3siieHsz5frViLC7fTQyFLvny1lU8JZc98Iyyz87j9x0VaDUdI
18 | 3Bj2DCK5xnyr8FuBtZTFrjqUz7BpesOnwLhMA6486nawSAoYXDYvLiQp7rsmD13X
19 | 1BP44jeQxQKBgQCiGxK8B+Pl8SnT4zTQspyCQ9oSsUMBfPmNqBuieeebhu4haxbx
20 | rDP8DYtDUNtzOWjeC5L2rUUG8K1sFhubYYUglpTDi+7/abrru6JFe3SfRhj/xWjp
21 | 8KgyCU/M5qr4limu4x5CaaaRSU1l1xu9yVFmkt1WQ9UA4inbPH2E5xdu4QKBgQC1
22 | ua9PY5VW5l0pk4P24xEikB+CAHbpjaVCoHpMCK2y/HeYTz4mZ8feIrQz4tsgj+RV
23 | KaXtMdhrFNQu7vVkON3gnqSKkBBvcTnePDNFWZjXbOYP4bWZiYRBudo3qnqrjgvI
24 | HcxOQOmjALUCT8dfLjyCe6oGVWV1T5OH49Z+6q3etQKBgHjvAphsWr7KmTA6KEfl
25 | wNIDfsqISUPB3kT74Wz4Oafgq1KrdwqvcV3SM7a7oR7lb3XMUvoq8EMaTjWtNm5l
26 | Wo36XTwAlkYNDBlGSqK631atVjBlx79xTMhUMzJDPEVrlXRrnVgyLIbjZUp2tzVd
27 | RuwaGX1/ReAhZPvrs01F9c9r
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/components/chat/Message.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import { deepOrange } from '@material-ui/core/colors';
4 |
5 | const useStyles = makeStyles(theme => ({
6 | messageRow: {
7 | display: 'flex'
8 | },
9 | messageRowRight: {
10 | display: 'flex',
11 | justifyContent: 'flex-end'
12 | },
13 | messageBlue: {
14 | position: 'relative',
15 | marginLeft: '20px',
16 | marginBottom: '10px',
17 | padding: '10px',
18 | backgroudColor: 'blue',
19 | width: '60%',
20 | textAlign: 'left',
21 | border: '1px solid #97C6E3',
22 | borderRadius: '10px',
23 | '&:after': {
24 | content: "''",
25 | position: 'absolute',
26 | width: '0',
27 | height: '0',
28 | borderTop: '15px solid #A8DDFD',
29 | borderLeft: '15px solid transparent',
30 | borderRight: '15px solid transparent',
31 | top: '0',
32 | left: '-15px'
33 | },
34 | '&:before': {
35 | content: "''",
36 | position: 'absolute',
37 | width: '0',
38 | height: '0',
39 | borderTop: '17px solid #97C6E3',
40 | borderLeft: '16px solid transparent',
41 | borderRight: '16px solid transparent',
42 | top: '-1px',
43 | left: '-17px'
44 | }
45 | },
46 | messageOrange: {
47 | position: 'relative',
48 | marginRight: '20px',
49 | marginBottom: '10px',
50 | padding: '10px',
51 | backgroundColor: '#f8e896',
52 | width: '60%',
53 | textAlign: 'left',
54 | border: '1px solid #dfd087',
55 | borderRadius: '10px',
56 | '&:after': {
57 | content: "''",
58 | position: 'absolute',
59 | width: '0',
60 | height: '0',
61 | borderTop: '15px solid #f8e896',
62 | borderLeft: '15px solid transparent',
63 | borderRight: '15px solid transparent',
64 | top: '0',
65 | right: '-15px'
66 | },
67 | '&:before': {
68 | content: '""',
69 | position: 'absolute',
70 | width: '0',
71 | height: '0',
72 | borderTop: '17px solid #dfd087',
73 | borderLeft: '16px solid transparent',
74 | borderRight: '16px solid transparent',
75 | top: '-1px',
76 | right: '-17px'
77 | }
78 | },
79 | messageContent: {
80 | padding: 0,
81 | margin: 0
82 | },
83 | orange: {
84 | color: theme.palette.getContrastText(deepOrange[500]),
85 | backgroundColor: deepOrange[500],
86 | width: theme.spacing(4),
87 | height: theme.spacing(4)
88 | }
89 | }));
90 |
91 | export const MessageLeft = props => {
92 | const classes = useStyles();
93 |
94 | return (
95 | <>
96 |
97 |
98 |
99 |
{ props.message }
100 |
101 |
102 |
103 | >
104 | );
105 | };
106 |
107 | export const MessageRight = props => {
108 | const classes = useStyles();
109 |
110 | return (
111 | <>
112 |
113 |
114 |
{ props.message }
115 |
116 |
117 | >
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/app/src/room/Create.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Card from '@material-ui/core/Card';
3 | import CardActions from '@material-ui/core/CardActions';
4 | import CardContent from '@material-ui/core/CardContent';
5 | import Button from '@material-ui/core/Button';
6 | import Typography from '@material-ui/core/Typography';
7 | import Icon from '@material-ui/core/Icon';
8 | import { makeStyles } from '@material-ui/core/styles';
9 | import Dialog from '@material-ui/core/Dialog';
10 | import DialogActions from '@material-ui/core/DialogActions';
11 | import DialogContent from '@material-ui/core/DialogContent';
12 | import DialogContentText from '@material-ui/core/DialogContentText';
13 | import DialogTitle from '@material-ui/core/DialogTitle';
14 | import { Link } from 'react-router-dom';
15 | import AssignmentIcon from '@material-ui/icons/Assignment';
16 | import Tooltip from '@material-ui/core/Tooltip';
17 | import InputAdornment from '@material-ui/core/InputAdornment';
18 | import IconButton from '@material-ui/core/IconButton';
19 | import CopyToClipBoard from 'react-copy-to-clipboard';
20 |
21 | import { create } from './api-room';
22 |
23 | const useStyles = makeStyles(theme => ({
24 | card: {
25 | maxWidth: 600,
26 | margin: 'auto',
27 | textAlign: 'center',
28 | marginTop: theme.spacing(5),
29 | paddingBottom: theme.spacing(2)
30 | },
31 | error: {
32 | verticalAlign: 'middle'
33 | },
34 | title: {
35 | marginTop: theme.spacing(2),
36 | },
37 | textField: {
38 | marginLeft: theme.spacing(1),
39 | marginRight: theme.spacing(1),
40 | width: 300
41 | },
42 | submit: {
43 | margin: 'auto',
44 | marginBottom: theme.spacing(2)
45 | }
46 | }));
47 |
48 | export default function Create () {
49 | const classes = useStyles();
50 | const [ values, setValues ] = useState({
51 | open: false,
52 | roomKey: '',
53 | error: '',
54 | openTooltip: false
55 | });
56 |
57 | const clickSubmit = async () => {
58 | console.log('create room');
59 | const data = await create();
60 |
61 | if (data.error) {
62 | setValues({ ...values, error: data.error });
63 |
64 | return;
65 | }
66 |
67 | console.log('got data', data);
68 | setValues({ ...values, roomKey: data.key, open: true });
69 | };
70 |
71 | const handleClipBoardClicked = () => {
72 | setValues({ ...values, openTooltip: true });
73 | };
74 |
75 | const closeTooltip = () => {
76 | setValues({ ...values, openTooltip: false });
77 | };
78 |
79 | return (
80 |
81 |
82 |
83 |
84 | Room Generator
85 |
86 | {
87 | values.error &&
88 | (
89 |
90 | error
91 | { values.error }
92 |
93 | )
94 | }
95 |
96 |
97 | Create Room
98 |
99 |
100 |
101 |
102 |
103 |
104 | New room was created succesfully access it via the following url:
105 |
106 |
107 | Copy URL:
108 |
109 |
110 |
118 |
119 |
120 |
121 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Go To Room
135 |
136 |
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/app/src/room/Room.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Grid from '@material-ui/core/Grid';
4 | import { Navigate, useParams, useNavigate } from 'react-router-dom';
5 | import io from 'socket.io-client';
6 |
7 | import { login } from './api-room';
8 | import { Video } from './../components/Video';
9 | import { ChatLog } from './../components/chat/ChatLog';
10 | import {
11 | getLocalMediaStream,
12 | initializePeerConnection
13 | } from './../webrtc/webrtc';
14 |
15 | const useStyles = makeStyles(theme => ({
16 | videoGrid: {
17 | position: 'relative'
18 | },
19 | chatLog: {
20 | flex: 1,
21 | maxWidth: '400px',
22 | marginLeft: 'auto'
23 | }
24 | }));
25 |
26 | export default function Room() {
27 | const params = useParams();
28 | const navigate = useNavigate();
29 | const videoRef = useRef(undefined);
30 | const remoteVideoRef = useRef(undefined);
31 | const socketRef = useRef();
32 | const classes = useStyles();
33 | const [ values, setValues ] = useState({
34 | redirectToCreate: false,
35 | localMediaStream: undefined,
36 | });
37 | const [ messages, setMessages ] = useState([]);
38 | const peerConnection = initializePeerConnection();
39 | const remoteMediaStream = new MediaStream();
40 |
41 | useEffect(() => {
42 | console.log('init');
43 | const abortController = new AbortController();
44 | const signal = abortController.signal;
45 |
46 | login(params.roomKey, signal).then((data) => {
47 | if (data && data.error) {
48 | setValues({ ...values, redirectToCreate: true });
49 |
50 | return;
51 | }
52 | });
53 |
54 | return function cleanup() {
55 | abortController.abort();
56 | }
57 | }, [ params.roomKey ]);
58 |
59 | useEffect(() => {
60 | // init socket
61 | socketRef.current = io(`https://${window.location.hostname}`, { reconnection: false });
62 | handleSocket(params.roomKey);
63 |
64 | return () => {
65 | socketRef.current.disconnect();
66 | };
67 | }, []);
68 |
69 | const handleSocket = (roomKey) => {
70 | socketRef.current.once('connect', () => {
71 | console.log('socket connected');
72 | initializeLocalMedia(roomKey);
73 | });
74 |
75 | socketRef.current.once('disconnect', reason => {
76 | console.log('socket disconnected due to reason', reason);
77 |
78 | if (videoRef.current) {
79 | const mediaTracks = videoRef.current.srcObject.getTracks();
80 |
81 | for (const mediaTrack of mediaTracks) {
82 | mediaTrack.stop();
83 | }
84 | }
85 |
86 | navigate('/');
87 | });
88 |
89 | socketRef.current.once('init', async () => {
90 | try {
91 | console.log('init call');
92 | } catch (error) {
93 | console.error(error);
94 | }
95 | });
96 |
97 | socketRef.current.once('offer', async () => {
98 | try {
99 | console.log('create local offer');
100 | const offer = await peerConnection.createOffer();
101 | await peerConnection.setLocalDescription(offer);
102 | socketRef.current.emit('message', {
103 | action: 'offer',
104 | offer: peerConnection.localDescription
105 | });
106 | } catch (error) {
107 | console.error(error);
108 | }
109 | });
110 |
111 | socketRef.current.on('message', message => {
112 | console.log('remote message', message);
113 |
114 | handleRemoteMessage(message);
115 | });
116 | };
117 |
118 | const initializeLocalMedia = async (roomKey) => {
119 | try {
120 | const mediaStream = await getLocalMediaStream();
121 | console.log('local media devices initialized');
122 |
123 | await setValues({ ...values, localMediaStream: mediaStream });
124 |
125 | setTimeout(() => {
126 | videoRef.current.srcObject = mediaStream;
127 | videoRef.current.play();
128 |
129 |
130 | handlePeerConnection(roomKey);
131 | }, 1000);
132 | } catch (error) {
133 | console.error(error);
134 | }
135 | };
136 |
137 | const handlePeerConnection = (roomKey) => {
138 | console.log('peerConnection', peerConnection);
139 |
140 | peerConnection.onicecandidate = ({ candidate }) => {
141 | if (!candidate) return;
142 |
143 | console.log('new candidate', candidate);
144 | socketRef.current.emit('message', {
145 | action: 'candidate',
146 | candidate
147 | });
148 | };
149 |
150 | peerConnection.oniceconnectionstatechange = () => {
151 | console.log('iceconnectionstatechange', peerConnection.iceConnectionState);
152 |
153 | if (peerConnection.iceConnectionState === 'disconnected') {
154 | console.warn('state is disconnected');
155 | }
156 |
157 | if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.isConnectionState === 'closed') {
158 | socketRef.current.disconnect();
159 | }
160 | };
161 |
162 | peerConnection.ontrack = ({ track }) => {
163 | console.log('on remote track', track.kind);
164 |
165 | remoteMediaStream.addTrack(track);
166 |
167 | if (track.kind === 'video') {
168 | remoteVideoRef.current.srcObject = remoteMediaStream;
169 | remoteVideoRef.current.load();
170 | }
171 | };
172 |
173 | for (const mediaTrack of videoRef.current.srcObject.getTracks()) {
174 | peerConnection.addTrack(mediaTrack);
175 | }
176 |
177 | console.log('init');
178 | socketRef.current.emit('init', roomKey);
179 | };
180 |
181 | const handleRemoteMessage = async (message) => {
182 | try {
183 | switch(message.action) {
184 | case 'offer':
185 | console.log('remote offer');
186 | await peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
187 |
188 | const answer = await peerConnection.createAnswer();
189 | await peerConnection.setLocalDescription(answer);
190 |
191 | socketRef.current.emit('message', {
192 | action: 'answer',
193 | answer
194 | });
195 | break;
196 | case 'answer':
197 | console.log('remote answer');
198 | await peerConnection.setRemoteDescription(message.answer);
199 | break;
200 | case 'candidate':
201 | console.log('remote ice');
202 | await peerConnection.addIceCandidate(message.candidate);
203 | break;
204 | case 'chat':
205 | message.data.isLocal = false;
206 | console.log('chat message', message.data);
207 |
208 | setMessages(prevMessages => [ ...prevMessages, message.data ]);
209 | break;
210 | default: console.warn('unknown action', message.action);
211 | }
212 | } catch (error) {
213 | console.error(error);
214 | }
215 | };
216 |
217 | const handleSendNewChatMessage = message => {
218 | console.log('send', message);
219 |
220 | const data = { isLocal: true, message };
221 | setMessages(prevMessages => [ ...prevMessages, data ]);
222 | console.log(messages);
223 |
224 | socketRef.current.emit('message', {
225 | action: 'chat',
226 | data
227 | });
228 | };
229 |
230 | if (values.redirectToCreate) {
231 | return ;
232 | }
233 |
234 | return (
235 |
236 |
237 |
238 | { values.localMediaStream &&
239 |
247 | }
248 |
249 |
255 |
256 |
257 |
258 |
259 |
264 |
265 |
266 |
267 |
268 | );
269 | };
270 |
--------------------------------------------------------------------------------