├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ └── feature_request.md └── workflows │ ├── npm-publish.yml │ ├── rtconnect-test.yml │ ├── site-s3-cloudfront.yml │ └── slack-notify.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── assets ├── RTConnect-demo.gif └── RTConnect-logo-transparent.png ├── dist ├── server │ ├── server.d.ts │ └── server.js └── src │ ├── components │ ├── Socket.d.ts │ ├── Socket.js │ ├── VideoCall.d.ts │ ├── VideoCall.js │ ├── VideoComponent.d.ts │ └── VideoComponent.js │ ├── constants │ ├── actions.d.ts │ ├── actions.js │ ├── mediaStreamConstraints.d.ts │ ├── mediaStreamConstraints.js │ ├── rtcConfiguration.d.ts │ └── rtcConfiguration.js │ ├── index.d.ts │ ├── index.js │ └── utils │ ├── Livestream.d.ts │ └── Livestream.js ├── jest.config.ts ├── lib ├── __tests__ │ ├── integration │ │ └── server.integration.test.ts │ ├── react-test │ │ ├── VideoCall.unit.test.tsx │ │ ├── VideoComponent.unit.test.tsx │ │ └── __snapshots__ │ │ │ └── VideoComponent.unit.test.tsx.snap │ └── unit │ │ └── ws.unit.test.ts ├── server │ └── server.ts └── src │ ├── components │ ├── InputButtons.jsx │ ├── Socket.tsx │ ├── VideoCall.tsx │ └── VideoComponent.tsx │ ├── constants │ ├── actions.ts │ ├── mediaStreamConstraints.ts │ └── rtcConfiguration.ts │ └── index.ts ├── package-lock.json ├── package.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't lint build or dist output 2 | dist 3 | build 4 | tsconfig.json -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react", 21 | "@typescript-eslint" 22 | ], 23 | "rules": { 24 | "indent": ["warn", 2, { 25 | "ignoreComments": false, 26 | "SwitchCase": 1, 27 | "MemberExpression": 0, 28 | "flatTernaryExpressions": true 29 | }], 30 | "no-unused-vars": ["off", { "vars": "local" }], 31 | "no-case-declarations": "warn", 32 | "prefer-const": "warn", 33 | "quotes": ["warn", "single", { "allowTemplateLiterals": true }], 34 | "react/prop-types": "off", 35 | "semi": ["warn", "always"], 36 | "space-infix-ops": "warn" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Automate NPM Package Publish 2 | 3 | on: 4 | push: 5 | branches: ["main", "npm-latest-publish"] 6 | pull_request: 7 | branches: ["main", "npm-latest-publish"] 8 | 9 | jobs: 10 | npmPublish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: '20' 17 | - run: npm ci 18 | - name: Publish npm package when version is updated 19 | uses: JS-DevTools/npm-publish@v3 20 | with: 21 | token: ${{ secrets.NPM_TOKEN }} # This works BUT it shows up as problem for some unknown reason ("Context access might be invalid: NPM_TOKEN") and there should not be any errors 22 | 23 | 24 | # https://github.com/JS-DevTools/npm-publish -------------------------------------------------------------------------------- /.github/workflows/rtconnect-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node and various OS (Windows, Ubuntu) 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: RTConnect CI 5 | 6 | on: 7 | push: 8 | branches: [ "main", "dev", "feature/**"] 9 | pull_request: 10 | branches: [ "main", "dev" ] 11 | 12 | jobs: 13 | build_test_lint: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | node-version: [17.x, 18.x, 19.x, 20.x] 18 | os: [windows-latest, ubuntu-latest] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | runs-on: ${{ matrix.os}} 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build 29 | - run: npm run lint 30 | - run: npm test 31 | # - run: npm run unit-test 32 | # env: 33 | # CI: true -------------------------------------------------------------------------------- /.github/workflows/site-s3-cloudfront.yml: -------------------------------------------------------------------------------- 1 | name: Update RTConnect Launch Site 2 | 3 | on: 4 | push: 5 | branches: ["website-s3-publish"] 6 | pull_request: 7 | branches: ["website-s3-publish"] 8 | 9 | env: 10 | AWS_REGION: 'us-east-1' 11 | AWS_S3_BUCKET_NAME: 'rtconnect-s3' 12 | AWS_CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.CDN_ID }} # context is correct - problem is a github actions glitch - see: https://github.com/github/vscode-github-actions/issues/222 13 | S3_BUILD_FILE_PATH: './build' 14 | # ROLE_NAME: '' 15 | 16 | jobs: 17 | deploy-s3: 18 | name: Build & Deploy 19 | runs-on: ubuntu-latest 20 | # environment: production 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: Build app 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20.x' 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | - name: Create build file 33 | run: npm run build 34 | 35 | # # TEST 36 | # - name: Configure AWS credentials from Test account 37 | # uses: aws-actions/configure-aws-credentials@v4 38 | # with: 39 | # role-to-assume: arn:aws:iam::111111111111:role/my-github-actions-role-test 40 | # aws-region: us-east-1 41 | 42 | - name: Configure AWS credentials for Production Account 43 | uses: aws-actions/configure-aws-credentials@v4 44 | with: 45 | # role-to-assume: ${{ env.ROLE_NAME }} 46 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} # context is correct - problem is a github actions glitch - see: https://github.com/github/vscode-github-actions/issues/222 47 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 48 | aws-region: ${{ env.AWS_REGION }} 49 | 50 | - name: Copy files to S3 bucket with AWS CLI 51 | run: | 52 | aws s3 --region ${{ env.AWS_REGION }} sync ./build s3://${{ env.AWS_S3_BUCKET_NAME }} 53 | 54 | - name: Invalidate cached files in CloudFront Distribution to Update Website 55 | run: | 56 | aws cloudfront create-invalidation --distribution-id ${{ env.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --path '/*' -------------------------------------------------------------------------------- /.github/workflows/slack-notify.yml: -------------------------------------------------------------------------------- 1 | name: Slack Notification of CI Status 2 | 3 | on: 4 | push: 5 | branches: ["main", "feature/**"] 6 | pull_request: 7 | branches: ["main"] 8 | env: 9 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} # This works BUT it shows up as problem for some unknown reason ("Context access might be invalid: NPM_TOKEN") and there should not be any errors 10 | # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 11 | jobs: 12 | slackNotification: 13 | runs-on: ubuntu-latest 14 | name: Slack CI status - notify on failure 15 | steps: 16 | - name: Slack Notify on Failure 17 | if: ${{ failure() }} 18 | id: slack 19 | uses: slackapi/slack-github-action@v1.24.0 20 | with: 21 | channel-id: ${{ secrets.SLACK_CHANNEL_ID }} 22 | slack-message: "Github CI Result: ${{ job.status }}\nGithub PR/Commit URL: ${{ github.event.pull_request.html_url || github.event.head_commit.url }}" 23 | 24 | # https://github.com/slackapi/slack-github-action -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | npm-debug.log 3 | .DS_Store 4 | .env 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | jest.config.ts 2 | lib/__tests__ 3 | node_modules 4 | .eslintignore 5 | .eslintrc.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | rtconnect
3 |

4 | 5 |

Implement live streaming and real-time video calls with RTConnect

6 | 7 | RTConnect is open source, React component library that facilitates live, real-time video/audio communications. 8 | 9 | RTConnect achieves these features via our downloadeable npm package, our VideoCall and LiveStream React components, and by providing developers with an importable signaling server module that simplifies the implementation of WebRTC, WebSockets, and signaling to establish low latency, real-time communications. While WebRTC takes care of transcoding, packetizing, networking, and security, it does not take care of signaling and implementing its connection logic is no easy walk in the park for even seasoned developers and front-end developers. 10 | 11 | That is where RTConnect comes in - we take care of signaling and implementing WebRTC connection logic for you so all you have to worry about is building compelling live streaming and video conferencing apps. By using RTConnect and letting us worry about all the technicalities of setting up signaling and WebRTC's connection logic, you can focus all your extra time and energy into what really matters - innovation, creation, maybe even disruption in the world of video conferencing and live streaming apps. Who knows? You might even create the next Zoom or Twitch. 12 | 13 | 14 | ## Table of Contents 15 | - [Key Features & Use Cases](#features) 16 | - [RTConnect Demo](#demo) 17 | - [Installation](#install) 18 | - [Getting Started with RTConnect](#implementation) 19 | - [Setting Up Public Endpoint/URL](#setting) 20 | - [Solutions/Fixes for Polyfill Errors](#errors) 21 | - [Contributing to RTConnect](#contribution) 22 | - [License](#license) 23 | - [The RTConnect Team](#team) 24 | - [Support RTConnect](#support) 25 | 26 | 27 | ## Key Features & Use Cases 28 | * Supports video, voice, screen sharing, and generic data to be sent between peers. 29 | * Importable, WebSockets based signaling server module that allows for the rapid exchange of . 30 | * Rapidly set up live video calls in your React codebase without the hassle of implementing WebRTC. 31 | 32 | 33 | ## Demo 34 |

35 | 36 |

37 | 38 | 39 | ##
Installing RTConnect 40 | 41 | RTConnect is available as an [npm package](https://www.npmjs.com/package/rtconnect). 42 | 43 | **npm:** 44 | ``` 45 | npm install rtconnect 46 | ``` 47 | 48 | ## Getting Started with RTConnect 49 | After installing the rtconnect npm package, import the VideoComponent component in your React file: 50 | 51 | 2. Create your server — you have the option of using an http server or setting up a more secure connection by implementing an https server in order to set up a WebSocket secure connection. 52 | 53 | (Note: Setting up an https server will require a few extra steps. Instructions on how to set up an https server) 54 | 55 | 3. Import the RTConnect Signaling Channel class/module and instantiate the RTConnect Signaling Channel. Pass in your http or https server as an argument. 56 | 57 | 4. Invoke the RTConnect Signaling Channel method, initializeConnection(). 58 | 59 | **server.js:** 60 | ``` 61 | // server.js file 62 | 63 | const path = require('path'); 64 | const express = require('express'); 65 | app.use(bodyParser.urlencoded({ extended: true })); 66 | const PORT = 3000; 67 | const app = express(); 68 | const { SignalingChannel } = require('rtconnect'); // import the RTConnect Signaling Channel class 69 | 70 | 71 | app.use(express.json()); 72 | app.use(bodyParser.urlencoded({extended : true})); 73 | app.use('/build', express.static(path.join(__dirname, '../build'))); 74 | 75 | app.get('/', (req, res) => { 76 | res.status(200).sendFile(path.resolve(__dirname, '../index.html')); 77 | }); 78 | 79 | const server = app.listen(PORT, () => { 80 | console.log('Listening on port', PORT); 81 | }); 82 | 83 | const SignalChannel = new SignalingChannel(server); // instantiate the RTConnect SignalingChannel 84 | 85 | SignalChannel.initializeConnection(); // invoke initializeConnection() method 86 | ``` 87 | 88 | 5. Import the RTConnect VideoCall component into your desired .jsx file. 89 | 90 | 6. Finally use the RTConnect VideoCall component as you would any other React component by passing in `‘ws://localhost:PORT’` as the URL prop as well as the optional mediaOptions prop 91 | 92 | - `URL={ ‘ws://localhost:PORT’}` (Note: the PORT whatever you specified when you set up your server so based on the server above, the port is 3000) 93 | - `mediaOptions={{ controls: true, style: { width: ‘640px’, height: ‘480px’ }}` 94 | 95 | (Note: If you are using an https server, then pass in `‘wss://localhost:PORT’` as the URL prop). 96 | 97 | **App.jsx:** 98 | ``` 99 | // App.jsx file 100 | 101 | import React from 'react'; 102 | import VideoCall from 'rtconnect'; 103 | 104 | const App = () => { 105 | return ( 106 | 110 | ) 111 | } 112 | 113 | export default App; 114 | ``` 115 | 116 | ## Setting Up Public Endpoint/URL Using a Secure Tunnel Service 117 | In order to create a publicly accessible URL that will allow you to share access to your localhost server, you have a number of different options but a simple option is to use a secure tunnel service. One such free, secure tunnel service that you can use to create a secure, encrypted, publicly accessible endpoint/URL that other users can access over the Internet is ngrok. 118 | 119 | ngrok Secure Tunnels operate by using a locally installed ngrok agent to establish a private connection to the ngrok service. Your localhost development server is mapped to an ngrok.io sub-domain, which a remote user can then access. Once the connection is established, you get a public endpoint that you or others can use to access your local port. When a user hits the public ngrok endpoint, the ngrok edge figures out where to route the request and forwards the request over an encrypted connection to the locally running ngrok agent. 120 | 121 | Thus, you do not need to expose ports, set up forwarding, or make any other network changes. You can simply install [ngrok npm package](https://www.npmjs.com/package/ngrok) and run it. 122 | 123 | ### Instructions for Using ngrok With RTConnect 124 | 1. Sign up for a free ngrok account, verify your email address, and copy your authorization token. 125 | 126 | 2. Run the following command and replace with add your own authorization token: 127 | ``` 128 | config authtoken 129 | ``` 130 | 131 | 3. Install the ngrok npm package globally: 132 | ``` 133 | npm install ngrok -g 134 | ``` 135 | 136 | 4. Start your app - make sure your server is running before you initiate the ngrok tunnel. 137 | 138 | * The following is a a basic example of what your App.jsx and server.js files might look like at this point if you used `npx create-react-app`. If you're using a proxy server, then the default port when you run `npm start` is 3000 so set your server port to something else such as 8080. 139 | 140 | **App.jsx:** 141 | ``` 142 | // App.jsx file 143 | 144 | import React from 'react'; 145 | import VideoCall from 'rtconnect'; 146 | 147 | const App = () => { 148 | return ( 149 | 153 | ) 154 | } 155 | 156 | export default App; 157 | ``` 158 | 159 | **server.js:** 160 | ``` 161 | // server.js 162 | 163 | const path = require('path'); 164 | const express = require('express'); 165 | const bodyParser = require('body-parser'); 166 | const ngrok = require('ngrok'); 167 | const PORT = 8080; 168 | const { SignalingChannel } = require('rtconnect'); // import the RTConnect Signaling Channel class 169 | const app = express(); 170 | 171 | app.use(express.json()); 172 | app.use(bodyParser.urlencoded({ extended: true })); 173 | 174 | app.get('/', (req, res) => { 175 | res.status(200).sendFile(path.resolve(__dirname, '../index.html')); 176 | }); 177 | 178 | const server = app.listen(PORT, () => { 179 | console.log('Listening on port', PORT); 180 | }); 181 | 182 | const SignalChannel = new SignalingChannel(server); // instantiate the RTConnect SignalingChannel 183 | 184 | SignalChannel.initializeConnection(); // invoke initializeConnection() method 185 | ``` 186 | 187 | 5. To start your ngrok Secure Tunnel, run the following command in your terminal: 188 | ``` 189 | ngrok http 3000 --host-header="localhost:3000" 190 | ``` 191 | 192 | * To make the connection more secure, you can enforce basic authorization on a tunnel endpoint - just use the username and password of your choosing: 193 | ``` 194 | ngrok http 3000 --host-header="localhost:3000" --auth=':` 195 | ``` 196 | 197 | 6. Copy the https forwarding URL from the terminal and paste it into a new browser tab or send the link to a remote user. 198 | 199 | 200 | ## Polyfill Errors 201 | 202 | If you are using Webpack v5.x or used the `npx create-react-app` command and are getting polyfill errors, the following are some potential solutions. 203 | 204 | Webpack 4 automatically polyfilled many Node APIs in the browser but Webpack 5 removed this functionality, hence why you might get polyfill errors when using the RTConnect VideoCall component. You can do the following to address polyfill errors related to using Webpack v5.x when using RTConnect. 205 | 206 | - [Fixing Polyfill Errors if Using the npx create-react-app Command](#npx). 207 | - [Fixing Polyfill Errors When Using Webpack v5.x](#webpack). 208 | 209 | ### If You Used npx create-react-app to Create Your React App 210 | 211 | 1. First, install the package using Yarn or npm: 212 | ``` 213 | npm install react-app-polyfill 214 | ``` 215 | or 216 | 217 | ``` 218 | yarn add react-app-polyfill 219 | ``` 220 | 221 | 2. Then add the following in your src/index.js file. 222 | 223 | ``` 224 | // These must be the first lines in src/index.js 225 | import "react-app-polyfill/ie11"; 226 | import "react-app-polyfill/stable"; 227 | // ... 228 | ``` 229 | 230 | ### If you are using Webpack v5.x 231 | 1. Add the following to your webpack.config.json 232 | 233 | ``` 234 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin") 235 | 236 | module.exports = { 237 | resolve: { 238 | fallback: { 239 | buffer: require.resolve('buffer/'), 240 | utils: require.resolve('utils'), 241 | tls: require.resolve('tls'), 242 | gyp: require.resolve('gyp'), 243 | fs: false, 244 | } 245 | }, 246 | 247 | target: 'web', 248 | 249 | plugins: [ 250 | new NodePolyfillPlugin(), 251 | ] 252 | } 253 | ``` 254 | 255 | 2. Then install the following npm packages: 256 | 257 | ``` 258 | npm install -D node-polyfill-webpack-plugin buffer utils tls gyp fs 259 | ``` 260 | 261 | ## Contributing to RTConnect 262 | There are many features and improvements that our team is still adding to RTConect but while we are in the process of implementing some of them, feel free to propose any bug fixes or improvements and how to build and test your changes! 263 | 264 | We are currently in the process of: 265 | - Creating group video calls/video conferences with 2 or more peers by implementing an SFU (Selective Forwarding Unit) video routing service and improving streaming by leveraging WebRTC Simulcast 266 | 267 | 268 | ## License 269 | RTConnect is developed under the MIT license. 270 | 271 | 272 | ## The Co-Creators of RTConnect 273 | Anthony King | [GitHub](https://github.com/thecapedcrusader) | [LinkedIn](https://www.linkedin.com/in/aking97) 274 |
275 | F. Raisa Iftekher | [GitHub](https://github.com/fraisai) | [LinkedIn](https://www.linkedin.com/in/fraisa/) 276 |
277 | Yoojin Chang | [GitHub](https://github.com/ychang49265) | [LinkedIn](https://www.linkedin.com/in/yoojin-chang-32a75892/) 278 |
279 | Louis Disen | [GitHub](https://github.com/LouisDisen) | [LinkedIn](https://www.linkedin.com/in/louis-disen/) 280 |
281 | 282 | 283 | ##
A big shoutout to all of RTConnect's stargazers! Thank you! 284 | [![Thank you to all of RTConnect's stargazers](https://git-lister.onrender.com/api/stars/oslabs-beta/RTConnect)](https://github.com/oslabs-beta/RTConnect/stargazers) 285 | -------------------------------------------------------------------------------- /assets/RTConnect-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RTConnect/60ef8652bd04be3a82aeb7a2b8f0be31745fc5c0/assets/RTConnect-demo.gif -------------------------------------------------------------------------------- /assets/RTConnect-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RTConnect/60ef8652bd04be3a82aeb7a2b8f0be31745fc5c0/assets/RTConnect-logo-transparent.png -------------------------------------------------------------------------------- /dist/server/server.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import { WebSocket, WebSocketServer } from 'ws'; 4 | import { Server } from 'http'; 5 | import { Server as httpsServer } from 'https'; 6 | /** 7 | * @class 8 | * @classdesc The SignalingChannel class, which utilizes WebSockets in order to facillitate communication between clients connected to the WebSocket server. 9 | * @prop { WebsocketServer } websocketServer - a simple WebSocket server 10 | * @prop { Map } users - object containing key-value pairs consisting of users' names and their corresponding WebSocket in the following fashion { username1: socket1, username2: socket2, ... , usernameN: socketN } 11 | */ 12 | declare class SignalingChannel { 13 | webSocketServer: WebSocketServer; 14 | users: Map; 15 | /** 16 | * @constructor constructing a websocket server with an http/https object or port passed in upon instantiating SignalingChannel 17 | * @param {Server} server - pass in a server (http or https) or pass in a port (this port cannot be the same as the application port and it has to listen on the same port) 18 | */ 19 | constructor(server: Server | httpsServer | number); 20 | /** 21 | * @description Upon creation and connection to the WebSocket server, the WebSocket server will add these event listeners to their socket to perform key functionality 22 | * @function initializeConnection Signaling server will listen to client when client has connected. 23 | * When the message event is triggered, it will either send each user list to each user upon login or send data to the receiver 24 | * @return a socket that corresponds to the client connecting. 25 | */ 26 | initializeConnection(): void; 27 | /** 28 | * @description Broadcasting from sender to receiver. Accessing the receiver from the data object and if the user exists, the data is sent 29 | * @param {object} data 30 | */ 31 | transmit(data: { 32 | ACTION_TYPE: string; 33 | receiver: string; 34 | }): void; 35 | /** 36 | * @description Getting user from Map 37 | * @function getByValue identifies user and their specific websocket 38 | * @param {Map} map 39 | * @param {WebSocket} searchValue 40 | * @returns {string} user 41 | */ 42 | getByValue(map: Map, searchValue: WebSocket): string; 43 | } 44 | export default SignalingChannel; 45 | -------------------------------------------------------------------------------- /dist/server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const ws_1 = require("ws"); 7 | const actions_1 = __importDefault(require("../src/constants/actions")); 8 | const { OFFER, ANSWER, ICECANDIDATE, LOGIN, LEAVE } = actions_1.default; 9 | /** 10 | * @class 11 | * @classdesc The SignalingChannel class, which utilizes WebSockets in order to facillitate communication between clients connected to the WebSocket server. 12 | * @prop { WebsocketServer } websocketServer - a simple WebSocket server 13 | * @prop { Map } users - object containing key-value pairs consisting of users' names and their corresponding WebSocket in the following fashion { username1: socket1, username2: socket2, ... , usernameN: socketN } 14 | */ 15 | class SignalingChannel { 16 | /** 17 | * @constructor constructing a websocket server with an http/https object or port passed in upon instantiating SignalingChannel 18 | * @param {Server} server - pass in a server (http or https) or pass in a port (this port cannot be the same as the application port and it has to listen on the same port) 19 | */ 20 | constructor(server) { 21 | this.webSocketServer = typeof server === 'number' ? new ws_1.WebSocket.Server({ port: server }) : new ws_1.WebSocket.Server({ server: server }); 22 | this.users = new Map(); 23 | // this.rooms = new Map(); //focus on later when constructing 2+ video conferencing functionality, SFU topology 24 | } 25 | /** 26 | * @description Upon creation and connection to the WebSocket server, the WebSocket server will add these event listeners to their socket to perform key functionality 27 | * @function initializeConnection Signaling server will listen to client when client has connected. 28 | * When the message event is triggered, it will either send each user list to each user upon login or send data to the receiver 29 | * @return a socket that corresponds to the client connecting. 30 | */ 31 | initializeConnection() { 32 | // socket: WebSocket 33 | this.webSocketServer.on('connection', (socket) => { 34 | console.log('A user has connected to the websocket server.'); 35 | // when a client closes their browser or connection to the websocket server (onclose), their socket gets terminated and they are removed from the map of users 36 | // lastly a new user list is sent out to all clients connected to the websocket server. 37 | socket.on('close', () => { 38 | const userToDelete = this.getByValue(this.users, socket); 39 | this.users.delete(userToDelete); 40 | socket.terminate(); 41 | const userList = { ACTION_TYPE: LOGIN, payload: Array.from(this.users.keys()) }; 42 | this.webSocketServer.clients.forEach(client => client.send(JSON.stringify(userList))); 43 | }); 44 | // the meat of the websocket server, when messages are received from the client... 45 | // we will filter through what course of action to take based on data.ACTION_TYPE (see constants/actions.ts) 46 | socket.on('message', (message) => { 47 | // messages sent between the client and websocket server must be strings 48 | // importantly, messages sent to the websocket server are passed as Buffer objects encoded in utf-8 format 49 | const stringifiedMessage = message.toString('utf-8'); 50 | const data = JSON.parse(stringifiedMessage); 51 | switch (data.ACTION_TYPE) { 52 | case OFFER: 53 | this.transmit(data); 54 | break; 55 | case ANSWER: 56 | this.transmit(data); 57 | break; 58 | case ICECANDIDATE: 59 | this.transmit(data); 60 | break; 61 | case LOGIN: 62 | this.users.set(data.payload, socket); 63 | this.webSocketServer.clients.forEach(client => client.send(JSON.stringify({ 64 | ACTION_TYPE: LOGIN, 65 | payload: Array.from(this.users.keys()) 66 | }))); 67 | break; 68 | case LEAVE: 69 | this.transmit(data); 70 | break; 71 | default: 72 | console.error('error', data); 73 | break; 74 | } 75 | }); 76 | }); 77 | } 78 | /** 79 | * @description Broadcasting from sender to receiver. Accessing the receiver from the data object and if the user exists, the data is sent 80 | * @param {object} data 81 | */ 82 | transmit(data) { 83 | var _a; 84 | (_a = this.users.get(data.receiver)) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify(data)); 85 | } 86 | /** 87 | * @description Getting user from Map 88 | * @function getByValue identifies user and their specific websocket 89 | * @param {Map} map 90 | * @param {WebSocket} searchValue 91 | * @returns {string} user 92 | */ 93 | getByValue(map, searchValue) { 94 | let user = ''; 95 | for (const [key, value] of map.entries()) { 96 | if (value === searchValue) 97 | user = key; 98 | } 99 | return user; 100 | } 101 | } 102 | exports.default = SignalingChannel; 103 | -------------------------------------------------------------------------------- /dist/src/components/Socket.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * @file Socket.tsx is the component that initalizes and loads the client's socket connection with event listeners. 4 | */ 5 | declare type SocketType = { 6 | ws: WebSocket; 7 | getUsers: (parsedData: { 8 | payload: string[]; 9 | }) => void; 10 | handleReceiveCall: (data: { 11 | sender: string; 12 | payload: RTCSessionDescriptionInit; 13 | }) => void; 14 | handleAnswer: (parsedData: { 15 | payload: RTCSessionDescriptionInit; 16 | }) => void; 17 | handleNewIceCandidate: (data: { 18 | payload: RTCIceCandidateInit; 19 | }) => void; 20 | endCall: (parsedData: boolean) => void; 21 | }; 22 | /** 23 | * @desc Using the initial WebSocket connection, this functional component provides the event listeners for each client's socket connection to allow bilateral communication. 24 | * @param {string} props.ws - the ws or wss socket url that will initiate the connection with the WebSocket server 25 | * @param {function} props.getUser - When data (the list of connected users) is received from the WebSocketServer/backend, getUser 26 | * function is invoked and it updates the userList state so that the list of currently connected users 27 | * can be displayed on the frontend. 28 | * @param props.handleReceiveCall 29 | * @param props.handleAnswer 30 | * @param props.handleNewIceCandidate 31 | * @param props.endCall 32 | * @returns an empty element when rendered and populates the client's socket connection with event listeners that can handle the offer-answer model and SDP objects being exchanged between peers. 33 | */ 34 | declare const Socket: ({ ws, getUsers, handleReceiveCall, handleAnswer, handleNewIceCandidate, endCall }: SocketType) => JSX.Element; 35 | export default Socket; 36 | -------------------------------------------------------------------------------- /dist/src/components/Socket.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const react_1 = __importDefault(require("react")); 7 | const actions_1 = __importDefault(require("../constants/actions")); 8 | const { LOGIN, ICECANDIDATE, OFFER, ANSWER, LEAVE } = actions_1.default; 9 | /** 10 | * @desc Using the initial WebSocket connection, this functional component provides the event listeners for each client's socket connection to allow bilateral communication. 11 | * @param {string} props.ws - the ws or wss socket url that will initiate the connection with the WebSocket server 12 | * @param {function} props.getUser - When data (the list of connected users) is received from the WebSocketServer/backend, getUser 13 | * function is invoked and it updates the userList state so that the list of currently connected users 14 | * can be displayed on the frontend. 15 | * @param props.handleReceiveCall 16 | * @param props.handleAnswer 17 | * @param props.handleNewIceCandidate 18 | * @param props.endCall 19 | * @returns an empty element when rendered and populates the client's socket connection with event listeners that can handle the offer-answer model and SDP objects being exchanged between peers. 20 | */ 21 | const Socket = ({ ws, getUsers, handleReceiveCall, handleAnswer, handleNewIceCandidate, endCall }) => { 22 | // IIFE, this function gets invoked when a new socket component is created 23 | (function initalizeConnection() { 24 | ws.addEventListener('open', () => { 25 | console.log('Websocket connection has opened.'); 26 | }); 27 | ws.addEventListener('close', () => { 28 | console.log('Websocket connection closed.'); 29 | }); 30 | ws.addEventListener('error', (e) => { 31 | console.error('Socket Error:', e); 32 | }); 33 | ws.addEventListener('message', message => { 34 | const parsedData = JSON.parse(message.data); 35 | switch (parsedData.ACTION_TYPE) { 36 | case LOGIN: 37 | getUsers(parsedData); 38 | break; 39 | case OFFER: 40 | handleReceiveCall(parsedData); 41 | break; 42 | case ANSWER: 43 | handleAnswer(parsedData); 44 | break; 45 | case ICECANDIDATE: 46 | handleNewIceCandidate(parsedData); 47 | break; 48 | case LEAVE: 49 | endCall(true); 50 | break; 51 | default: 52 | console.error('error', parsedData); 53 | break; 54 | } 55 | }); 56 | })(); 57 | // should return an empty JSX fragment 58 | return (react_1.default.createElement(react_1.default.Fragment, null)); 59 | }; 60 | exports.default = Socket; 61 | -------------------------------------------------------------------------------- /dist/src/components/VideoCall.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * @func VideoCall 4 | * @param {String} props.URL - ws or wss link that establishes a connection between the WebSocket object and the server 5 | * @param {object} props.mediaOptions video embed attributes 6 | 7 | * @desc Wrapper component containing the logic necessary for peer connections using WebRTC APIs (RTCPeerConnect API + MediaSession API) and WebSockets. 8 | * 9 | * ws, localVideo, remoteVideo, peerRef, localStream, otherUser, senders are all mutable ref objects that are created using the useRef hook. The useRef hook allows you to persist values between renders and it is used to store a mutable value that does NOT cause a re-render when updated. 10 | * 11 | * The WebSocket connection (ws.current) is established using the useEffect hook and once the component mounts, the Socket component is rendered. The Socket component adds event listeners that handle the offer-answer model and the exchange of SDP objects between peers and the socket. 12 | * 13 | * The WebSocket message event will filter through various events to determine the payloads that will be sent to other serverside socket connection via WebSocket. 14 | * 15 | * @type {state} username - username state stores the name the client enters. All users (see getUsers) will be able to see an updated list of all other users whenever a new user logs in or leaves. 16 | * @type {state} users - users state is the list of connected users that is rendered on the frontend. 17 | * 18 | * @returns A component that renders two VideoComponents, 19 | */ 20 | declare const VideoCall: ({ URL, mediaOptions }: { 21 | URL: string; 22 | mediaOptions: { 23 | controls: boolean; 24 | style: { 25 | width: string; 26 | height: string; 27 | }; 28 | }; 29 | }) => JSX.Element; 30 | export default VideoCall; 31 | -------------------------------------------------------------------------------- /dist/src/components/VideoCall.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 3 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 4 | if (k2 === undefined) k2 = k; 5 | var desc = Object.getOwnPropertyDescriptor(m, k); 6 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 7 | desc = { enumerable: true, get: function() { return m[k]; } }; 8 | } 9 | Object.defineProperty(o, k2, desc); 10 | }) : (function(o, m, k, k2) { 11 | if (k2 === undefined) k2 = k; 12 | o[k2] = m[k]; 13 | })); 14 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 15 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 16 | }) : function(o, v) { 17 | o["default"] = v; 18 | }); 19 | var __importStar = (this && this.__importStar) || function (mod) { 20 | if (mod && mod.__esModule) return mod; 21 | var result = {}; 22 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 23 | __setModuleDefault(result, mod); 24 | return result; 25 | }; 26 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 27 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 28 | return new (P || (P = Promise))(function (resolve, reject) { 29 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 30 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 31 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 32 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 33 | }); 34 | }; 35 | var __importDefault = (this && this.__importDefault) || function (mod) { 36 | return (mod && mod.__esModule) ? mod : { "default": mod }; 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | const react_1 = __importStar(require("react")); 40 | const Socket_1 = __importDefault(require("./Socket")); 41 | const VideoComponent_1 = __importDefault(require("./VideoComponent")); 42 | const actions_1 = __importDefault(require("../constants/actions")); 43 | const mediaStreamConstraints_1 = __importDefault(require("../constants/mediaStreamConstraints")); 44 | const rtcConfiguration_1 = __importDefault(require("../constants/rtcConfiguration")); 45 | const { LOGIN, ICECANDIDATE, OFFER, ANSWER, LEAVE } = actions_1.default; 46 | /** 47 | * @func VideoCall 48 | * @param {String} props.URL - ws or wss link that establishes a connection between the WebSocket object and the server 49 | * @param {object} props.mediaOptions video embed attributes 50 | 51 | * @desc Wrapper component containing the logic necessary for peer connections using WebRTC APIs (RTCPeerConnect API + MediaSession API) and WebSockets. 52 | * 53 | * ws, localVideo, remoteVideo, peerRef, localStream, otherUser, senders are all mutable ref objects that are created using the useRef hook. The useRef hook allows you to persist values between renders and it is used to store a mutable value that does NOT cause a re-render when updated. 54 | * 55 | * The WebSocket connection (ws.current) is established using the useEffect hook and once the component mounts, the Socket component is rendered. The Socket component adds event listeners that handle the offer-answer model and the exchange of SDP objects between peers and the socket. 56 | * 57 | * The WebSocket message event will filter through various events to determine the payloads that will be sent to other serverside socket connection via WebSocket. 58 | * 59 | * @type {state} username - username state stores the name the client enters. All users (see getUsers) will be able to see an updated list of all other users whenever a new user logs in or leaves. 60 | * @type {state} users - users state is the list of connected users that is rendered on the frontend. 61 | * 62 | * @returns A component that renders two VideoComponents, 63 | */ 64 | const VideoCall = ({ URL, mediaOptions }) => { 65 | const [username, setUsername] = (0, react_1.useState)(''); 66 | const [users, setUsers] = (0, react_1.useState)(); 67 | /** 68 | * @type {mutable ref WebSocket object} ws is the mutable ref object that contains the WebSocket object in its .current property (ws.current). It cannot be null or undefined. 69 | * 70 | * @desc ws.current property contains the WebSocket object, which is created using the useEffect hook and it establishes the WebSocket connection to the server. The useEffect Hook creates the WebSocket object using the URL parameter when the component mounts. 71 | * 72 | * ws.current.send enqueues the specified messages that need to be transmitted to the server over the WebSocket connection and this WebSocket connection is connected to the server by using RTConnect's importable SignalingChannel module. 73 | */ 74 | const ws = (0, react_1.useRef)(null); 75 | /** 76 | * @type {mutable ref object} localVideo - video element of the local user. It will not be null or undefined. 77 | * @property {HTMLVideoElement} localVideo.current 78 | */ 79 | const localVideo = (0, react_1.useRef)(null); 80 | /** 81 | * @type {mutable ref object} remoteVideo - video stream of the remote user. It cannot be null or undefined. 82 | */ 83 | const remoteVideo = (0, react_1.useRef)(null); 84 | /** 85 | * @type {mutable ref object} peerRef - It cannot be null or undefined. 86 | */ 87 | const peerRef = (0, react_1.useRef)(null); 88 | /** 89 | * @type {mutable ref string} otherUser - 90 | */ 91 | const otherUser = (0, react_1.useRef)(); 92 | /** 93 | * @type {mutable ref object} localStream - It cannot be null or undefined. 94 | */ 95 | const localStream = (0, react_1.useRef)(null); 96 | /** 97 | * @type {mutable ref array} senders - 98 | */ 99 | const senders = (0, react_1.useRef)([]); 100 | /** 101 | * @type {string} userField - the username that is entered in the input field when the Submit Username button is clicked. 102 | */ 103 | let userField = ''; 104 | /** 105 | * @type {string} receiver - . 106 | */ 107 | let receiver = ''; 108 | (0, react_1.useEffect)(() => { 109 | ws.current = new WebSocket(URL); 110 | openUserMedia(); 111 | }, []); 112 | /** 113 | * A diagram of the WebRTC Connection logic 114 | * Peer A Stun Signaling Channel(WebSockets) Peer B Step 115 | * |------>| | | Who Am I? + RTCPeerConnection(configuration) this contains methods to connect to a remote Peer 116 | * |<------| | | Symmetric NAT (your ip that you can be connected to) 117 | * |-------------------------->|------------------>| Calling Peer B, Offer SDP is generated and sent over WebSocket 118 | * |-------------------------->|------------------>| ICE Candidates are also being trickled in, where and what IP:PORT can Peer B connect to Peer A 119 | * | |<------------------|-------------------| Who Am I? PeerB this time! 120 | * | |-------------------|------------------>| Peer B's NAT 121 | * |<--------------------------|-------------------| Accepting Peer A's call, sending Answer SDP 122 | * |<--------------------------|-------------------| Peer B's ICE Candidates are now being trickled in to peer A for connectivity. 123 | * |-------------------------->|------------------>| ICE Candidates from Peer A, these steps repeat and are only necessary if Peer B can't connect to the earlier candidates sent. 124 | * |<--------------------------|-------------------| ICE Candidate trickling from Peer B, could also take a second if there's a firewall to be circumvented. 125 | * | | | | Connected! Peer to Peer connection is made and now both users are streaming data to eachother! 126 | * 127 | * If Peer A starts a call their order of functions being invoked is... handleOffer --> callUser --> createPeer --> peerRef.current.negotiationNeeded event (handleNegotiationNeededEvent) --> ^send Offer SDP^ --> start ICE trickle, handleIceCandidateEvent --> ^receive Answer^ SDP --> handleIceCandidateMsg --> once connected, handleTrackEvent 128 | * If Peer B receives a call then we invoke... ^Receive Offer SDP^ --> handleReceiveCall --> createPeer --> ^send Answer SDP^ --> handleIceCandidateMsg --> handleIceCandidateEvent --> once connected, handleTrackEvent 129 | * 130 | * Note: Media is attached to the Peer Connection and sent along with the offers/answers to describe what media each client has. 131 | * 132 | * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack 133 | */ 134 | /** 135 | * @func handleUsername 136 | * 137 | * @desc Invoked when clients click the Submit Username button. A loginPayload object is initiated - it contains the LOGIN event and its payload contains the client's username. 138 | * 139 | * The loginPayload object is sent via the WebSocketServer (ws.current.send(loginPayload)) to the backend/SignalingChannel. 140 | * 141 | * Then, the username state is updated with the userField string (the username entered by the client when they clicked the Submit Username). setUsername(userField) 142 | */ 143 | const handleUsername = () => { 144 | const loginPayload = { 145 | ACTION_TYPE: LOGIN, 146 | payload: userField, 147 | }; 148 | ws.current.send(JSON.stringify(loginPayload)); 149 | setUsername(userField); 150 | }; 151 | /** 152 | * @func handleOffer 153 | * @desc When a username is entered that the client wants to "Call" and the client clicks the Call button, into the input field, this starts the Offer-Answer Model exchange 154 | */ 155 | const handleOffer = () => { 156 | const inputField = document.querySelector('#receiverName'); 157 | if (inputField) { 158 | receiver = inputField.value; 159 | inputField.value = ''; 160 | otherUser.current = receiver; 161 | callUser(receiver); 162 | } 163 | }; 164 | /** 165 | * @function getUser 166 | * @desc When data (the list of connected users) is received from the WebSocketServer, getUser is invoked and it creates div tags to render the names of each of the connected users on the front end. 167 | * @param {Object} parsedData - The object (containing the payload with the array of connected usernames) that is returned from backend/WebSocketServer. parsedData.payload contains the array with the strings of connected usernames 168 | * @returns Re-renders the page with the new list of connected users 169 | */ 170 | const getUsers = (parsedData) => { 171 | const userList = parsedData.payload.map((name, idx) => (react_1.default.createElement("div", { key: idx }, name))); 172 | setUsers(userList); 173 | }; 174 | /** 175 | * @async 176 | * @function openUserMedia is invoked in the useEffect Hook after WebSocket connection is established. 177 | * @desc If the localVideo.current property exists, openUserMedia invokes the MediaDevices interface getUserMedia() method to prompt the clients for audio and video permission. 178 | * 179 | * If clients grant permissions, getUserMedia() uses the video and audio constraints to assign the local MediaStream from the clients' cameras/microphones to the local