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 | [](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