├── .gitignore
├── README.md
├── gumPlayground
├── .gitignore
├── changeButtons.js
├── changeVideoSize.js
├── expressServer.js
├── index.html
├── inputOutput.js
├── package.json
├── screenRecorder.js
├── scripts.js
├── shareScreen.js
└── styles.css
├── signalingPeerConnection
├── .gitignore
├── index.html
├── package-lock.json
├── package.json
├── scripts.js
├── server.js
├── socketListeners.js
├── styles.css
└── taskList.txt
├── starterFiles
├── gumPlayground
│ ├── changeButtons.js
│ ├── index.html
│ └── styles.css
├── signalingPeerConnection
│ ├── index.html
│ ├── stunServers.js
│ └── styles.css
└── teleLegalSite
│ ├── .htaccess-file
│ ├── ActionButtons.js
│ ├── CallInfo.js
│ ├── HangUpButtons.js
│ ├── ProDashboard.css
│ ├── ProDashboard.js
│ ├── apptSeedData.js
│ ├── callStatusReducer.js
│ ├── reverse-proxy-file
│ ├── stunServers.js
│ ├── vhost-file
│ └── videoComponents.css
└── teleLegalSite
├── .gitignore
├── teleLegal-back-end
├── expressRoutes.js
├── index.js
├── package.json
├── server.js
└── socketServer.js
└── telelegal-front-end
├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.css
├── App.js
├── index.js
├── redux-elements
├── actions
│ ├── addStream.js
│ └── updateCallStatus.js
└── reducers
│ ├── callStatusReducer.js
│ ├── rootReducer.js
│ └── streamsReducer.js
├── siteComponents
├── ProDashboard.css
└── ProDashboard.js
├── videoComponents
├── ActionButtonCaretDropDown.js
├── ActionButtons.js
├── AudioButton
│ ├── AudioButton.js
│ └── startAudioStream.js
├── CallInfo.js
├── ChatWindow.js
├── HangupButton.js
├── MainVideoPage.js
├── ProMainVideoPage.js
├── VideoButton
│ ├── VideoButton.js
│ ├── getDevices.js
│ └── startLocalVideoStream.js
└── VideoComponents.css
└── webRTCutilities
├── clientSocketListeners.js
├── createPeerConnection.js
├── proSocketListeners.js
├── socketConnection.js
└── stunServers.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # webrtcCourse
2 | This is the codebase for [my Udemy webRTC course](https://www.udemy.com/course/mastering-webrtc-part-2-real-time-video-and-screen-share/?couponCode=C8CE681D6BEF9443510E). If the coupon doesn't work, you can message me on Udemy and I'll send you one.
3 |
4 | The tags below coorespond to the git tag for the beginning of the given lecture. For instance, if you are about to start lecture 21 and you need to reset or compare your code to mine, you do:
5 | $ git checkout Add-Ice
6 |
7 | This will put your code base at the same point mine is when I start the video. You must have used git clone for this work (downloading the repo as a directory doesn't include tags), and there may be some mistakes, but I hope it helps a ton!
8 |
9 | * lec-10 - Set-Local-Description
10 | * lec-11 - Add-Local-Description
11 | * lec-12 - Add-Socketio
12 | * lec-14 - Emit-Offer
13 | * lec-15 - Emit-Offer
14 | * lec-16 - Load-Existing-Offers
15 | * lec-17 - Create-Answer
16 | * lec-18 - Signal-Error-Handling
17 | * lec-19 - Emitting-Answer
18 | * lec-20 - On-Answer-Response
19 | * lec-21 - Add-Ice
20 | * lec-22 - Apply-Ice
21 | * lec-23 - Add-Tracks
22 | * lec-28 - Create-JWT
23 | * lec-29 - Add-React-Router
24 | * lec-30 - Join-Video
25 | * lec-31 - Starting-Components
26 | * lec-32 - Wire-Up-Redux
27 | * lec-33 - Add-Action-Buttons
28 | * lec-34 - GUM-Store-Stream
29 | * lec-35 - Create-Peer-Connection
30 | * lec-36 - Thinking-Functions
31 | * lec-37 - Abstract-Video-Audio-Buttons
32 | * lec-38 - Add-Video-Feed
33 | * lec-40 - Enable-Disable-Video
34 | * lec-41 - Display-Local-Video-Inputs
35 | * lec-42 - Set-New-Video-Device
36 | * lec-43 - Replace-Tracks
37 | * lec-44 - Abstract-DropDown
38 | * lec-45 - Setup-Audio-Button
39 | * lec-46 - Switch-Audio-Devices
40 | * lec-47 - Start-Mute-Audio
41 | * lec-48 - Organize-Offers
42 | * lec-49 - Create-Offer-Tele
43 | * lec-50 - Add-Dashboard
44 | * lec-52 - Auth-Pro
45 | * lec-53 - Socket-Refactor
46 | * lec-54 - Reorg-Appt-Data
47 | * lec-55 - Pull-Appt-Data
48 | * lec-56 - Listen-Offers-Tele
49 | * lec-57 - Join-Video-Route
50 | * lec-58 - Call-Info-Pro
51 | * lec-59 - Create-Answer-Tele
52 | * lec-61 - Emit-Answer-Server
53 | * lec-62 - Listen-For-Answer
54 | * lec-63 - Emit-Ice-Candidates-Server
55 | * lec-64 - Ice-To-Clients
56 | * lec-65 - Add-Ice-PC
57 | * lec-66 - Add-Tracks-Victory
58 | * lec-67 - Tele-Bug-Fix-1
59 | * lec-68 - HangUp-Button
60 | * lec-69 - Replace-Tracks-Tele
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/gumPlayground/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
--------------------------------------------------------------------------------
/gumPlayground/changeButtons.js:
--------------------------------------------------------------------------------
1 | //green = btn-success
2 | //blue = btn-primary
3 | //grey = btn-secondary
4 | //red = btn-danger
5 |
6 | const buttonsById = [
7 | 'share', 'show-video', 'stop-video', 'change-size', 'start-record',
8 | 'stop-record','play-record','share-screen'
9 | ]
10 |
11 | //buttonEls will be an array of dom elements in order of buttonsById
12 | const buttonEls = buttonsById.map(buttonId=>document.getElementById(buttonId));
13 |
14 | const changeButtons = (colorsArray)=>{
15 | colorsArray.forEach((color,i)=>{
16 | buttonEls[i].classList.remove('btn-success');
17 | buttonEls[i].classList.remove('btn-primary');
18 | buttonEls[i].classList.remove('btn-secondary');
19 | buttonEls[i].classList.remove('btn-danger');
20 | if(color === "green"){
21 | buttonEls[i].classList.add('btn-success');
22 | }else if(color === "blue"){
23 | buttonEls[i].classList.add('btn-primary');
24 | }else if(color === "grey"){
25 | buttonEls[i].classList.add('btn-secondary');
26 | }else if(color === "red"){
27 | buttonEls[i].classList.add('btn-danger');
28 | }
29 | })
30 | }
--------------------------------------------------------------------------------
/gumPlayground/changeVideoSize.js:
--------------------------------------------------------------------------------
1 |
2 | const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
3 | console.log(supportedConstraints);
4 |
5 | const changeVideoSize = ()=>{
6 | stream.getVideoTracks().forEach(track=>{
7 | //track is a video track.
8 | //we can get it's capabilities from .getCapabilities()
9 | //or we can apply new constraints with applyConstraints();'
10 | const capabilities = track.getCapabilities()
11 | const height = document.querySelector('#vid-height').value
12 | const width = document.querySelector('#vid-width').value
13 | const vConstraints = {
14 | height: {exact: height < capabilities.height.max ? height : capabilities.height.max},
15 | width: {exact: width < capabilities.width.max ? width : capabilities.width.max},
16 | // frameRate: 5,
17 | // aspectRatio: 10,
18 | }
19 | track.applyConstraints(vConstraints)
20 | })
21 |
22 | // stream.getTracks().forEach(track=>{
23 | // const capabilities = track.getCapabilities()
24 | // console.log(capabilities);
25 | // })
26 | }
--------------------------------------------------------------------------------
/gumPlayground/expressServer.js:
--------------------------------------------------------------------------------
1 | //we need this to run in a localhost context instead of file
2 | //so that we can run enumerate devices (it must be run in a secure context)
3 | //and localhost counts
4 | const path = require('path');
5 | const express = require('express');
6 | const app = express();
7 | app.use(express.static(path.join(__dirname)))
8 | app.listen(3000)
--------------------------------------------------------------------------------
/gumPlayground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
44 |
45 |
46 |
My feed
47 |
48 |
49 |
50 |
Their feed
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/gumPlayground/inputOutput.js:
--------------------------------------------------------------------------------
1 | const audioInputEl = document.querySelector('#audio-input')
2 | const audioOutputEl = document.querySelector('#audio-output')
3 | const videoInputEl = document.querySelector('#video-input')
4 |
5 | const getDevices = async()=>{
6 | try{
7 | const devices = await navigator.mediaDevices.enumerateDevices();
8 | console.log(devices)
9 | devices.forEach(d=>{
10 | const option = document.createElement('option') //create the option tag
11 | option.value = d.deviceId
12 | option.text = d.label
13 | //add the option tag we just created to the right select
14 | if(d.kind === "audioinput"){
15 | audioInputEl.appendChild(option)
16 | }else if(d.kind === "audiooutput"){
17 | audioOutputEl.appendChild(option)
18 | }else if(d.kind === "videoinput"){
19 | videoInputEl.appendChild(option)
20 | }
21 | })
22 | }catch(err){
23 | console.log(err);
24 | }
25 | }
26 |
27 | const changeAudioInput = async(e)=>{
28 | //changed audio input!!!
29 | const deviceId = e.target.value;
30 | const newConstraints = {
31 | audio: {deviceId: {exact: deviceId}},
32 | video: true,
33 | }
34 | try{
35 | stream = await navigator.mediaDevices.getUserMedia(newConstraints);
36 | console.log(stream);
37 | const tracks = stream.getAudioTracks();
38 | console.log(tracks);
39 | }catch(err){
40 | console.log(err)
41 | }
42 | }
43 |
44 | const changeAudioOutput = async(e)=>{
45 | await videoEl.setSinkId(e.target.value)
46 | console.log("Changed audio device!")
47 | }
48 |
49 | const changeVideo = async(e)=>{
50 | //changed video input!!!
51 | const deviceId = e.target.value;
52 | const newConstraints = {
53 | audio: true,
54 | video: {deviceId: {exact: deviceId}},
55 | }
56 | try{
57 | stream = await navigator.mediaDevices.getUserMedia(newConstraints);
58 | console.log(stream);
59 | const tracks = stream.getVideoTracks();
60 | console.log(tracks);
61 | }catch(err){
62 | console.log(err)
63 | }
64 | }
65 |
66 | getDevices();
67 |
--------------------------------------------------------------------------------
/gumPlayground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gumplayground",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "changeButtons.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "express": "^4.18.2"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/gumPlayground/screenRecorder.js:
--------------------------------------------------------------------------------
1 | let mediaRecorder;
2 | let recordedBlobs;
3 |
4 | const startRecording = ()=>{
5 | if(!stream){ //you could use mediaStream!
6 | alert("No current feed");
7 | return
8 | }
9 | console.log("Start recording")
10 | recordedBlobs = []; // an array to hold the blobs for playback
11 | //you could use mediaStream to record!
12 | mediaRecorder = new MediaRecorder(stream) //make a mediaRecorder from the constructor
13 | mediaRecorder.ondataavailable = e=>{
14 | //ondataavailable will run when the stream ends, or stopped, or we specifically ask for it
15 | console.log("Data is available for the media recorder!")
16 | recordedBlobs.push(e.data)
17 | }
18 | mediaRecorder.start();
19 | changeButtons([
20 | 'green','green','blue','blue','green','blue','grey','blue'
21 | ])
22 | }
23 |
24 |
25 | const stopRecording = ()=>{
26 | if(!mediaRecorder){
27 | alert("Please record before stopping!")
28 | return
29 | }
30 | console.log("stop recording")
31 | mediaRecorder.stop()
32 | changeButtons([
33 | 'green','green','blue','blue','green','green','blue','blue'
34 | ])
35 |
36 | }
37 |
38 | const playRecording = ()=>{
39 | console.log("play recording")
40 | if(!recordedBlobs){
41 | alert("No Recording saved")
42 | return
43 | }
44 | const superBuffer = new Blob(recordedBlobs) // superBuffer is a super buffer of our array of blobs
45 | const recordedVideoEl = document.querySelector('#other-video');
46 | recordedVideoEl.src = window.URL.createObjectURL(superBuffer);
47 | recordedVideoEl.controls = true;
48 | recordedVideoEl.play();
49 | changeButtons([
50 | 'green','green','blue','blue','green','green','green','blue'
51 | ])
52 | }
53 |
--------------------------------------------------------------------------------
/gumPlayground/scripts.js:
--------------------------------------------------------------------------------
1 | const videoEl = document.querySelector('#my-video');
2 | let stream = null // Init stream var so we can use anywhere
3 | let mediaStream = null //Init mediaStream var for screenShare
4 | const constraints = {
5 | audio: true, //use your headphones, or be prepared for feedback!
6 | video: true,
7 | }
8 |
9 |
10 | const getMicAndCamera = async(e)=>{
11 | try{
12 | stream = await navigator.mediaDevices.getUserMedia(constraints);
13 | console.log(stream)
14 | changeButtons([
15 | 'green','blue','blue','grey','grey','grey','grey','grey'
16 | ])
17 | }catch(err){
18 | //user denied access to constraints
19 | console.log("user denied access to constraints")
20 | console.log(err)
21 | }
22 | };
23 |
24 | const showMyFeed = e=>{
25 | console.log("showMyFeed is working")
26 | if(!stream){
27 | alert("Stream still loading...")
28 | return;
29 | }
30 | videoEl.srcObject = stream; // this will set our MediaStream (stream) to our
31 | const tracks = stream.getTracks();
32 | console.log(tracks);
33 | changeButtons([
34 | 'green','green','blue','blue','blue','grey','grey','blue'
35 | ])
36 |
37 | }
38 | const stopMyFeed = e=>{
39 | if(!stream){
40 | alert("Stream still loading...")
41 | return;
42 | }
43 | const tracks = stream.getTracks();
44 | tracks.forEach(track=>{
45 | // console.log(track)
46 | track.stop(); //disassociates the track with the source
47 | })
48 | changeButtons([
49 | 'blue','grey','grey','grey','grey','grey','grey','grey'
50 | ])
51 | }
52 |
53 | document.querySelector('#share').addEventListener('click',e=>getMicAndCamera(e))
54 | document.querySelector('#show-video').addEventListener('click',e=>showMyFeed(e))
55 | document.querySelector('#stop-video').addEventListener('click',e=>stopMyFeed(e))
56 | document.querySelector('#change-size').addEventListener('click',e=>changeVideoSize(e))
57 | document.querySelector('#start-record').addEventListener('click',e=>startRecording(e))
58 | document.querySelector('#stop-record').addEventListener('click',e=>stopRecording(e))
59 | document.querySelector('#play-record').addEventListener('click',e=>playRecording(e))
60 | document.querySelector('#share-screen').addEventListener('click',e=>shareScreen(e))
61 |
62 | document.querySelector('#audio-input').addEventListener('change',e=>changeAudioInput(e))
63 | document.querySelector('#audio-output').addEventListener('change',e=>changeAudioOutput(e))
64 | document.querySelector('#video-input').addEventListener('change',e=>changeVideo(e))
--------------------------------------------------------------------------------
/gumPlayground/shareScreen.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const shareScreen = async()=>{
4 |
5 | const options = {
6 | video: true,
7 | audio: false,
8 | surfaceSwitching: 'include', //include/exclude NOT true/false
9 | }
10 | try{
11 | mediaStream = await navigator.mediaDevices.getDisplayMedia(options)
12 | }catch(err){
13 | console.log(err);
14 | }
15 |
16 |
17 | //we don't handle all button paths. To do so, you'd need
18 | //to check the DOM or use a UI framework.
19 | changeButtons([
20 | 'green','green','blue','blue','green','green','green','green'
21 | ])
22 | }
--------------------------------------------------------------------------------
/gumPlayground/styles.css:
--------------------------------------------------------------------------------
1 |
2 | .container{
3 | margin: 20px auto;
4 | }
5 |
6 | .video{
7 | background-color:#333;
8 | padding:20px;
9 | border-radius:10px;
10 | color:#fff;
11 | font-size:40px;
12 | height: 40vh;
13 | }
14 |
15 | #vid-width,#vid-height{
16 | width: 50px;
17 | }
--------------------------------------------------------------------------------
/signalingPeerConnection/.gitignore:
--------------------------------------------------------------------------------
1 | *.crt
2 | *.key
--------------------------------------------------------------------------------
/signalingPeerConnection/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Page Title
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Waiting for answer...
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/signalingPeerConnection/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "signalingpeerconnection",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "signalingpeerconnection",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "express": "^4.18.2",
13 | "mkcert": "^1.5.1",
14 | "socket.io": "^4.6.2"
15 | }
16 | },
17 | "node_modules/@socket.io/component-emitter": {
18 | "version": "3.1.0",
19 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
20 | "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
21 | },
22 | "node_modules/@types/cookie": {
23 | "version": "0.4.1",
24 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
25 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
26 | },
27 | "node_modules/@types/cors": {
28 | "version": "2.8.13",
29 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
30 | "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
31 | "dependencies": {
32 | "@types/node": "*"
33 | }
34 | },
35 | "node_modules/@types/node": {
36 | "version": "20.3.1",
37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
38 | "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg=="
39 | },
40 | "node_modules/accepts": {
41 | "version": "1.3.8",
42 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
43 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
44 | "dependencies": {
45 | "mime-types": "~2.1.34",
46 | "negotiator": "0.6.3"
47 | },
48 | "engines": {
49 | "node": ">= 0.6"
50 | }
51 | },
52 | "node_modules/array-flatten": {
53 | "version": "1.1.1",
54 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
55 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
56 | },
57 | "node_modules/base64id": {
58 | "version": "2.0.0",
59 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
60 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
61 | "engines": {
62 | "node": "^4.5.0 || >= 5.9"
63 | }
64 | },
65 | "node_modules/body-parser": {
66 | "version": "1.20.1",
67 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
68 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
69 | "dependencies": {
70 | "bytes": "3.1.2",
71 | "content-type": "~1.0.4",
72 | "debug": "2.6.9",
73 | "depd": "2.0.0",
74 | "destroy": "1.2.0",
75 | "http-errors": "2.0.0",
76 | "iconv-lite": "0.4.24",
77 | "on-finished": "2.4.1",
78 | "qs": "6.11.0",
79 | "raw-body": "2.5.1",
80 | "type-is": "~1.6.18",
81 | "unpipe": "1.0.0"
82 | },
83 | "engines": {
84 | "node": ">= 0.8",
85 | "npm": "1.2.8000 || >= 1.4.16"
86 | }
87 | },
88 | "node_modules/bytes": {
89 | "version": "3.1.2",
90 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
91 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
92 | "engines": {
93 | "node": ">= 0.8"
94 | }
95 | },
96 | "node_modules/call-bind": {
97 | "version": "1.0.2",
98 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
99 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
100 | "dependencies": {
101 | "function-bind": "^1.1.1",
102 | "get-intrinsic": "^1.0.2"
103 | },
104 | "funding": {
105 | "url": "https://github.com/sponsors/ljharb"
106 | }
107 | },
108 | "node_modules/commander": {
109 | "version": "9.5.0",
110 | "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
111 | "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
112 | "engines": {
113 | "node": "^12.20.0 || >=14"
114 | }
115 | },
116 | "node_modules/content-disposition": {
117 | "version": "0.5.4",
118 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
119 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
120 | "dependencies": {
121 | "safe-buffer": "5.2.1"
122 | },
123 | "engines": {
124 | "node": ">= 0.6"
125 | }
126 | },
127 | "node_modules/content-type": {
128 | "version": "1.0.5",
129 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
130 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
131 | "engines": {
132 | "node": ">= 0.6"
133 | }
134 | },
135 | "node_modules/cookie": {
136 | "version": "0.5.0",
137 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
138 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
139 | "engines": {
140 | "node": ">= 0.6"
141 | }
142 | },
143 | "node_modules/cookie-signature": {
144 | "version": "1.0.6",
145 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
146 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
147 | },
148 | "node_modules/cors": {
149 | "version": "2.8.5",
150 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
151 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
152 | "dependencies": {
153 | "object-assign": "^4",
154 | "vary": "^1"
155 | },
156 | "engines": {
157 | "node": ">= 0.10"
158 | }
159 | },
160 | "node_modules/debug": {
161 | "version": "2.6.9",
162 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
163 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
164 | "dependencies": {
165 | "ms": "2.0.0"
166 | }
167 | },
168 | "node_modules/depd": {
169 | "version": "2.0.0",
170 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
171 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
172 | "engines": {
173 | "node": ">= 0.8"
174 | }
175 | },
176 | "node_modules/destroy": {
177 | "version": "1.2.0",
178 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
179 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
180 | "engines": {
181 | "node": ">= 0.8",
182 | "npm": "1.2.8000 || >= 1.4.16"
183 | }
184 | },
185 | "node_modules/ee-first": {
186 | "version": "1.1.1",
187 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
188 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
189 | },
190 | "node_modules/encodeurl": {
191 | "version": "1.0.2",
192 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
193 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
194 | "engines": {
195 | "node": ">= 0.8"
196 | }
197 | },
198 | "node_modules/engine.io": {
199 | "version": "6.4.2",
200 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz",
201 | "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==",
202 | "dependencies": {
203 | "@types/cookie": "^0.4.1",
204 | "@types/cors": "^2.8.12",
205 | "@types/node": ">=10.0.0",
206 | "accepts": "~1.3.4",
207 | "base64id": "2.0.0",
208 | "cookie": "~0.4.1",
209 | "cors": "~2.8.5",
210 | "debug": "~4.3.1",
211 | "engine.io-parser": "~5.0.3",
212 | "ws": "~8.11.0"
213 | },
214 | "engines": {
215 | "node": ">=10.0.0"
216 | }
217 | },
218 | "node_modules/engine.io-parser": {
219 | "version": "5.0.7",
220 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.7.tgz",
221 | "integrity": "sha512-P+jDFbvK6lE3n1OL+q9KuzdOFWkkZ/cMV9gol/SbVfpyqfvrfrFTOFJ6fQm2VC3PZHlU3QPhVwmbsCnauHF2MQ==",
222 | "engines": {
223 | "node": ">=10.0.0"
224 | }
225 | },
226 | "node_modules/engine.io/node_modules/cookie": {
227 | "version": "0.4.2",
228 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
229 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
230 | "engines": {
231 | "node": ">= 0.6"
232 | }
233 | },
234 | "node_modules/engine.io/node_modules/debug": {
235 | "version": "4.3.4",
236 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
237 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
238 | "dependencies": {
239 | "ms": "2.1.2"
240 | },
241 | "engines": {
242 | "node": ">=6.0"
243 | },
244 | "peerDependenciesMeta": {
245 | "supports-color": {
246 | "optional": true
247 | }
248 | }
249 | },
250 | "node_modules/engine.io/node_modules/ms": {
251 | "version": "2.1.2",
252 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
253 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
254 | },
255 | "node_modules/escape-html": {
256 | "version": "1.0.3",
257 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
258 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
259 | },
260 | "node_modules/etag": {
261 | "version": "1.8.1",
262 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
263 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
264 | "engines": {
265 | "node": ">= 0.6"
266 | }
267 | },
268 | "node_modules/express": {
269 | "version": "4.18.2",
270 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
271 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
272 | "dependencies": {
273 | "accepts": "~1.3.8",
274 | "array-flatten": "1.1.1",
275 | "body-parser": "1.20.1",
276 | "content-disposition": "0.5.4",
277 | "content-type": "~1.0.4",
278 | "cookie": "0.5.0",
279 | "cookie-signature": "1.0.6",
280 | "debug": "2.6.9",
281 | "depd": "2.0.0",
282 | "encodeurl": "~1.0.2",
283 | "escape-html": "~1.0.3",
284 | "etag": "~1.8.1",
285 | "finalhandler": "1.2.0",
286 | "fresh": "0.5.2",
287 | "http-errors": "2.0.0",
288 | "merge-descriptors": "1.0.1",
289 | "methods": "~1.1.2",
290 | "on-finished": "2.4.1",
291 | "parseurl": "~1.3.3",
292 | "path-to-regexp": "0.1.7",
293 | "proxy-addr": "~2.0.7",
294 | "qs": "6.11.0",
295 | "range-parser": "~1.2.1",
296 | "safe-buffer": "5.2.1",
297 | "send": "0.18.0",
298 | "serve-static": "1.15.0",
299 | "setprototypeof": "1.2.0",
300 | "statuses": "2.0.1",
301 | "type-is": "~1.6.18",
302 | "utils-merge": "1.0.1",
303 | "vary": "~1.1.2"
304 | },
305 | "engines": {
306 | "node": ">= 0.10.0"
307 | }
308 | },
309 | "node_modules/finalhandler": {
310 | "version": "1.2.0",
311 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
312 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
313 | "dependencies": {
314 | "debug": "2.6.9",
315 | "encodeurl": "~1.0.2",
316 | "escape-html": "~1.0.3",
317 | "on-finished": "2.4.1",
318 | "parseurl": "~1.3.3",
319 | "statuses": "2.0.1",
320 | "unpipe": "~1.0.0"
321 | },
322 | "engines": {
323 | "node": ">= 0.8"
324 | }
325 | },
326 | "node_modules/forwarded": {
327 | "version": "0.2.0",
328 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
329 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
330 | "engines": {
331 | "node": ">= 0.6"
332 | }
333 | },
334 | "node_modules/fresh": {
335 | "version": "0.5.2",
336 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
337 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
338 | "engines": {
339 | "node": ">= 0.6"
340 | }
341 | },
342 | "node_modules/function-bind": {
343 | "version": "1.1.1",
344 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
345 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
346 | },
347 | "node_modules/get-intrinsic": {
348 | "version": "1.2.1",
349 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
350 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
351 | "dependencies": {
352 | "function-bind": "^1.1.1",
353 | "has": "^1.0.3",
354 | "has-proto": "^1.0.1",
355 | "has-symbols": "^1.0.3"
356 | },
357 | "funding": {
358 | "url": "https://github.com/sponsors/ljharb"
359 | }
360 | },
361 | "node_modules/has": {
362 | "version": "1.0.3",
363 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
364 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
365 | "dependencies": {
366 | "function-bind": "^1.1.1"
367 | },
368 | "engines": {
369 | "node": ">= 0.4.0"
370 | }
371 | },
372 | "node_modules/has-proto": {
373 | "version": "1.0.1",
374 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
375 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
376 | "engines": {
377 | "node": ">= 0.4"
378 | },
379 | "funding": {
380 | "url": "https://github.com/sponsors/ljharb"
381 | }
382 | },
383 | "node_modules/has-symbols": {
384 | "version": "1.0.3",
385 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
386 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
387 | "engines": {
388 | "node": ">= 0.4"
389 | },
390 | "funding": {
391 | "url": "https://github.com/sponsors/ljharb"
392 | }
393 | },
394 | "node_modules/http-errors": {
395 | "version": "2.0.0",
396 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
397 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
398 | "dependencies": {
399 | "depd": "2.0.0",
400 | "inherits": "2.0.4",
401 | "setprototypeof": "1.2.0",
402 | "statuses": "2.0.1",
403 | "toidentifier": "1.0.1"
404 | },
405 | "engines": {
406 | "node": ">= 0.8"
407 | }
408 | },
409 | "node_modules/iconv-lite": {
410 | "version": "0.4.24",
411 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
412 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
413 | "dependencies": {
414 | "safer-buffer": ">= 2.1.2 < 3"
415 | },
416 | "engines": {
417 | "node": ">=0.10.0"
418 | }
419 | },
420 | "node_modules/inherits": {
421 | "version": "2.0.4",
422 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
423 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
424 | },
425 | "node_modules/ip-regex": {
426 | "version": "4.3.0",
427 | "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
428 | "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
429 | "engines": {
430 | "node": ">=8"
431 | }
432 | },
433 | "node_modules/ipaddr.js": {
434 | "version": "1.9.1",
435 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
436 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
437 | "engines": {
438 | "node": ">= 0.10"
439 | }
440 | },
441 | "node_modules/media-typer": {
442 | "version": "0.3.0",
443 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
444 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
445 | "engines": {
446 | "node": ">= 0.6"
447 | }
448 | },
449 | "node_modules/merge-descriptors": {
450 | "version": "1.0.1",
451 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
452 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
453 | },
454 | "node_modules/methods": {
455 | "version": "1.1.2",
456 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
457 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
458 | "engines": {
459 | "node": ">= 0.6"
460 | }
461 | },
462 | "node_modules/mime": {
463 | "version": "1.6.0",
464 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
465 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
466 | "bin": {
467 | "mime": "cli.js"
468 | },
469 | "engines": {
470 | "node": ">=4"
471 | }
472 | },
473 | "node_modules/mime-db": {
474 | "version": "1.52.0",
475 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
476 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
477 | "engines": {
478 | "node": ">= 0.6"
479 | }
480 | },
481 | "node_modules/mime-types": {
482 | "version": "2.1.35",
483 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
484 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
485 | "dependencies": {
486 | "mime-db": "1.52.0"
487 | },
488 | "engines": {
489 | "node": ">= 0.6"
490 | }
491 | },
492 | "node_modules/mkcert": {
493 | "version": "1.5.1",
494 | "resolved": "https://registry.npmjs.org/mkcert/-/mkcert-1.5.1.tgz",
495 | "integrity": "sha512-MHOmridCutIIPMKvaQwueIAo+lsHPyO0WotbGIOq5V4mPywrjtOPlzdS/kgk/2vjRELWv4OrDSKo4KA8H7VARw==",
496 | "dependencies": {
497 | "commander": "^9.4.0",
498 | "ip-regex": "^4.3.0",
499 | "node-forge": "^1.3.1"
500 | },
501 | "bin": {
502 | "mkcert": "src/cli.js"
503 | },
504 | "engines": {
505 | "node": ">=12"
506 | }
507 | },
508 | "node_modules/ms": {
509 | "version": "2.0.0",
510 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
511 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
512 | },
513 | "node_modules/negotiator": {
514 | "version": "0.6.3",
515 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
516 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
517 | "engines": {
518 | "node": ">= 0.6"
519 | }
520 | },
521 | "node_modules/node-forge": {
522 | "version": "1.3.1",
523 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
524 | "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
525 | "engines": {
526 | "node": ">= 6.13.0"
527 | }
528 | },
529 | "node_modules/object-assign": {
530 | "version": "4.1.1",
531 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
532 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
533 | "engines": {
534 | "node": ">=0.10.0"
535 | }
536 | },
537 | "node_modules/object-inspect": {
538 | "version": "1.12.3",
539 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
540 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
541 | "funding": {
542 | "url": "https://github.com/sponsors/ljharb"
543 | }
544 | },
545 | "node_modules/on-finished": {
546 | "version": "2.4.1",
547 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
548 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
549 | "dependencies": {
550 | "ee-first": "1.1.1"
551 | },
552 | "engines": {
553 | "node": ">= 0.8"
554 | }
555 | },
556 | "node_modules/parseurl": {
557 | "version": "1.3.3",
558 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
559 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
560 | "engines": {
561 | "node": ">= 0.8"
562 | }
563 | },
564 | "node_modules/path-to-regexp": {
565 | "version": "0.1.7",
566 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
567 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
568 | },
569 | "node_modules/proxy-addr": {
570 | "version": "2.0.7",
571 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
572 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
573 | "dependencies": {
574 | "forwarded": "0.2.0",
575 | "ipaddr.js": "1.9.1"
576 | },
577 | "engines": {
578 | "node": ">= 0.10"
579 | }
580 | },
581 | "node_modules/qs": {
582 | "version": "6.11.0",
583 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
584 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
585 | "dependencies": {
586 | "side-channel": "^1.0.4"
587 | },
588 | "engines": {
589 | "node": ">=0.6"
590 | },
591 | "funding": {
592 | "url": "https://github.com/sponsors/ljharb"
593 | }
594 | },
595 | "node_modules/range-parser": {
596 | "version": "1.2.1",
597 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
598 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
599 | "engines": {
600 | "node": ">= 0.6"
601 | }
602 | },
603 | "node_modules/raw-body": {
604 | "version": "2.5.1",
605 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
606 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
607 | "dependencies": {
608 | "bytes": "3.1.2",
609 | "http-errors": "2.0.0",
610 | "iconv-lite": "0.4.24",
611 | "unpipe": "1.0.0"
612 | },
613 | "engines": {
614 | "node": ">= 0.8"
615 | }
616 | },
617 | "node_modules/safe-buffer": {
618 | "version": "5.2.1",
619 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
620 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
621 | "funding": [
622 | {
623 | "type": "github",
624 | "url": "https://github.com/sponsors/feross"
625 | },
626 | {
627 | "type": "patreon",
628 | "url": "https://www.patreon.com/feross"
629 | },
630 | {
631 | "type": "consulting",
632 | "url": "https://feross.org/support"
633 | }
634 | ]
635 | },
636 | "node_modules/safer-buffer": {
637 | "version": "2.1.2",
638 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
639 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
640 | },
641 | "node_modules/send": {
642 | "version": "0.18.0",
643 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
644 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
645 | "dependencies": {
646 | "debug": "2.6.9",
647 | "depd": "2.0.0",
648 | "destroy": "1.2.0",
649 | "encodeurl": "~1.0.2",
650 | "escape-html": "~1.0.3",
651 | "etag": "~1.8.1",
652 | "fresh": "0.5.2",
653 | "http-errors": "2.0.0",
654 | "mime": "1.6.0",
655 | "ms": "2.1.3",
656 | "on-finished": "2.4.1",
657 | "range-parser": "~1.2.1",
658 | "statuses": "2.0.1"
659 | },
660 | "engines": {
661 | "node": ">= 0.8.0"
662 | }
663 | },
664 | "node_modules/send/node_modules/ms": {
665 | "version": "2.1.3",
666 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
667 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
668 | },
669 | "node_modules/serve-static": {
670 | "version": "1.15.0",
671 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
672 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
673 | "dependencies": {
674 | "encodeurl": "~1.0.2",
675 | "escape-html": "~1.0.3",
676 | "parseurl": "~1.3.3",
677 | "send": "0.18.0"
678 | },
679 | "engines": {
680 | "node": ">= 0.8.0"
681 | }
682 | },
683 | "node_modules/setprototypeof": {
684 | "version": "1.2.0",
685 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
686 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
687 | },
688 | "node_modules/side-channel": {
689 | "version": "1.0.4",
690 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
691 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
692 | "dependencies": {
693 | "call-bind": "^1.0.0",
694 | "get-intrinsic": "^1.0.2",
695 | "object-inspect": "^1.9.0"
696 | },
697 | "funding": {
698 | "url": "https://github.com/sponsors/ljharb"
699 | }
700 | },
701 | "node_modules/socket.io": {
702 | "version": "4.6.2",
703 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.2.tgz",
704 | "integrity": "sha512-Vp+lSks5k0dewYTfwgPT9UeGGd+ht7sCpB7p0e83VgO4X/AHYWhXITMrNk/pg8syY2bpx23ptClCQuHhqi2BgQ==",
705 | "dependencies": {
706 | "accepts": "~1.3.4",
707 | "base64id": "~2.0.0",
708 | "debug": "~4.3.2",
709 | "engine.io": "~6.4.2",
710 | "socket.io-adapter": "~2.5.2",
711 | "socket.io-parser": "~4.2.4"
712 | },
713 | "engines": {
714 | "node": ">=10.0.0"
715 | }
716 | },
717 | "node_modules/socket.io-adapter": {
718 | "version": "2.5.2",
719 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
720 | "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
721 | "dependencies": {
722 | "ws": "~8.11.0"
723 | }
724 | },
725 | "node_modules/socket.io-parser": {
726 | "version": "4.2.4",
727 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
728 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
729 | "dependencies": {
730 | "@socket.io/component-emitter": "~3.1.0",
731 | "debug": "~4.3.1"
732 | },
733 | "engines": {
734 | "node": ">=10.0.0"
735 | }
736 | },
737 | "node_modules/socket.io-parser/node_modules/debug": {
738 | "version": "4.3.4",
739 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
740 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
741 | "dependencies": {
742 | "ms": "2.1.2"
743 | },
744 | "engines": {
745 | "node": ">=6.0"
746 | },
747 | "peerDependenciesMeta": {
748 | "supports-color": {
749 | "optional": true
750 | }
751 | }
752 | },
753 | "node_modules/socket.io-parser/node_modules/ms": {
754 | "version": "2.1.2",
755 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
756 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
757 | },
758 | "node_modules/socket.io/node_modules/debug": {
759 | "version": "4.3.4",
760 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
761 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
762 | "dependencies": {
763 | "ms": "2.1.2"
764 | },
765 | "engines": {
766 | "node": ">=6.0"
767 | },
768 | "peerDependenciesMeta": {
769 | "supports-color": {
770 | "optional": true
771 | }
772 | }
773 | },
774 | "node_modules/socket.io/node_modules/ms": {
775 | "version": "2.1.2",
776 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
777 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
778 | },
779 | "node_modules/statuses": {
780 | "version": "2.0.1",
781 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
782 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
783 | "engines": {
784 | "node": ">= 0.8"
785 | }
786 | },
787 | "node_modules/toidentifier": {
788 | "version": "1.0.1",
789 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
790 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
791 | "engines": {
792 | "node": ">=0.6"
793 | }
794 | },
795 | "node_modules/type-is": {
796 | "version": "1.6.18",
797 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
798 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
799 | "dependencies": {
800 | "media-typer": "0.3.0",
801 | "mime-types": "~2.1.24"
802 | },
803 | "engines": {
804 | "node": ">= 0.6"
805 | }
806 | },
807 | "node_modules/unpipe": {
808 | "version": "1.0.0",
809 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
810 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
811 | "engines": {
812 | "node": ">= 0.8"
813 | }
814 | },
815 | "node_modules/utils-merge": {
816 | "version": "1.0.1",
817 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
818 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
819 | "engines": {
820 | "node": ">= 0.4.0"
821 | }
822 | },
823 | "node_modules/vary": {
824 | "version": "1.1.2",
825 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
826 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
827 | "engines": {
828 | "node": ">= 0.8"
829 | }
830 | },
831 | "node_modules/ws": {
832 | "version": "8.11.0",
833 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
834 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
835 | "engines": {
836 | "node": ">=10.0.0"
837 | },
838 | "peerDependencies": {
839 | "bufferutil": "^4.0.1",
840 | "utf-8-validate": "^5.0.2"
841 | },
842 | "peerDependenciesMeta": {
843 | "bufferutil": {
844 | "optional": true
845 | },
846 | "utf-8-validate": {
847 | "optional": true
848 | }
849 | }
850 | }
851 | }
852 | }
853 |
--------------------------------------------------------------------------------
/signalingPeerConnection/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "signalingpeerconnection",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "scripts.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "express": "^4.18.2",
15 | "mkcert": "^1.5.1",
16 | "socket.io": "^4.6.2"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/signalingPeerConnection/scripts.js:
--------------------------------------------------------------------------------
1 | const userName = "Rob-"+Math.floor(Math.random() * 100000)
2 | const password = "x";
3 | document.querySelector('#user-name').innerHTML = userName;
4 |
5 | const socket = io.connect('https://localhost:8181/',{
6 | auth: {
7 | userName,password
8 | }
9 | })
10 |
11 | const localVideoEl = document.querySelector('#local-video');
12 | const remoteVideoEl = document.querySelector('#remote-video');
13 |
14 | let localStream; //a var to hold the local video stream
15 | let remoteStream; //a var to hold the remote video stream
16 | let peerConnection; //the peerConnection that the two clients use to talk
17 | let didIOffer = false;
18 |
19 | let peerConfiguration = {
20 | iceServers:[
21 | {
22 | urls:[
23 | 'stun:stun.l.google.com:19302',
24 | 'stun:stun1.l.google.com:19302'
25 | ]
26 | }
27 | ]
28 | }
29 |
30 | //when a client initiates a call
31 | const call = async e=>{
32 | await fetchUserMedia();
33 |
34 | //peerConnection is all set with our STUN servers sent over
35 | await createPeerConnection();
36 |
37 | //create offer time!
38 | try{
39 | console.log("Creating offer...")
40 | const offer = await peerConnection.createOffer();
41 | console.log(offer);
42 | peerConnection.setLocalDescription(offer);
43 | didIOffer = true;
44 | socket.emit('newOffer',offer); //send offer to signalingServer
45 | }catch(err){
46 | console.log(err)
47 | }
48 |
49 | }
50 |
51 | const answerOffer = async(offerObj)=>{
52 | await fetchUserMedia()
53 | await createPeerConnection(offerObj);
54 | const answer = await peerConnection.createAnswer({}); //just to make the docs happy
55 | await peerConnection.setLocalDescription(answer); //this is CLIENT2, and CLIENT2 uses the answer as the localDesc
56 | console.log(offerObj)
57 | console.log(answer)
58 | // console.log(peerConnection.signalingState) //should be have-local-pranswer because CLIENT2 has set its local desc to it's answer (but it won't be)
59 | //add the answer to the offerObj so the server knows which offer this is related to
60 | offerObj.answer = answer
61 | //emit the answer to the signaling server, so it can emit to CLIENT1
62 | //expect a response from the server with the already existing ICE candidates
63 | const offerIceCandidates = await socket.emitWithAck('newAnswer',offerObj)
64 | offerIceCandidates.forEach(c=>{
65 | peerConnection.addIceCandidate(c);
66 | console.log("======Added Ice Candidate======")
67 | })
68 | console.log(offerIceCandidates)
69 | }
70 |
71 | const addAnswer = async(offerObj)=>{
72 | //addAnswer is called in socketListeners when an answerResponse is emitted.
73 | //at this point, the offer and answer have been exchanged!
74 | //now CLIENT1 needs to set the remote
75 | await peerConnection.setRemoteDescription(offerObj.answer)
76 | // console.log(peerConnection.signalingState)
77 | }
78 |
79 | const fetchUserMedia = ()=>{
80 | return new Promise(async(resolve, reject)=>{
81 | try{
82 | const stream = await navigator.mediaDevices.getUserMedia({
83 | video: true,
84 | // audio: true,
85 | });
86 | localVideoEl.srcObject = stream;
87 | localStream = stream;
88 | resolve();
89 | }catch(err){
90 | console.log(err);
91 | reject()
92 | }
93 | })
94 | }
95 |
96 | const createPeerConnection = (offerObj)=>{
97 | return new Promise(async(resolve, reject)=>{
98 | //RTCPeerConnection is the thing that creates the connection
99 | //we can pass a config object, and that config object can contain stun servers
100 | //which will fetch us ICE candidates
101 | peerConnection = await new RTCPeerConnection(peerConfiguration)
102 | remoteStream = new MediaStream()
103 | remoteVideoEl.srcObject = remoteStream;
104 |
105 |
106 | localStream.getTracks().forEach(track=>{
107 | //add localtracks so that they can be sent once the connection is established
108 | peerConnection.addTrack(track,localStream);
109 | })
110 |
111 | peerConnection.addEventListener("signalingstatechange", (event) => {
112 | console.log(event);
113 | console.log(peerConnection.signalingState)
114 | });
115 |
116 | peerConnection.addEventListener('icecandidate',e=>{
117 | console.log('........Ice candidate found!......')
118 | console.log(e)
119 | if(e.candidate){
120 | socket.emit('sendIceCandidateToSignalingServer',{
121 | iceCandidate: e.candidate,
122 | iceUserName: userName,
123 | didIOffer,
124 | })
125 | }
126 | })
127 |
128 | peerConnection.addEventListener('track',e=>{
129 | console.log("Got a track from the other peer!! How excting")
130 | console.log(e)
131 | e.streams[0].getTracks().forEach(track=>{
132 | remoteStream.addTrack(track,remoteStream);
133 | console.log("Here's an exciting moment... fingers cross")
134 | })
135 | })
136 |
137 | if(offerObj){
138 | //this won't be set when called from call();
139 | //will be set when we call from answerOffer()
140 | // console.log(peerConnection.signalingState) //should be stable because no setDesc has been run yet
141 | await peerConnection.setRemoteDescription(offerObj.offer)
142 | // console.log(peerConnection.signalingState) //should be have-remote-offer, because client2 has setRemoteDesc on the offer
143 | }
144 | resolve();
145 | })
146 | }
147 |
148 | const addNewIceCandidate = iceCandidate=>{
149 | peerConnection.addIceCandidate(iceCandidate)
150 | console.log("======Added Ice Candidate======")
151 | }
152 |
153 |
154 | document.querySelector('#call').addEventListener('click',call)
--------------------------------------------------------------------------------
/signalingPeerConnection/server.js:
--------------------------------------------------------------------------------
1 |
2 | const fs = require('fs');
3 | const https = require('https')
4 | const express = require('express');
5 | const app = express();
6 | const socketio = require('socket.io');
7 | app.use(express.static(__dirname))
8 |
9 | //we need a key and cert to run https
10 | //we generated them with mkcert
11 | // $ mkcert create-ca
12 | // $ mkcert create-cert
13 | const key = fs.readFileSync('cert.key');
14 | const cert = fs.readFileSync('cert.crt');
15 |
16 | //we changed our express setup so we can use https
17 | //pass the key and cert to createServer on https
18 | const expressServer = https.createServer({key, cert}, app);
19 | //create our socket.io server... it will listen to our express port
20 | const io = socketio(expressServer);
21 | expressServer.listen(8181);
22 |
23 | //offers will contain {}
24 | const offers = [
25 | // offererUserName
26 | // offer
27 | // offerIceCandidates
28 | // answererUserName
29 | // answer
30 | // answererIceCandidates
31 | ];
32 | const connectedSockets = [
33 | //username, socketId
34 | ]
35 |
36 | io.on('connection',(socket)=>{
37 | // console.log("Someone has connected");
38 | const userName = socket.handshake.auth.userName;
39 | const password = socket.handshake.auth.password;
40 |
41 | if(password !== "x"){
42 | socket.disconnect(true);
43 | return;
44 | }
45 | connectedSockets.push({
46 | socketId: socket.id,
47 | userName
48 | })
49 |
50 | //a new client has joined. If there are any offers available,
51 | //emit them out
52 | if(offers.length){
53 | socket.emit('availableOffers',offers);
54 | }
55 |
56 | socket.on('newOffer',newOffer=>{
57 | offers.push({
58 | offererUserName: userName,
59 | offer: newOffer,
60 | offerIceCandidates: [],
61 | answererUserName: null,
62 | answer: null,
63 | answererIceCandidates: []
64 | })
65 | // console.log(newOffer.sdp.slice(50))
66 | //send out to all connected sockets EXCEPT the caller
67 | socket.broadcast.emit('newOfferAwaiting',offers.slice(-1))
68 | })
69 |
70 | socket.on('newAnswer',(offerObj,ackFunction)=>{
71 | console.log(offerObj);
72 | //emit this answer (offerObj) back to CLIENT1
73 | //in order to do that, we need CLIENT1's socketid
74 | const socketToAnswer = connectedSockets.find(s=>s.userName === offerObj.offererUserName)
75 | if(!socketToAnswer){
76 | console.log("No matching socket")
77 | return;
78 | }
79 | //we found the matching socket, so we can emit to it!
80 | const socketIdToAnswer = socketToAnswer.socketId;
81 | //we find the offer to update so we can emit it
82 | const offerToUpdate = offers.find(o=>o.offererUserName === offerObj.offererUserName)
83 | if(!offerToUpdate){
84 | console.log("No OfferToUpdate")
85 | return;
86 | }
87 | //send back to the answerer all the iceCandidates we have already collected
88 | ackFunction(offerToUpdate.offerIceCandidates);
89 | offerToUpdate.answer = offerObj.answer
90 | offerToUpdate.answererUserName = userName
91 | //socket has a .to() which allows emiting to a "room"
92 | //every socket has it's own room
93 | socket.to(socketIdToAnswer).emit('answerResponse',offerToUpdate)
94 | })
95 |
96 | socket.on('sendIceCandidateToSignalingServer',iceCandidateObj=>{
97 | const { didIOffer, iceUserName, iceCandidate } = iceCandidateObj;
98 | // console.log(iceCandidate);
99 | if(didIOffer){
100 | //this ice is coming from the offerer. Send to the answerer
101 | const offerInOffers = offers.find(o=>o.offererUserName === iceUserName);
102 | if(offerInOffers){
103 | offerInOffers.offerIceCandidates.push(iceCandidate)
104 | // 1. When the answerer answers, all existing ice candidates are sent
105 | // 2. Any candidates that come in after the offer has been answered, will be passed through
106 | if(offerInOffers.answererUserName){
107 | //pass it through to the other socket
108 | const socketToSendTo = connectedSockets.find(s=>s.userName === offerInOffers.answererUserName);
109 | if(socketToSendTo){
110 | socket.to(socketToSendTo.socketId).emit('receivedIceCandidateFromServer',iceCandidate)
111 | }else{
112 | console.log("Ice candidate recieved but could not find answere")
113 | }
114 | }
115 | }
116 | }else{
117 | //this ice is coming from the answerer. Send to the offerer
118 | //pass it through to the other socket
119 | const offerInOffers = offers.find(o=>o.answererUserName === iceUserName);
120 | const socketToSendTo = connectedSockets.find(s=>s.userName === offerInOffers.offererUserName);
121 | if(socketToSendTo){
122 | socket.to(socketToSendTo.socketId).emit('receivedIceCandidateFromServer',iceCandidate)
123 | }else{
124 | console.log("Ice candidate recieved but could not find offerer")
125 | }
126 | }
127 | // console.log(offers)
128 | })
129 |
130 | })
131 |
132 |
133 |
--------------------------------------------------------------------------------
/signalingPeerConnection/socketListeners.js:
--------------------------------------------------------------------------------
1 |
2 | //on connection get all available offers and call createOfferEls
3 | socket.on('availableOffers',offers=>{
4 | console.log(offers)
5 | createOfferEls(offers)
6 | })
7 |
8 | //someone just made a new offer and we're already here - call createOfferEls
9 | socket.on('newOfferAwaiting',offers=>{
10 | createOfferEls(offers)
11 | })
12 |
13 | socket.on('answerResponse',offerObj=>{
14 | console.log(offerObj)
15 | addAnswer(offerObj)
16 | })
17 |
18 | socket.on('receivedIceCandidateFromServer',iceCandidate=>{
19 | addNewIceCandidate(iceCandidate)
20 | console.log(iceCandidate)
21 | })
22 |
23 | function createOfferEls(offers){
24 | //make green answer button for this new offer
25 | const answerEl = document.querySelector('#answer');
26 | offers.forEach(o=>{
27 | console.log(o);
28 | const newOfferEl = document.createElement('div');
29 | newOfferEl.innerHTML = ``
30 | newOfferEl.addEventListener('click',()=>answerOffer(o))
31 | answerEl.appendChild(newOfferEl);
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/signalingPeerConnection/styles.css:
--------------------------------------------------------------------------------
1 | #videos{
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | gap: 2em;
5 | }
6 |
7 | .video-player{
8 | background-color: black;
9 | width: 100%;
10 | }
11 |
12 | #video-wrapper{
13 | position: relative;
14 | }
15 |
16 | #waiting{
17 | display: none;
18 | position: absolute;
19 | left: 0;
20 | right: 0;
21 | top: 0;
22 | bottom: 0;
23 | margin: auto;
24 | width: 240px;
25 | height: 40px;
26 |
27 | }
28 |
29 | #answer{
30 |
31 | }
--------------------------------------------------------------------------------
/signalingPeerConnection/taskList.txt:
--------------------------------------------------------------------------------
1 | 1. Someone must getUserMedia() - CLIENT1/Init/Caller/Offerer
2 | 2. CLIENT1 creates RTCPeerConnection
3 | 3. peerConnection needs STUN servers
4 | - we will need ICE candidates later
5 | 4. CLIENT1 add localstream tracks to peerConnection
6 | - we need to associate CLIENT1 feed with peerConnection
7 | 5. CLIENT1 creates an offer
8 | - needed peerConnection with tracks
9 | - offer = RTCSessionDescription
10 | 1. SDP - codec/resolution information
11 | 2. Type (offer)
12 | 6. CLIENT1 hands offer to pc.setLocalDescription
13 | ~7. ICE candidates can now start coming in (ASYNC)
14 | SIGNALING (someone to help the browser find/talk to each)
15 | 8. CLIENT1 emits offer
16 | - socket.io server holds it for the other browser
17 | - associate with CLIENT1
18 | ~9. Once 7 happens, emit ICE c. up to signaling server
19 | - socket.io server holds it for the other browser
20 | - associate with CLIENT1
21 | CLIENT1 and Signaling server wait.
22 | - wait for an answerer/CLIENT2/reciever
23 | 10. CLIENT2 loads up the webpage with io.connect()
24 | - a new client is connected to signaling/socket.io server
25 | 11. socket.io emit out the RTCSessionDesc to the new client
26 | - an offer to be sent!
27 | 12. CLIENT2 runs getUserMedia()
28 | 13. CLIENT2 creates a peerConnection()
29 | - pass STUN servers
30 | 14. CLIENT2 adds localstream tracks to peerconnection
31 | 15. CLIENT2 creates an answer (createAnswer());
32 | - createAnswer = RTCSessionDescription (sdp/type)
33 | 16. CLIENT2 hands answer to pc.setLocalDescription
34 | 17. Because CLIENT2 has the offer, CLIENT2 can hand the offer to pc.setRemoteDescription
35 | ~18. when setLocalDescription, start collecting ICE candidates (ASYNC)
36 | Signaling server has been waiting...
37 | 19. CLIENT2 emit answer (RTCSessionDesc - sdp/type) up to signaling server
38 | ~20. CLIENT2 will listen for tracks/ICE from remote.
39 | - and is done.
40 | - waiting on ICE candidates
41 | - waiting on tracks
42 | 21. signaling server listens for answer, emits CLIENT1 answer (RTCSessionDesc - sdp/type)
43 | 22. CLIENT1 takes the answer and hands it to pc.setRemoteDesc
44 | ~23. CLIENT1 waits for ICE candidates and tracks
45 |
46 | 21 & 23 are waiting for ICE. Once ICE is exchanged, tracks will exchange
47 |
--------------------------------------------------------------------------------
/starterFiles/gumPlayground/changeButtons.js:
--------------------------------------------------------------------------------
1 | //green = btn-success
2 | //blue = btn-primary
3 | //grey = btn-secondary
4 | //red = btn-danger
5 |
6 | const buttonsById = [
7 | 'share', 'show-video', 'stop-video', 'change-size', 'start-record',
8 | 'stop-record','play-record','share-screen'
9 | ]
10 |
11 | //buttonEls will be an array of dom elements in order of buttonsById
12 | const buttonEls = buttonsById.map(buttonId=>document.getElementById(buttonId));
13 |
14 | const changeButtons = (colorsArray)=>{
15 | colorsArray.forEach((color,i)=>{
16 | buttonEls[i].classList.remove('btn-success');
17 | buttonEls[i].classList.remove('btn-primary');
18 | buttonEls[i].classList.remove('btn-secondary');
19 | buttonEls[i].classList.remove('btn-danger');
20 | if(color === "green"){
21 | buttonEls[i].classList.add('btn-success');
22 | }else if(color === "blue"){
23 | buttonEls[i].classList.add('btn-primary');
24 | }else if(color === "grey"){
25 | buttonEls[i].classList.add('btn-secondary');
26 | }else if(color === "red"){
27 | buttonEls[i].classList.add('btn-danger');
28 | }
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/starterFiles/gumPlayground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
44 |
45 |
46 |
My feed
47 |
48 |
49 |
50 |
Their feed
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/starterFiles/gumPlayground/styles.css:
--------------------------------------------------------------------------------
1 |
2 | .container{
3 | margin: 20px auto;
4 | }
5 |
6 | .video{
7 | background-color:#333;
8 | padding:20px;
9 | border-radius:10px;
10 | color:#fff;
11 | font-size:40px;
12 | height: 40vh;
13 | }
14 |
15 | #vid-width,#vid-height{
16 | width: 50px;
17 | }
--------------------------------------------------------------------------------
/starterFiles/signalingPeerConnection/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Page Title
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Waiting for answer...
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/starterFiles/signalingPeerConnection/stunServers.js:
--------------------------------------------------------------------------------
1 | let peerConfiguration = {
2 | iceServers:[
3 | {
4 | urls:[
5 | 'stun:stun.l.google.com:19302',
6 | 'stun:stun1.l.google.com:19302'
7 | ]
8 | }
9 | ]
10 | }
--------------------------------------------------------------------------------
/starterFiles/signalingPeerConnection/styles.css:
--------------------------------------------------------------------------------
1 | #videos{
2 | display: grid;
3 | grid-template-columns: 1fr 1fr;
4 | gap: 2em;
5 | }
6 |
7 | .video-player{
8 | background-color: black;
9 | width: 100%;
10 | }
11 |
12 | #video-wrapper{
13 | position: relative;
14 | }
15 |
16 | #waiting{
17 | display: none;
18 | position: absolute;
19 | left: 0;
20 | right: 0;
21 | top: 0;
22 | bottom: 0;
23 | margin: auto;
24 | width: 240px;
25 | height: 40px;
26 |
27 | }
28 |
29 | #answer{
30 |
31 | }
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/.htaccess-file:
--------------------------------------------------------------------------------
1 |
2 | RewriteEngine On
3 | RewriteBase /subdirectory
4 | RewriteRule ^index\.html$ - [L]
5 | RewriteCond %{REQUEST_FILENAME} !-f
6 | RewriteCond %{REQUEST_FILENAME} !-d
7 | RewriteCond %{REQUEST_FILENAME} !-l
8 | RewriteRule . /index.html [L]
9 |
10 |
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/ActionButtons.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | // import { useDispatch, useSelector } from 'react-redux';
3 | import HangupButton from './HangupButton'
4 | import socket from '../utilities/socketConnection'
5 | import { useSelector } from 'react-redux';
6 |
7 | const ActionButtons = ({openCloseChat,smallFeedlEl})=>{
8 | const callStatus = useSelector(state=>state.callStatus);
9 | // const callStatus = useSelector(state=>state.callStatus);
10 | const menuButtons = useRef(null)
11 | let timer;
12 |
13 |
14 | useEffect(()=>{
15 | const setTimer = ()=>{
16 | // console.log(callStatus.current)
17 | if(callStatus.current !== "idle"){
18 | timer = setTimeout(()=>{
19 | menuButtons.current.classList.add('hidden');
20 | // console.log("no movement for 4sec. Hiding")
21 | }, 4000);
22 | }
23 | }
24 |
25 | window.addEventListener('mousemove', ()=>{
26 | //mouse moved!
27 | //it's hidden. Remove class to display and start the timer
28 | if (menuButtons.current && menuButtons.current.classList && menuButtons.current.classList.contains('hidden')) {
29 | // console.log("Not showing. Show now")
30 | menuButtons.current.classList.remove('hidden');
31 | setTimer();
32 | }else{
33 | // Not hidden, just reset start timer
34 | clearTimeout(timer); //clear out the old timer
35 | setTimer();
36 | }
37 | });
38 | },[])
39 |
40 | let micText;
41 | if(callStatus.current === "idle"){
42 | micText = "Join Audio"
43 | }else if(callStatus.audio){
44 | micText = "Mute"
45 | }else{
46 | micText = "Unmute"
47 | }
48 |
49 | return(
50 |
95 | )
96 | }
97 |
98 | export default ActionButtons;
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/CallInfo.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 | import { useEffect, useState } from 'react'
3 |
4 | const CallInfo = ({apptInfo})=>{
5 |
6 | const [ momentText, setMomentText ] = useState(moment(apptInfo.apptDate).fromNow())
7 |
8 | useEffect(() => {
9 | const timeInterval = setInterval(()=>{
10 | setMomentText(moment(apptInfo.apptDate).fromNow())
11 | console.log("Updating time")
12 | },5000)
13 | return () => {
14 | console.log("Clearing")
15 | clearInterval(timeInterval);
16 | };
17 | }, []);
18 |
19 | return(
20 |
21 |
22 | {apptInfo.professionalsFullName} has been notified.
23 | Your appointment is {momentText}.
24 |
25 |
26 | )
27 | }
28 |
29 | export default CallInfo
30 |
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/HangUpButtons.js:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux"
2 | import updateCallStatus from "../redux-elements/actions/updateCallStatus"
3 |
4 | const HangupButton = ()=>{
5 |
6 | const dispatch = useDispatch()
7 | const callStatus = useSelector(state=>state.callStatus)
8 |
9 | const hangupCall = ()=>{
10 | dispatch(updateCallStatus('current','complete'))
11 | }
12 |
13 | if(callStatus.current === "complete"){
14 | return <>>
15 | }
16 |
17 | return(
18 |
22 | )
23 | }
24 |
25 | export default HangupButton
26 |
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/ProDashboard.css:
--------------------------------------------------------------------------------
1 | img{
2 | max-width: 100%;
3 | }
4 |
5 | .dash-box.redish-bg img{
6 | width: 50%;
7 | text-align: center;
8 | }
9 |
10 | li{
11 | list-style: none;
12 | }
13 |
14 | .main-border{
15 | margin-top: 10px;
16 | height: 50px;
17 | border-radius: 10px 0 0;
18 | }
19 |
20 | .row.num-2{
21 | margin-top: 15px;
22 | }
23 |
24 | .col-8.row .col-6{
25 | padding: 15px;
26 | }
27 |
28 | .left-rail{
29 | height: 85vh;
30 |
31 | }
32 |
33 | .clients-board{
34 | border-radius: 10px;
35 | height: 200px;
36 | overflow: scroll;
37 | }
38 |
39 | .purple-bg{
40 | background-color:#705CF3;
41 | }
42 |
43 | .blue-bg{
44 | background-color:#3B80E1;
45 | }
46 |
47 | .orange-bg{
48 | background-color:#EA9C44;
49 | }
50 |
51 | .green-bg{
52 | background-color:#418F8F
53 | }
54 |
55 | .redish-bg{
56 | background-color:#a7265a;
57 | }
58 |
59 | .dash-box{
60 | color: white;
61 | padding: 10px;
62 | }
63 |
64 | .row.num-2 .col-6 {
65 | overflow: hidden;
66 | }
67 |
68 | .row.num-2 .dash-box {
69 | padding-bottom: 999px;
70 | margin-bottom: -999px;
71 | height: auto;
72 | }
73 |
74 | .pointer{
75 | cursor: pointer;
76 | }
77 |
78 | .calendar{
79 |
80 | }
81 |
82 | .fa.fa-user{
83 | text-align: center;
84 | font-size: 60px;
85 | border: 1px solid white;
86 | border-radius: 50%;
87 | padding: 30px;
88 | color: white;
89 | }
90 |
91 | .menu-item{
92 | padding: 20px 0px;
93 | margin: 0px -10px;
94 | }
95 |
96 | .menu-item.active, .menu-item:hover{
97 | background: #4a3cff
98 | }
99 |
100 | .left-rail li{
101 | text-align: left;
102 | width: 60%;
103 | margin: auto;
104 | font-size: 24px;
105 | color: white;
106 | }
107 |
108 | .left-rail li i{
109 | margin-right: 10px;
110 | }
111 |
112 | .waiting-text{
113 | animation: blinker 1s linear infinite;
114 | font-size: 24px;
115 | color: red;
116 | }
117 |
118 | .join-btn{
119 | margin-left: 10px;
120 | }
121 |
122 | @keyframes blinker {
123 | 50% {
124 | opacity: 0;
125 | }
126 | }
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/ProDashboard.js:
--------------------------------------------------------------------------------
1 | import './ProDashboard.css'
2 |
3 | const ProDashboard = ()=>{
4 | console.log("Test")
5 | return(
6 |
7 |
10 |
11 |
12 |
13 |
14 |
Dashboard
15 |
16 |
17 |
Calendar
18 |
19 |
20 |
Settings
21 |
22 |
23 |
Files
24 |
25 |
26 |
Reports
27 |
28 |
29 |
30 |
31 |
Dashboard
32 |
33 |
34 |
35 |
Clients
36 | Jim Jones
37 |
38 |
39 |
40 |
41 |
Coming Appointments
42 |
Akash Patel - 8-10-23 11am Waiting
43 |
Jim Jones - 8-10-23, 2pm
44 |
Mike Williams - 8-10-23 3pm
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
Files
53 |
54 |
file
55 |
56 |
57 |
58 |
59 |
Analytics
60 |
61 |

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |

73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default ProDashboard
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/apptSeedData.js:
--------------------------------------------------------------------------------
1 | const professionalAppointments = [{
2 | professionalsFullName: "Peter Chan, J.D.",
3 | apptDate: Date.now() + 500000,
4 | uuid:1,
5 | clientName: "Jim Jones",
6 | },{
7 | professionalsFullName: "Peter Chan, J.D.",
8 | apptDate: Date.now() - 2000000,
9 | uuid:2,// uuid:uuidv4(),
10 | clientName: "Akash Patel",
11 | },{
12 | professionalsFullName: "Peter Chan, J.D.",
13 | apptDate: Date.now() + 10000000,
14 | uuid:3,//uuid:uuidv4(),
15 | clientName: "Mike Williams",
16 | }];
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/callStatusReducer.js:
--------------------------------------------------------------------------------
1 | const initState = {
2 | current: "idle",
3 | video: false,
4 | audio: false,
5 | audioDevice: 'default',
6 | videoDevice: 'default',
7 | shareScreen: false,
8 | haveMedia: false,
9 | }
10 |
11 | export default (state = initState, action)=>{
12 | if (action.type === "UPDATE_CALL_STATUS"){
13 | const copyState = {...state}
14 | copyState[action.payload.prop] = action.payload.value
15 | return copyState
16 | }else if((action.type === "LOGOUT_ACTION") || (action.type === "NEW_VERSION")){
17 | return initState
18 | }else{
19 | return state
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/reverse-proxy-file:
--------------------------------------------------------------------------------
1 |
2 | ServerName api.deploying-javascript.com
3 |
4 | ProxyPreserveHost On
5 | ProxyPass http://127.0.0.1:9000/
6 | ProxyPassReverse http://127.0.0.1:9000/
7 |
8 | RewriteEngine on
9 | RewriteCond ${HTTP:Upgrade} websocket [NC]
10 | RewriteCond ${HTTP:Connection} upgrade [NC]
11 | RewriteRule .* "wss:/localhost:3000/$1" [P,L]
12 |
13 |
14 |
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/stunServers.js:
--------------------------------------------------------------------------------
1 | let peerConfiguration = {
2 | iceServers:[
3 | {
4 | urls:[
5 | 'stun:stun.l.google.com:19302',
6 | 'stun:stun1.l.google.com:19302'
7 | ]
8 | }
9 | ]
10 | }
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/vhost-file:
--------------------------------------------------------------------------------
1 |
2 | DocumentRoot /var/www/LOCATION_OF_YOUR_APP
3 | ServerName deploying-javascript.com/
4 |
5 | allow from all
6 | AllowOverride All
7 | Order allow,deny
8 | Options +Indexes
9 |
10 |
--------------------------------------------------------------------------------
/starterFiles/teleLegalSite/videoComponents.css:
--------------------------------------------------------------------------------
1 | .container-fluid{
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | #large-feed{
7 | background-color: black;
8 | height: 100vh;
9 | width: 100vw;
10 | transform: scaleX(-1);
11 | }
12 |
13 | #own-feed{
14 | position: absolute;
15 | border: white 1px solid;
16 | right: 50px;
17 | top: 50px;
18 | border-radius: 10px;
19 | width: 320px;
20 | }
21 |
22 | #menu-buttons{
23 | height: 80px;
24 | width: 100%;
25 | background-color: #333;
26 | position: absolute;
27 | bottom: -6px;
28 | left: 11px;
29 | }
30 |
31 | #menu-buttons .fa{
32 | font-size: 24px;
33 | }
34 |
35 | .hidden{
36 | display: none;
37 | }
38 |
39 | .button-no-caret, .button-wrapper, .button{
40 | width: 100px;
41 | height: 80px;
42 | position: relative;
43 | }
44 |
45 |
46 |
47 | .button-no-caret:hover, .button:hover{
48 | position: relative;
49 | background-color: #555;
50 | cursor: pointer;
51 | }
52 |
53 | .button-no-caret i, .button-wrapper i{
54 | font-size:32px;
55 | color:#ccc;
56 | position: absolute;
57 | left: 35px;
58 | top: 20px;
59 | }
60 |
61 | .button-wrapper i.fa-caret-up{
62 | left: 75px;
63 | top: 0px;
64 | z-index: 100000;
65 | padding: 5px;
66 | }
67 |
68 | .button-wrapper i.fa-caret-up:hover{
69 | background-color: #555;
70 | cursor: pointer;
71 | }
72 |
73 | .caret-dropdown{
74 | position: absolute;
75 | left: 100px;
76 | }
77 |
78 | .caret-dropdown select{
79 | background-color: #333;
80 | color: white;
81 | }
82 |
83 | .button .fa.fa-comment{
84 | left: 40px;
85 | }
86 |
87 | .hang-up{
88 | position: relative;
89 | right: 10px;
90 | top: 20px;
91 | }
92 |
93 | .btn-text{
94 | position: absolute;
95 | bottom: 10px;
96 | color: white;
97 | text-align: center;
98 | width: 100%;
99 | }
100 |
101 |
102 | .chat-window{
103 | position: absolute;
104 | right: -25%;
105 | height: 100px;
106 | transition: all 1s;
107 | z-index: 1000;
108 | width: 25%;
109 | top: 0;
110 | height: 100vh;
111 | }
112 |
113 | .chat-window.show{
114 | border: 1px solid black;
115 | background-color: white;
116 | right:0px;
117 | }
118 |
119 | .video-chat-wrapper{
120 | position: relative;
121 | overflow: hidden;
122 | }
123 |
124 | .call-info{
125 | position: absolute;
126 | top: 50%;
127 | left: 50%;
128 | transform: translate(-50%, -50%);
129 | border: 1px solid #cacaca;
130 | background-color: #222;
131 | padding: 10px;
132 | }
133 |
134 | .call-info h1{
135 | color: white;
136 | }
137 |
--------------------------------------------------------------------------------
/teleLegalSite/.gitignore:
--------------------------------------------------------------------------------
1 | */certs/*
2 | */package-lock.json
3 | */yarn.lock
4 |
5 |
--------------------------------------------------------------------------------
/teleLegalSite/teleLegal-back-end/expressRoutes.js:
--------------------------------------------------------------------------------
1 | //this is where all our express stuff happens (routes)
2 | const app = require('./server').app;
3 | const jwt = require('jsonwebtoken');
4 | const linkSecret = "ijr2iq34rfeiadsfkjq3ew";
5 | const { v4: uuidv4 } = require('uuid');
6 |
7 | //normally this would be persistent data... db, api, file, etc.
8 | const professionalAppointments = [{
9 | professionalsFullName: "Peter Chan, J.D.",
10 | apptDate: Date.now() + 500000,
11 | uuid:1,
12 | clientName: "Jim Jones",
13 | },{
14 | professionalsFullName: "Peter Chan, J.D.",
15 | apptDate: Date.now() - 2000000,
16 | uuid:2,// uuid:uuidv4(),
17 | clientName: "Akash Patel",
18 | },{
19 | professionalsFullName: "Peter Chan, J.D.",
20 | apptDate: Date.now() + 10000000,
21 | uuid:3,//uuid:uuidv4(),
22 | clientName: "Mike Williams",
23 | }];
24 |
25 | app.set('professionalAppointments',professionalAppointments)
26 |
27 | //this route is for US! In production, a receptionist, or calender/scheduling app
28 | //would send this out. We will print it out and paste it in. It will drop
29 | //us on our React site with the right info for CLIENT1 to make an offer
30 | app.get('/user-link',(req, res)=>{
31 |
32 | const apptData = professionalAppointments[0];
33 |
34 | professionalAppointments.push(apptData);
35 |
36 | //we need to encode this data in a token
37 | //so it can be added to a url
38 | const token = jwt.sign(apptData,linkSecret);
39 | res.send('https://localhost:3000/join-video?token='+token);
40 | // res.json("This is a test route")
41 | })
42 |
43 | app.post('/validate-link',(req, res)=>{
44 | //get the token from the body of the post request ( thanks express.json() )
45 | const token = req.body.token;
46 | //decode the jwt with our secret
47 | const decodedData = jwt.verify(token,linkSecret);
48 | //send the decoded data (our object) back to the front end
49 | res.json(decodedData)
50 | })
51 |
52 | app.get('/pro-link',(req, res)=>{
53 | const userData = {
54 | fullName: "Peter Chan, J.D.",
55 | proId: 1234,
56 | }
57 | const token = jwt.sign(userData,linkSecret);
58 | res.send(`Link Here`);
59 | })
--------------------------------------------------------------------------------
/teleLegalSite/teleLegal-back-end/index.js:
--------------------------------------------------------------------------------
1 | //this is our entry point - run nodemon here!
2 |
3 | require('./socketServer')
4 | require('./expressRoutes')
5 |
--------------------------------------------------------------------------------
/teleLegalSite/teleLegal-back-end/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "telelegal-back-end",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "cors": "^2.8.5",
14 | "express": "^4.18.2",
15 | "jsonwebtoken": "^9.0.0",
16 | "socket.io": "^4.7.0",
17 | "uuid": "^9.0.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/teleLegalSite/teleLegal-back-end/server.js:
--------------------------------------------------------------------------------
1 | //this is where we create the express and socket.io server
2 |
3 | const fs = require('fs'); //the file system
4 | const https = require('https');
5 | const http = require('http');
6 | const express = require('express');
7 | const cors = require('cors');
8 |
9 | const socketio = require('socket.io');
10 | const app = express();
11 | app.use(cors()) //this will open our Express API to ANY domain
12 | app.use(express.static(__dirname+'/public'));
13 | app.use(express.json()); //this will allow us to parse json in the body with the body parser
14 |
15 | // const key = fs.readFileSync('./certs/cert.key'); //for local development https
16 | // const cert = fs.readFileSync('./certs/cert.crt'); //for local development https
17 |
18 | // const expressServer = https.createServer({key, cert}, app); //for local development https
19 | const expressServer = http.createServer({}, app);
20 | const io = socketio(expressServer,{
21 | cors: [
22 | 'https://localhost:3000',
23 | 'https://localhost:3001',
24 | 'https://localhost:3002',
25 | 'https://www.deploying-javascript.com',
26 | // 'http://www.deploying-javascript.com', TEST ONLY
27 | ]
28 | })
29 |
30 | expressServer.listen(9000);
31 | module.exports = { io, expressServer, app };
32 |
--------------------------------------------------------------------------------
/teleLegalSite/teleLegal-back-end/socketServer.js:
--------------------------------------------------------------------------------
1 | //all our socketServer stuff happens here
2 | const io = require('./server').io;
3 | const app = require('./server').app;
4 | const linkSecret = "ijr2iq34rfeiadsfkjq3ew";
5 | const jwt = require('jsonwebtoken');
6 |
7 | // const professionalAppointments = app.get('professionalAppointments')
8 |
9 | const connectedProfessionals = [];
10 | const connectedClients = [];
11 |
12 | const allKnownOffers = {
13 | // uniqueId - key
14 | //offer
15 | //professionalsFullName
16 | //clientName
17 | //apptDate
18 | //offererIceCandidates
19 | //answer
20 | //answerIceCandidates
21 | };
22 |
23 | io.on('connection',socket=>{
24 | console.log(socket.id,"has connected")
25 |
26 | const handshakeData = socket.handshake.auth.jwt;
27 | let decodedData
28 | try{
29 | decodedData = jwt.verify(handshakeData,linkSecret);
30 | }catch(err){
31 | console.log(err);
32 | //these arent the droids were looking for. Star wars...
33 | // goodbye.
34 | socket.disconnect()
35 | return
36 | }
37 |
38 | const { fullName, proId } = decodedData;
39 |
40 | if(proId){
41 | //this is a professional. Update/add to connectedProfessionals
42 | //check to see if this user is already in connectedProfessionals
43 | //this would happen because they have reconnected
44 | const connectedPro = connectedProfessionals.find(cp=>cp.proId === proId)
45 | if(connectedPro){
46 | //if they are, then just update the new socket.id
47 | connectedPro.socketId = socket.id;
48 | }else{
49 | //otherwise push them on
50 | connectedProfessionals.push({
51 | socketId: socket.id,
52 | fullName,
53 | proId
54 | })
55 | }
56 | //send the appt data out to the professional
57 | const professionalAppointments = app.get('professionalAppointments');
58 | socket.emit('apptData',professionalAppointments.filter(pa=>pa.professionalsFullName === fullName))
59 |
60 | //loop through all known offers and send out to the professional that just joined,
61 | //the ones that belong to him/her
62 | for(const key in allKnownOffers){
63 | if(allKnownOffers[key].professionalsFullName === fullName){
64 | //this offer is for this pro
65 | io.to(socket.id).emit('newOfferWaiting',allKnownOffers[key])
66 | }
67 | }
68 | }else{
69 | //this is a client
70 | const { professionalsFullName, uuid, clientName } = decodedData;
71 | //check to see if the client is already in the array
72 | //why? could have reconnected
73 | const clientExist = connectedClients.find(c=>c.uuid == uuid)
74 | if(clientExist){
75 | //already connected. just update the id
76 | clientExist.socketId = socket.id
77 | }else{
78 | //add them
79 | connectedClients.push({
80 | clientName,
81 | uuid,
82 | professionalMeetingWith: professionalsFullName,
83 | socketId: socket.id,
84 | })
85 | }
86 |
87 | const offerForThisClient = allKnownOffers[uuid];
88 | if(offerForThisClient){
89 | io.to(socket.id).emit('answerToClient',offerForThisClient.answer);
90 | }
91 |
92 | }
93 |
94 | console.log(connectedProfessionals)
95 |
96 | socket.on('newAnswer',({answer,uuid})=>{
97 | //emit this to the client
98 | const socketToSendTo = connectedClients.find(c=>c.uuid == uuid);
99 | if(socketToSendTo){
100 | socket.to(socketToSendTo.socketId).emit('answerToClient',answer);
101 | }
102 | //update the offer
103 | const knownOffer = allKnownOffers[uuid];
104 | if(knownOffer){
105 | knownOffer.answer = answer;
106 | }
107 |
108 | })
109 |
110 | socket.on('newOffer',({offer, apptInfo})=>{
111 | //offer = sdp/type, apptInfo has the uuid that we can add to allKnownOffers
112 | //so that, the professional can find EXACTLY the right allKnownOffers
113 | allKnownOffers[apptInfo.uuid] = {
114 | ...apptInfo,
115 | offer,
116 | offererIceCandidates: [],
117 | answer: null,
118 | answerIceCandidates: [],
119 | }
120 | //we dont emit this to everyone like we did our chat server
121 | //we only want this to go to our professional.
122 |
123 | //we got professionalAppointments from express (thats where its made)
124 | const professionalAppointments = app.get('professionalAppointments');
125 | //find this particular appt so we can update that the user is waiting (has sent us an offer)
126 | const pa = professionalAppointments.find(pa=>pa.uuid === apptInfo.uuid);
127 | if(pa){
128 | pa.waiting = true;
129 | }
130 |
131 | //find this particular professional so we can emit
132 | const p = connectedProfessionals.find(cp=>cp.fullName === apptInfo.professionalsFullName)
133 | if(p){
134 | //only emit if the professional is connected
135 | const socketId = p.socketId;
136 | //send the new offer over
137 | socket.to(socketId).emit('newOfferWaiting',allKnownOffers[apptInfo.uuid])
138 | //send the updated appt info with the new waiting
139 | socket.to(socketId).emit('apptData',professionalAppointments.filter(pa=>pa.professionalsFullName === apptInfo.professionalsFullName))
140 | }
141 | })
142 |
143 | socket.on('getIce',(uuid,who,ackFunc)=>{
144 | const offer = allKnownOffers[uuid];
145 | // console.log(offer)
146 | let iceCandidates = [];
147 | if(offer){
148 | if(who === "professional"){
149 | iceCandidates = offer.offererIceCandidates
150 | }else if(who === "client"){
151 | iceCandidates = offer.answerIceCandidates;
152 | }
153 | ackFunc(iceCandidates)
154 | }
155 | })
156 |
157 | socket.on('iceToServer',({who,iceC,uuid})=>{
158 | console.log("==============",who)
159 | const offerToUpdate = allKnownOffers[uuid];
160 | if(offerToUpdate){
161 | if(who === "client"){
162 | //this means the client has sent up an iceC
163 | //update the offer
164 | offerToUpdate.offererIceCandidates.push(iceC)
165 | const socketToSendTo = connectedProfessionals.find(cp=>cp.fullName === decodedData.professionalsFullName)
166 | if(socketToSendTo){
167 | socket.to(socketToSendTo.socketId).emit('iceToClient',iceC);
168 | }
169 | }else if(who === "professional"){
170 | offerToUpdate.answerIceCandidates.push(iceC)
171 | const socketToSendTo = connectedClients.find(cp=>cp.uuid == uuid)
172 | if(socketToSendTo){
173 | socket.to(socketToSendTo.socketId).emit('iceToClient',iceC);
174 | }
175 | }
176 | }
177 | })
178 | })
179 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/.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 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "telelegal-front-end",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^13.0.0",
8 | "@testing-library/user-event": "^13.2.1",
9 | "axios": "^1.4.0",
10 | "moment": "^2.29.4",
11 | "react": "^18.2.0",
12 | "react-dom": "^18.2.0",
13 | "react-redux": "^8.1.1",
14 | "react-router-dom": "^6.14.0",
15 | "react-scripts": "5.0.1",
16 | "redux": "^4.2.1",
17 | "socket.io-client": "^4.7.0",
18 | "web-vitals": "^2.1.0"
19 | },
20 | "scripts": {
21 | "start": "HTTPS=true SSL_CRT_FILE=./certs/cert.crt SSL_KEY_FILE=./certs/cert.key 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 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robertbunch/webrtcCourse/a0d1091bc168690ab6397782e11083f664d0b22b/teleLegalSite/telelegal-front-end/public/favicon.ico
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
19 |
20 |
29 | React App
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robertbunch/webrtcCourse/a0d1091bc168690ab6397782e11083f664d0b22b/teleLegalSite/telelegal-front-end/public/logo192.png
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robertbunch/webrtcCourse/a0d1091bc168690ab6397782e11083f664d0b22b/teleLegalSite/telelegal-front-end/public/logo512.png
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/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 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/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 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
2 | import './App.css';
3 | import socketConnection from './webRTCutilities/socketConnection'
4 | import MainVideoPage from './videoComponents/MainVideoPage';
5 | import ProDashboard from './siteComponents/ProDashboard';
6 | import ProMainVideoPage from './videoComponents/ProMainVideoPage';
7 |
8 | const Home = ()=>Hello, Home page
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | export default App;
24 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 | import { createStore } from 'redux'; //get createStore method so we can make a redux store
5 | import { Provider } from 'react-redux'; //get the Provider component to wrap around our whole app
6 | import rootReducer from './redux-elements/reducers/rootReducer';
7 |
8 | const theStore = createStore(rootReducer);
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 | root.render(
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/redux-elements/actions/addStream.js:
--------------------------------------------------------------------------------
1 |
2 | export default (who,stream,peerConnection)=>{
3 | return{
4 | type: "ADD_STREAM",
5 | payload: {
6 | who,
7 | stream,
8 | peerConnection // for local, undefined
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/redux-elements/actions/updateCallStatus.js:
--------------------------------------------------------------------------------
1 |
2 | export default(prop,value)=>{
3 | return{
4 | type:"UPDATE_CALL_STATUS",
5 | payload: {prop,value}
6 | }
7 | }
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/redux-elements/reducers/callStatusReducer.js:
--------------------------------------------------------------------------------
1 | const initState = {
2 | current: "idle", //negotiating, progress, complete
3 | video: "off", //video feed status: "off" "enabled" "disabled" "complete"
4 | audio: "off", //audio feed status: "off" "enabled" "disabled" "complete"
5 | audioDevice: 'default', //enumerate devices, chosen audio input device (we dont care about the output device)
6 | videoDevice: 'default',
7 | shareScreen: false,
8 | haveMedia: false, //is there a localStream, has getUserMedia been run
9 | haveCreatedOffer: false,
10 | }
11 |
12 | export default (state = initState, action)=>{
13 | if (action.type === "UPDATE_CALL_STATUS"){
14 | const copyState = {...state}
15 | copyState[action.payload.prop] = action.payload.value
16 | return copyState
17 | }else if((action.type === "LOGOUT_ACTION") || (action.type === "NEW_VERSION")){
18 | return initState
19 | }else{
20 | return state
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/redux-elements/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import callStatusReducer from "./callStatusReducer";
3 | import streamsReducer from "./streamsReducer";
4 |
5 | const rootReducer = combineReducers({
6 | callStatus: callStatusReducer,
7 | streams: streamsReducer,
8 | })
9 |
10 | export default rootReducer
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/redux-elements/reducers/streamsReducer.js:
--------------------------------------------------------------------------------
1 | //this holds all streams as objects
2 | //{
3 | // who
4 | // stream = thing with tracks that plays in
5 | // peerConnection = actual webRTC connection
6 | // }
7 |
8 | //local, remote1, remote2+
9 |
10 | export default (state = {}, action)=>{
11 | if(action.type === "ADD_STREAM"){
12 | const copyState = {...state};
13 | copyState[action.payload.who] = action.payload
14 | return copyState
15 | }else if(action.type === "LOGOUT_ACTION"){
16 | return {}
17 | }else{
18 | return state;
19 | }
20 | }
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/siteComponents/ProDashboard.css:
--------------------------------------------------------------------------------
1 | img{
2 | max-width: 100%;
3 | }
4 |
5 | .dash-box.redish-bg img{
6 | width: 50%;
7 | text-align: center;
8 | }
9 |
10 | li{
11 | list-style: none;
12 | }
13 |
14 | .main-border{
15 | margin-top: 10px;
16 | height: 50px;
17 | border-radius: 10px 0 0;
18 | }
19 |
20 | .row.num-2{
21 | margin-top: 15px;
22 | }
23 |
24 | .col-8.row .col-6{
25 | padding: 15px;
26 | }
27 |
28 | .left-rail{
29 | height: 85vh;
30 |
31 | }
32 |
33 | .clients-board{
34 | border-radius: 10px;
35 | height: 200px;
36 | overflow: scroll;
37 | }
38 |
39 | .purple-bg{
40 | background-color:#705CF3;
41 | }
42 |
43 | .blue-bg{
44 | background-color:#3B80E1;
45 | }
46 |
47 | .orange-bg{
48 | background-color:#EA9C44;
49 | }
50 |
51 | .green-bg{
52 | background-color:#418F8F
53 | }
54 |
55 | .redish-bg{
56 | background-color:#a7265a;
57 | }
58 |
59 | .dash-box{
60 | color: white;
61 | padding: 10px;
62 | }
63 |
64 | .row.num-2 .col-6 {
65 | overflow: hidden;
66 | }
67 |
68 | .row.num-2 .dash-box {
69 | padding-bottom: 999px;
70 | margin-bottom: -999px;
71 | height: auto;
72 | }
73 |
74 | .pointer{
75 | cursor: pointer;
76 | }
77 |
78 | .calendar{
79 |
80 | }
81 |
82 | .fa.fa-user{
83 | text-align: center;
84 | font-size: 60px;
85 | border: 1px solid white;
86 | border-radius: 50%;
87 | padding: 30px;
88 | color: white;
89 | }
90 |
91 | .menu-item{
92 | padding: 20px 0px;
93 | margin: 0px -10px;
94 | }
95 |
96 | .menu-item.active, .menu-item:hover{
97 | background: #4a3cff
98 | }
99 |
100 | .left-rail li{
101 | text-align: left;
102 | width: 60%;
103 | margin: auto;
104 | font-size: 24px;
105 | color: white;
106 | }
107 |
108 | .left-rail li i{
109 | margin-right: 10px;
110 | }
111 |
112 | .waiting-text{
113 | animation: blinker 1s linear infinite;
114 | font-size: 24px;
115 | color: red;
116 | }
117 |
118 | .join-btn{
119 | margin-left: 10px;
120 | }
121 |
122 | @keyframes blinker {
123 | 50% {
124 | opacity: 0;
125 | }
126 | }
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/siteComponents/ProDashboard.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import './ProDashboard.css'
3 | import { useSearchParams, useNavigate } from 'react-router-dom';
4 | import axios from 'axios';
5 | import socketConnection from '../webRTCutilities/socketConnection';
6 | import proSocketListeners from '../webRTCutilities/proSocketListeners';
7 | import moment from 'moment';
8 | import { useDispatch } from 'react-redux';
9 |
10 | const ProDashboard = ()=>{
11 |
12 | const [ searchParams, setSearchParams ] = useSearchParams();
13 | const navigate = useNavigate();
14 | const [ apptInfo, setApptInfo ] = useState([]);
15 | const dispatch = useDispatch();
16 |
17 | useEffect(()=>{
18 | //grab the token var out of the query string
19 | const token = searchParams.get('token');
20 | const socket = socketConnection(token);
21 | proSocketListeners.proDashabordSocketListeners(socket,setApptInfo,dispatch);
22 | },[])
23 |
24 | const joinCall = (appt)=>{
25 | console.log(appt);
26 | const token = searchParams.get('token');
27 | //navigate to /join-video-pro
28 | navigate(`/join-video-pro?token=${token}&uuid=${appt.uuid}&client=${appt.clientName}`)
29 | }
30 |
31 | return(
32 |
33 |
36 |
37 |
38 |
39 |
40 |
Dashboard
41 |
42 |
43 |
Calendar
44 |
45 |
46 |
Settings
47 |
48 |
49 |
Files
50 |
51 |
52 |
Reports
53 |
54 |
55 |
56 |
57 |
Dashboard
58 |
59 |
60 |
61 |
Clients
62 | Jim Jones
63 |
64 |
65 |
66 |
67 |
Coming Appointments
68 | {apptInfo.map(a=>
69 |
{a.clientName} - {moment(a.apptDate).calendar()}
70 | {a.waiting ? <>
71 | Waiting
72 |
73 | > : <>>}
74 |
75 |
76 | )}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
Files
86 |
87 |
file
88 |
89 |
90 |
91 |
92 |
Analytics
93 |
94 |

95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |

106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | )
115 | }
116 |
117 | export default ProDashboard
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/ActionButtonCaretDropDown.js:
--------------------------------------------------------------------------------
1 |
2 | const ActionButtonCaretDropDown = ({defaultValue,changeHandler,deviceList,type})=>{
3 |
4 | let dropDownEl;
5 | if(type==="video"){
6 | dropDownEl = deviceList.map(vd=>)
7 | }else if(type === "audio"){
8 | const audioInputEl = [];
9 | const audioOutputEl = [];
10 | deviceList.forEach((d,i)=>{
11 | if(d.kind === "audioinput"){
12 | audioInputEl.push()
13 | }else if(d.kind === "audiooutput"){
14 | audioOutputEl.push()
15 | }
16 | })
17 | audioInputEl.unshift()
18 | audioOutputEl.unshift()
19 | dropDownEl = audioInputEl.concat(audioOutputEl)
20 | }
21 |
22 | return(
23 |
24 |
27 |
28 | )
29 | }
30 |
31 | export default ActionButtonCaretDropDown
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/ActionButtons.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | // import { useDispatch, useSelector } from 'react-redux';
3 | import HangupButton from './HangupButton'
4 | import socket from '../webRTCutilities/socketConnection'
5 | import { useSelector } from 'react-redux';
6 | import VideoButton from './VideoButton/VideoButton';
7 | import AudioButton from './AudioButton/AudioButton';
8 |
9 | const ActionButtons = ({openCloseChat,smallFeedEl, largeFeedEl})=>{
10 | const callStatus = useSelector(state=>state.callStatus);
11 | // const callStatus = useSelector(state=>state.callStatus);
12 | const menuButtons = useRef(null)
13 | let timer;
14 |
15 |
16 | useEffect(()=>{
17 | const setTimer = ()=>{
18 | // console.log(callStatus.current)
19 | if(callStatus.current !== "idle"){
20 | timer = setTimeout(()=>{
21 | menuButtons.current.classList.add('hidden');
22 | // console.log("no movement for 4sec. Hiding")
23 | }, 4000);
24 | }
25 | }
26 |
27 | window.addEventListener('mousemove', ()=>{
28 | //mouse moved!
29 | //it's hidden. Remove class to display and start the timer
30 | if (menuButtons.current && menuButtons.current.classList && menuButtons.current.classList.contains('hidden')) {
31 | // console.log("Not showing. Show now")
32 | menuButtons.current.classList.remove('hidden');
33 | setTimer();
34 | }else{
35 | // Not hidden, just reset start timer
36 | clearTimeout(timer); //clear out the old timer
37 | setTimer();
38 | }
39 | });
40 | },[])
41 |
42 | return(
43 |
79 | )
80 | }
81 |
82 | export default ActionButtons;
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/AudioButton/AudioButton.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux"
3 | import ActionButtonCaretDropDown from "../ActionButtonCaretDropDown";
4 | import getDevices from "../VideoButton/getDevices";
5 | import updateCallStatus from "../../redux-elements/actions/updateCallStatus";
6 | import addStream from "../../redux-elements/actions/addStream";
7 | import startAudioStream from "./startAudioStream";
8 |
9 | const AudioButton = ({smallFeedEl})=>{
10 |
11 | const dispatch = useDispatch()
12 | const callStatus = useSelector(state=>state.callStatus);
13 | const streams = useSelector(state=>state.streams);
14 | const [ caretOpen, setCaretOpen ] = useState(false);
15 | const [ audioDeviceList, setAudioDeviceList ] = useState([]);
16 |
17 | let micText;
18 | if(callStatus.audio === "off"){
19 | micText = "Join Audio"
20 | }else if(callStatus.audio === "enabled"){
21 | micText = "Mute"
22 | }else{
23 | micText = "Unmute"
24 | }
25 |
26 | useEffect(()=>{
27 | const getDevicesAsync = async()=>{
28 | if(caretOpen){
29 | //then we need to check for audio devices
30 | const devices = await getDevices();
31 | console.log(devices.videoDevices)
32 | setAudioDeviceList(devices.audioOutputDevices.concat(devices.audioInputDevices))
33 | }
34 | }
35 | getDevicesAsync()
36 | },[caretOpen])
37 |
38 | const startStopAudio = ()=>{
39 | //first, check if the audio is enabled, if so disabled
40 | if(callStatus.audio === "enabled"){
41 | //update redux callStatus
42 | dispatch(updateCallStatus('audio',"disabled"));
43 | //set the stream to disabled
44 | const tracks = streams.localStream.stream.getAudioTracks();
45 | tracks.forEach(t=>t.enabled = false);
46 | }else if(callStatus.audio === "disabled"){
47 | //second, check if the audio is disabled, if so enable
48 | //update redux callStatus
49 | dispatch(updateCallStatus('audio',"enabled"));
50 | const tracks = streams.localStream.stream.getAudioTracks();
51 | tracks.forEach(t=>t.enabled = true);
52 | }else{
53 | //audio is "off" What do we do?
54 | changeAudioDevice({target:{value:"inputdefault"}})
55 | //add the tracks
56 | startAudioStream(streams);
57 | }
58 | }
59 |
60 | const changeAudioDevice = async(e)=>{
61 | //the user changed the desired ouput audio device OR input audio device
62 | //1. we need to get that deviceId AND the type
63 | const deviceId = e.target.value.slice(5);
64 | const audioType = e.target.value.slice(0,5);
65 | console.log(e.target.value)
66 |
67 | if(audioType === "output"){
68 | //4 (sort of out of order). update the smallFeedEl
69 | //we are now DONE! We dont care about the output for any other reason
70 | smallFeedEl.current.setSinkId(deviceId);
71 | }else if(audioType === "input"){
72 | //2. we need to getUserMedia (permission)
73 | const newConstraints = {
74 | audio: {deviceId: {exact: deviceId}},
75 | video: callStatus.videoDevice === "default" ? true : {deviceId: {exact: callStatus.videoDevice}},
76 | }
77 | const stream = await navigator.mediaDevices.getUserMedia(newConstraints)
78 | //3. update Redux with that videoDevice, and that video is enabled
79 | dispatch(updateCallStatus('audioDevice',deviceId));
80 | dispatch(updateCallStatus('audio','enabled'))
81 | //5. we need to update the localStream in streams
82 | dispatch(addStream('localStream',stream))
83 | //6. add tracks - actually replaceTracks
84 | const [audioTrack] = stream.getAudioTracks();
85 | //come back to this later
86 |
87 | for(const s in streams){
88 | if(s !== "localStream"){
89 | //getSenders will grab all the RTCRtpSenders that the PC has
90 | //RTCRtpSender manages how tracks are sent via the PC
91 | const senders = streams[s].peerConnection.getSenders();
92 | //find the sender that is in charge of the video track
93 | const sender = senders.find(s=>{
94 | if(s.track){
95 | //if this track matches the videoTrack kind, return it
96 | return s.track.kind === audioTrack.kind
97 | }else{
98 | return false;
99 | }
100 | })
101 | //sender is RTCRtpSender, so it can replace the track
102 | sender.replaceTrack(audioTrack)
103 | }
104 | }
105 |
106 | }
107 | }
108 |
109 | return(
110 |
111 |
setCaretOpen(!caretOpen)}>
112 |
113 |
114 |
{micText}
115 |
116 | {caretOpen ?
: <>>}
122 |
123 | )
124 | }
125 |
126 | export default AudioButton
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/AudioButton/startAudioStream.js:
--------------------------------------------------------------------------------
1 |
2 | //this functions job is to update all peerConnections (addTracks) and update redux callStatus
3 | import updateCallStatus from "../../redux-elements/actions/updateCallStatus";
4 |
5 | const startAudioStream = (streams)=>{
6 | const localStream = streams.localStream;
7 | for(const s in streams){ //s is the key
8 | if(s !== "localStream"){
9 | //we don't addTracks to the localStream
10 | const curStream = streams[s];
11 | //addTracks to all peerConnecions
12 | localStream.stream.getAudioTracks().forEach(t=>{
13 | curStream.peerConnection.addTrack(t,streams.localStream.stream);
14 | })
15 | }
16 | }
17 | }
18 |
19 | export default startAudioStream;
20 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/CallInfo.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 | import { useEffect, useState } from 'react'
3 |
4 | const CallInfo = ({apptInfo})=>{
5 |
6 | const [ momentText, setMomentText ] = useState(moment(apptInfo.apptDate).fromNow())
7 |
8 | useEffect(() => {
9 | const timeInterval = setInterval(()=>{
10 | setMomentText(moment(apptInfo.apptDate).fromNow())
11 | // console.log("Updating time")
12 | },5000)
13 | //clean up function
14 | return () => {
15 | // console.log("Clearing")
16 | clearInterval(timeInterval);
17 | };
18 | }, []);
19 |
20 | return(
21 |
22 |
23 | {apptInfo.professionalsFullName} has been notified.
24 | Your appointment is {momentText}.
25 |
26 |
27 | )
28 | }
29 |
30 | export default CallInfo
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/ChatWindow.js:
--------------------------------------------------------------------------------
1 |
2 | const ChatWindow = ()=>{
3 |
4 | return(
5 |
6 |
Chat
7 |
8 | )
9 |
10 | }
11 |
12 | export default ChatWindow
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/HangupButton.js:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux"
2 | import updateCallStatus from "../redux-elements/actions/updateCallStatus"
3 |
4 | const HangupButton = ({largeFeedEl, smallFeedEl})=>{
5 |
6 | const dispatch = useDispatch()
7 | const callStatus = useSelector(state=>state.callStatus)
8 | const streams = useSelector(state=>state.streams)
9 |
10 | const hangupCall = ()=>{
11 | dispatch(updateCallStatus('current','complete'))
12 | //user has clicked hang up
13 | for(const s in streams){
14 | //loop through all streams, and if there is a pc, close it
15 | //remove listeners
16 | //set it to null
17 | if(streams[s].peerConnection){
18 | streams[s].peerConnection.close();
19 | streams[s].peerConnection.onicecandidate = null
20 | streams[s].peerConnection.onaddstream = null
21 | streams[s].peerConnection = null;
22 | }
23 | }
24 | //set both video tags to empty
25 | smallFeedEl.current.srcObject = null;
26 | largeFeedEl.current.srcObject = null;
27 | }
28 |
29 | if(callStatus.current === "complete"){
30 | return <>>
31 | }
32 |
33 | return(
34 |
38 | )
39 | }
40 |
41 | export default HangupButton
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/MainVideoPage.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import { useSearchParams } from "react-router-dom"
3 | import axios from 'axios';
4 | import './VideoComponents.css';
5 | import CallInfo from "./CallInfo";
6 | import ChatWindow from "./ChatWindow";
7 | import ActionButtons from "./ActionButtons";
8 | import addStream from '../redux-elements/actions/addStream';
9 | import { useDispatch, useSelector } from "react-redux";
10 | import createPeerConnection from "../webRTCutilities/createPeerConnection";
11 | import socketConnection from '../webRTCutilities/socketConnection';
12 | import updateCallStatus from "../redux-elements/actions/updateCallStatus";
13 | import clientSocketListeners from "../webRTCutilities/clientSocketListeners";
14 |
15 | const MainVideoPage = ()=>{
16 |
17 | const dispatch = useDispatch();
18 | const callStatus = useSelector(state=>state.callStatus)
19 | const streams = useSelector(state=>state.streams)
20 | //get query string finder hook
21 | const [ searchParams, setSearchParams ] = useSearchParams();
22 | const [ apptInfo, setApptInfo ] = useState({})
23 | const smallFeedEl = useRef(null); //this is a React ref to a dom element, so we can interact with it the React way
24 | const largeFeedEl = useRef(null);
25 | const uuidRef = useRef(null);
26 | const streamsRef = useRef(null);
27 | const [ showCallInfo, setShowCallInfo] = useState(true)
28 |
29 | useEffect(()=>{
30 | //fetch the user media
31 | const fetchMedia = async()=>{
32 | const constraints = {
33 | video: true, //must have one constraint, just dont show it yet
34 | audio: true, //if you make a video chat app that doesnt use audio, but does (????), then init this as false, and add logic later ... hahaha
35 | }
36 | try{
37 | const stream = await navigator.mediaDevices.getUserMedia(constraints);
38 | dispatch(updateCallStatus('haveMedia',true)); //update our callStatus reducer to know that we have the media
39 | //dispatch will send this function to the redux dispatcher so all reducers are notified
40 | //we send 2 args, the who, and the stream
41 | dispatch(addStream('localStream',stream));
42 | const { peerConnection, remoteStream } = await createPeerConnection(addIce);
43 | //we don't know "who" we are talking to... yet.
44 | dispatch(addStream('remote1',remoteStream, peerConnection));
45 | //we have a peerconnection... let's make an offer!
46 | //EXCEPT, it's not time yet.
47 | //SDP = information about the feed, and we have NO tracks
48 | //socket.emit...
49 | largeFeedEl.current.srcObject = remoteStream //we have the remoteStream from our peerConnection. Set the video feed to be the remoteStream jsut created
50 | }catch(err){
51 | console.log(err);
52 | }
53 | }
54 | fetchMedia()
55 | },[])
56 |
57 | useEffect(()=>{
58 | //we cannot update streamsRef until we know redux is finished
59 | if(streams.remote1){
60 | streamsRef.current = streams;
61 | }
62 | },[streams])
63 |
64 | useEffect(()=>{
65 | const createOfferAsync = async()=>{
66 | //we have audio and video and we need an offer. Let's make it!
67 | for(const s in streams){
68 | if(s !== "localStream"){
69 | try{
70 | const pc = streams[s].peerConnection;
71 | const offer = await pc.createOffer()
72 | pc.setLocalDescription(offer);
73 | //get the token from the url for the socket connection
74 | const token = searchParams.get('token');
75 | //get the socket from socketConnection
76 | const socket = socketConnection(token)
77 | socket.emit('newOffer',{offer,apptInfo})
78 | //add our event listeners
79 | }catch(err){
80 | console.log(err);
81 | }
82 | }
83 | }
84 | dispatch(updateCallStatus('haveCreatedOffer',true));
85 | }
86 | if(callStatus.audio === "enabled" && callStatus.video === "enabled" && !callStatus.haveCreatedOffer){
87 | createOfferAsync()
88 | }
89 | },[callStatus.audio, callStatus.video, callStatus.haveCreatedOffer])
90 |
91 | useEffect(()=>{
92 | const asyncAddAnswer = async()=>{
93 | //listen for changes to callStatus.answer
94 | //if it exists, we have an answer!
95 | for(const s in streams){
96 | if(s !== "localStream"){
97 | const pc = streams[s].peerConnection;
98 | await pc.setRemoteDescription(callStatus.answer);
99 | console.log(pc.signalingState)
100 | console.log("Answer added!")
101 | }
102 | }
103 | }
104 |
105 | if(callStatus.answer){
106 | asyncAddAnswer()
107 | }
108 |
109 | },[callStatus.answer])
110 |
111 | useEffect(()=>{
112 | //grab the token var out of the query string
113 | const token = searchParams.get('token');
114 | console.log(token)
115 | const fetchDecodedToken = async()=>{
116 | const resp = await axios.post('https://api.deploying-javascript.com/validate-link',{token});
117 | console.log(resp.data);
118 | setApptInfo(resp.data)
119 | uuidRef.current = resp.data.uuid;
120 | }
121 | fetchDecodedToken();
122 | },[])
123 |
124 | useEffect(()=>{
125 | //grab the token var out of the query string
126 | const token = searchParams.get('token');
127 | const socket = socketConnection(token);
128 | clientSocketListeners(socket,dispatch,addIceCandidateToPc);
129 | },[])
130 |
131 | const addIceCandidateToPc = (iceC)=>{
132 | //add an ice candidate form the remote, to the pc
133 | for (const s in streamsRef.current){
134 | if(s !== 'localStream'){
135 | const pc = streamsRef.current[s].peerConnection;
136 | pc.addIceCandidate(iceC);
137 | console.log("Added an iceCandidate to existing page presence")
138 | setShowCallInfo(false);
139 | }
140 | }
141 | }
142 |
143 | const addIce = (iceC)=>{
144 | //emit a new icecandidate to the signalaing server
145 | const socket = socketConnection(searchParams.get('token'));
146 | socket.emit('iceToServer',{
147 | iceC,
148 | who: 'client',
149 | uuid: uuidRef.current, //we used a useRef to keep the value fresh
150 | })
151 |
152 | }
153 |
154 | return(
155 |
156 |
157 | {/* Div to hold our remote video, our local video, and our chat window*/}
158 |
159 |
160 | {showCallInfo ? : <>>}
161 |
162 |
163 |
167 |
168 | )
169 | }
170 |
171 | export default MainVideoPage
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/ProMainVideoPage.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import { useSearchParams } from "react-router-dom"
3 | import axios from 'axios';
4 | import './VideoComponents.css';
5 | import CallInfo from "./CallInfo";
6 | import ChatWindow from "./ChatWindow";
7 | import ActionButtons from "./ActionButtons";
8 | import addStream from '../redux-elements/actions/addStream';
9 | import { useDispatch, useSelector } from "react-redux";
10 | import createPeerConnection from "../webRTCutilities/createPeerConnection";
11 | import socketConnection from '../webRTCutilities/socketConnection';
12 | import updateCallStatus from "../redux-elements/actions/updateCallStatus";
13 | import proSocketListeners from "../webRTCutilities/proSocketListeners";
14 |
15 | const ProMainVideoPage = ()=>{
16 |
17 | const dispatch = useDispatch();
18 | const callStatus = useSelector(state=>state.callStatus)
19 | const streams = useSelector(state=>state.streams)
20 | //get query string finder hook
21 | const [ searchParams, setSearchParams ] = useSearchParams();
22 | const [ apptInfo, setApptInfo ] = useState({})
23 | const smallFeedEl = useRef(null); //this is a React ref to a dom element, so we can interact with it the React way
24 | const largeFeedEl = useRef(null);
25 | const [ haveGottenIce, setHaveGottenIce ] = useState(false)
26 | const streamsRef = useRef(null);
27 |
28 | useEffect(()=>{
29 | //fetch the user media
30 | const fetchMedia = async()=>{
31 | const constraints = {
32 | video: true, //must have one constraint, just dont show it yet
33 | audio: true, //if you make a video chat app that doesnt use audio, but does (????), then init this as false, and add logic later ... hahaha
34 | }
35 | try{
36 | const stream = await navigator.mediaDevices.getUserMedia(constraints);
37 | dispatch(updateCallStatus('haveMedia',true)); //update our callStatus reducer to know that we have the media
38 | //dispatch will send this function to the redux dispatcher so all reducers are notified
39 | //we send 2 args, the who, and the stream
40 | dispatch(addStream('localStream',stream));
41 | const { peerConnection, remoteStream } = await createPeerConnection(addIce);
42 | //we don't know "who" we are talking to... yet.
43 | dispatch(addStream('remote1',remoteStream, peerConnection));
44 | //we have a peerconnection... let's make an offer!
45 | //EXCEPT, it's not time yet.
46 | //SDP = information about the feed, and we have NO tracks
47 | //socket.emit...
48 | largeFeedEl.current.srcObject = remoteStream
49 | }catch(err){
50 | console.log(err);
51 | }
52 | }
53 | fetchMedia()
54 | },[])
55 |
56 | useEffect(()=>{
57 | const getIceAsync = async()=>{
58 | const socket = socketConnection(searchParams.get('token'))
59 | const uuid = searchParams.get('uuid');
60 | const iceCandidates = await socket.emitWithAck('getIce',uuid,"professional")
61 | console.log("iceCandidate Received");
62 | console.log(iceCandidates);
63 | iceCandidates.forEach(iceC=>{
64 | for(const s in streams){
65 | if(s !== 'localStream'){
66 | const pc = streams[s].peerConnection;
67 | pc.addIceCandidate(iceC)
68 | console.log("=======Added Ice Candidate!!!!!!!")
69 | }
70 | }
71 | })
72 | }
73 | if(streams.remote1 && !haveGottenIce){
74 | setHaveGottenIce(true);
75 | getIceAsync()
76 | streamsRef.current = streams; //update streamsRef once we know streams exists
77 | }
78 |
79 | },[streams,haveGottenIce])
80 |
81 | useEffect(()=>{
82 | const setAsyncOffer = async()=>{
83 | for(const s in streams){
84 | if(s !== "localStream"){
85 | const pc = streams[s].peerConnection;
86 | await pc.setRemoteDescription(callStatus.offer)
87 | console.log(pc.signalingstate); //should be have remote offer
88 | }
89 | }
90 | }
91 | if(callStatus.offer && streams.remote1 && streams.remote1.peerConnection){
92 | setAsyncOffer()
93 | }
94 | },[callStatus.offer,streams.remote1])
95 |
96 | useEffect(()=>{
97 | const createAnswerAsync = async()=>{
98 | //we have audio and video, we can make an answer and setLocalDescription
99 | for(const s in streams){
100 | if(s !== "localStream"){
101 | const pc = streams[s].peerConnection;
102 | //make an answer
103 | const answer = await pc.createAnswer();
104 | //because this is the answering client, the answer is the localDesc
105 | await pc.setLocalDescription(answer);
106 | console.log(pc.signalingState);//have local answer
107 | dispatch(updateCallStatus('haveCreatedAnswer',true))
108 | dispatch(updateCallStatus('answer',answer))
109 | //emit the answer to the server
110 | const token = searchParams.get('token');
111 | const socket = socketConnection(token);
112 | const uuid = searchParams.get('uuid');
113 | console.log("emitting",answer,uuid)
114 | socket.emit('newAnswer',{answer,uuid})
115 | }
116 | }
117 | }
118 | //we only create an answer if audio and video are enabled AND haveCreatedAnswer is false
119 | //this may run many times, but these 3 events will only happen one
120 | if(callStatus.audio === "enabled" && callStatus.video === "enabled" && !callStatus.haveCreatedAnswer){
121 | createAnswerAsync()
122 | }
123 | },[callStatus.audio, callStatus.video, callStatus.haveCreatedAnswer])
124 |
125 |
126 | useEffect(()=>{
127 | //grab the token var out of the query string
128 | const token = searchParams.get('token');
129 | console.log(token)
130 | const fetchDecodedToken = async()=>{
131 | const resp = await axios.post('https://api.deploying-javascript.com/validate-link',{token});
132 | console.log(resp.data);
133 | setApptInfo(resp.data)
134 | }
135 | fetchDecodedToken();
136 | },[])
137 |
138 | useEffect(()=>{
139 | //grab the token var out of the query string
140 | const token = searchParams.get('token');
141 | const socket = socketConnection(token);
142 | proSocketListeners.proVideoSocketListeners(socket,addIceCandidateToPc);
143 | },[])
144 |
145 | const addIceCandidateToPc = (iceC)=>{
146 | //add an ice candidate form the remote, to the pc
147 | for (const s in streamsRef.current){
148 | if(s !== 'localStream'){
149 | const pc = streamsRef.current[s].peerConnection;
150 | pc.addIceCandidate(iceC);
151 | console.log("Added an iceCandidate to existing page presence")
152 | }
153 | }
154 | }
155 |
156 | const addIce = (iceC)=>{
157 | //emit ice candidate to the server
158 | const socket = socketConnection(searchParams.get('token'))
159 | socket.emit('iceToServer',{
160 | iceC,
161 | who: 'professional',
162 | uuid: searchParams.get('uuid')
163 | })
164 | }
165 |
166 | return(
167 |
168 |
169 | {/* Div to hold our remote video, our local video, and our chat window*/}
170 |
171 |
172 | {callStatus.audio === "off" || callStatus.video === "off" ?
173 |
174 |
175 | {searchParams.get('client')} is in the waiting room.
176 | Call will start when video and audio are enabled
177 |
178 | : <>>
179 | }
180 |
181 |
182 |
186 |
187 | )
188 | }
189 |
190 | export default ProMainVideoPage
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/VideoButton/VideoButton.js:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { useEffect, useState } from 'react';
3 | import startLocalVideoStream from "./startLocalVideoStream";
4 | import updateCallStatus from "../../redux-elements/actions/updateCallStatus";
5 | import getDevices from "./getDevices";
6 | import addStream from "../../redux-elements/actions/addStream";
7 | import ActionButtonCaretDropDown from "../ActionButtonCaretDropDown";
8 |
9 | const VideoButton = ({smallFeedEl})=>{
10 |
11 | const dispatch = useDispatch();
12 | const callStatus = useSelector(state=>state.callStatus)
13 | const streams = useSelector(state=>state.streams);
14 | const [ pendingUpdate, setPendingUpdate ] = useState(false);
15 | const [ caretOpen, setCaretOpen ] = useState(false);
16 | const [ videoDeviceList, setVideoDeviceList ] = useState([])
17 |
18 | const DropDown = ()=>{
19 |
20 | }
21 |
22 | useEffect(()=>{
23 | const getDevicesAsync = async()=>{
24 | if(caretOpen){
25 | //then we need to check for video devices
26 | const devices = await getDevices();
27 | console.log(devices.videoDevices)
28 | setVideoDeviceList(devices.videoDevices)
29 | }
30 | }
31 | getDevicesAsync()
32 | },[caretOpen])
33 |
34 | const changeVideoDevice = async(e)=>{
35 | //the user changed the desired video device
36 | //1. we need to get that deviceId
37 | const deviceId = e.target.value;
38 | // console.log(deviceId)
39 | //2. we need to getUserMedia (permission)
40 | const newConstraints = {
41 | audio: callStatus.audioDevice === "default" ? true : {deviceId: {exact: callStatus.audioDevice}},
42 | video: {deviceId: {exact: deviceId}}
43 | }
44 | const stream = await navigator.mediaDevices.getUserMedia(newConstraints)
45 | //3. update Redux with that videoDevice, and that video is enabled
46 | dispatch(updateCallStatus('videoDevice',deviceId));
47 | dispatch(updateCallStatus('video','enabled'))
48 | //4. update the smallFeedEl
49 | smallFeedEl.current.srcObject = stream;
50 | //5. we need to update the localStream in streams
51 | dispatch(addStream('localStream',stream))
52 | //6. add tracks
53 | const [videoTrack] = stream.getVideoTracks();
54 | //come back to this later
55 | //if we stop the old tracks, and add the new tracks, that will mean
56 | // ... renegotiation
57 | for(const s in streams){
58 | if(s !== "localStream"){
59 | //getSenders will grab all the RTCRtpSenders that the PC has
60 | //RTCRtpSender manages how tracks are sent via the PC
61 | const senders = streams[s].peerConnection.getSenders();
62 | //find the sender that is in charge of the video track
63 | const sender = senders.find(s=>{
64 | if(s.track){
65 | //if this track matches the videoTrack kind, return it
66 | return s.track.kind === videoTrack.kind
67 | }else{
68 | return false;
69 | }
70 | })
71 | //sender is RTCRtpSender, so it can replace the track
72 | sender.replaceTrack(videoTrack)
73 | }
74 | }
75 |
76 | }
77 |
78 | const startStopVideo = ()=>{
79 | // console.log("Sanity Check")
80 | //first, check if the video is enabled, if so disabled
81 | if(callStatus.video === "enabled"){
82 | //update redux callStatus
83 | dispatch(updateCallStatus('video',"disabled"));
84 | //set the stream to disabled
85 | const tracks = streams.localStream.stream.getVideoTracks();
86 | tracks.forEach(t=>t.enabled = false);
87 | }else if(callStatus.video === "disabled"){
88 | //second, check if the video is disabled, if so enable
89 | //update redux callStatus
90 | dispatch(updateCallStatus('video',"enabled"));
91 | const tracks = streams.localStream.stream.getVideoTracks();
92 | tracks.forEach(t=>t.enabled = true);
93 | }else if(callStatus.haveMedia){
94 | //thirdly, check to see if we have media, if so, start the stream
95 | //we have the media! show the feed
96 | smallFeedEl.current.srcObject = streams.localStream.stream
97 | //add tracks to the peerConnections
98 | startLocalVideoStream(streams, dispatch);
99 | }else{
100 | //lastly, it is possible, we dont have the media, wait for the media, then start the stream
101 | setPendingUpdate(true);
102 | }
103 | }
104 |
105 | useEffect(()=>{
106 | if(pendingUpdate && callStatus.haveMedia){
107 | console.log('Pending update succeeded!')
108 | //this useEffect will run if pendingUpdate changes to true!
109 | setPendingUpdate(false) // switch back to false
110 | smallFeedEl.current.srcObject = streams.localStream.stream
111 | startLocalVideoStream(streams, dispatch);
112 | }
113 | },[pendingUpdate,callStatus.haveMedia])
114 |
115 | return(
116 |
117 |
setCaretOpen(!caretOpen)}>
118 |
119 |
120 |
{callStatus.video === "enabled" ? "Stop" : "Start"} Video
121 |
122 | {caretOpen ?
: <>>}
128 |
129 | )
130 | }
131 | export default VideoButton;
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/VideoButton/getDevices.js:
--------------------------------------------------------------------------------
1 |
2 | //a utility funciton that fetches all available devices
3 | //both video and audio
4 |
5 | const getDevices = ()=>{
6 | return new Promise(async(resolve, reject)=>{
7 | const devices = await navigator.mediaDevices.enumerateDevices()
8 | // console.log(devices);
9 | const videoDevices = devices.filter(d=>d.kind === "videoinput");
10 | const audioOutputDevices = devices.filter(d=>d.kind === "audiooutput");
11 | const audioInputDevices = devices.filter(d=>d.kind === "audioinput");
12 | resolve({
13 | videoDevices,
14 | audioOutputDevices,
15 | audioInputDevices
16 | })
17 | })
18 | }
19 |
20 | export default getDevices
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/VideoButton/startLocalVideoStream.js:
--------------------------------------------------------------------------------
1 |
2 | //this functions job is to update all peerConnections (addTracks) and update redux callStatus
3 | import updateCallStatus from "../../redux-elements/actions/updateCallStatus";
4 |
5 | const startLocalVideoStream = (streams, dispatch)=>{
6 | const localStream = streams.localStream;
7 | for(const s in streams){ //s is the key
8 | if(s !== "localStream"){
9 | //we don't addTracks to the localStream
10 | const curStream = streams[s];
11 | //addTracks to all peerConnecions
12 | localStream.stream.getVideoTracks().forEach(t=>{
13 | curStream.peerConnection.addTrack(t,streams.localStream.stream);
14 | })
15 | //update redux callStatus
16 | dispatch(updateCallStatus('video',"enabled"));
17 | }
18 |
19 | }
20 | }
21 |
22 | export default startLocalVideoStream;
23 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/videoComponents/VideoComponents.css:
--------------------------------------------------------------------------------
1 | .container-fluid{
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | #large-feed{
7 | background-color: black;
8 | height: 100vh;
9 | width: 100vw;
10 | transform: scaleX(-1);
11 | }
12 |
13 | #own-feed{
14 | position: absolute;
15 | border: white 1px solid;
16 | right: 50px;
17 | top: 50px;
18 | border-radius: 10px;
19 | width: 320px;
20 | }
21 |
22 | #menu-buttons{
23 | height: 80px;
24 | width: 100%;
25 | background-color: #333;
26 | position: absolute;
27 | bottom: -6px;
28 | left: 11px;
29 | }
30 |
31 | #menu-buttons .fa{
32 | font-size: 24px;
33 | }
34 |
35 | .hidden{
36 | display: none;
37 | }
38 |
39 | .button-no-caret, .button-wrapper, .button{
40 | width: 100px;
41 | height: 80px;
42 | position: relative;
43 | }
44 |
45 |
46 |
47 | .button-no-caret:hover, .button:hover{
48 | position: relative;
49 | background-color: #555;
50 | cursor: pointer;
51 | }
52 |
53 | .button-no-caret i, .button-wrapper i{
54 | font-size:32px;
55 | color:#ccc;
56 | position: absolute;
57 | left: 35px;
58 | top: 20px;
59 | }
60 |
61 | .button-wrapper i.fa-caret-up{
62 | left: 75px;
63 | top: 0px;
64 | z-index: 100000;
65 | padding: 5px;
66 | }
67 |
68 | .button-wrapper i.fa-caret-up:hover{
69 | background-color: #555;
70 | cursor: pointer;
71 | }
72 |
73 | .caret-dropdown{
74 | position: absolute;
75 | left: 100px;
76 | }
77 |
78 | .caret-dropdown select{
79 | background-color: #333;
80 | color: white;
81 | }
82 |
83 | .button .fa.fa-comment{
84 | left: 40px;
85 | }
86 |
87 | .hang-up{
88 | position: relative;
89 | right: 10px;
90 | top: 20px;
91 | }
92 |
93 | .btn-text{
94 | position: absolute;
95 | bottom: 10px;
96 | color: white;
97 | text-align: center;
98 | width: 100%;
99 | }
100 |
101 |
102 | .chat-window{
103 | position: absolute;
104 | right: -25%;
105 | height: 100px;
106 | transition: all 1s;
107 | z-index: 1000;
108 | width: 25%;
109 | top: 0;
110 | height: 100vh;
111 | }
112 |
113 | .chat-window.show{
114 | border: 1px solid black;
115 | background-color: white;
116 | right:0px;
117 | }
118 |
119 | .video-chat-wrapper{
120 | position: relative;
121 | overflow: hidden;
122 | }
123 |
124 | .call-info{
125 | position: absolute;
126 | top: 50%;
127 | left: 50%;
128 | transform: translate(-50%, -50%);
129 | border: 1px solid #cacaca;
130 | background-color: #222;
131 | padding: 10px;
132 | }
133 |
134 | .call-info h1{
135 | color: white;
136 | }
137 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/webRTCutilities/clientSocketListeners.js:
--------------------------------------------------------------------------------
1 | import updateCallStatus from "../redux-elements/actions/updateCallStatus";
2 |
3 | const clientSocketListeners = (socket, dispatch,addIceCandidateToPc)=>{
4 |
5 | socket.on('answerToClient',answer=>{
6 | console.log(answer);
7 | dispatch(updateCallStatus('answer',answer))
8 | dispatch(updateCallStatus('myRole','offerer'))
9 | })
10 |
11 | socket.on('iceToClient',iceC=>{
12 | addIceCandidateToPc(iceC)
13 | })
14 |
15 | }
16 |
17 | export default clientSocketListeners
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/webRTCutilities/createPeerConnection.js:
--------------------------------------------------------------------------------
1 | import peerConfiguration from './stunServers'
2 |
3 |
4 | const createPeerConnection = (addIce)=>{
5 | return new Promise(async(resolve, reject)=>{
6 | const peerConnection = await new RTCPeerConnection(peerConfiguration);
7 | //rtcPeerConnection is the connection to the peer.
8 | //we may need more than one this time!!
9 | //we pass it the config object, which is just stun servers
10 | //it will get us ICE candidates
11 | const remoteStream = new MediaStream();
12 | peerConnection.addEventListener('signalingstatechange',(e)=>{
13 | console.log("Signaling State Change")
14 | console.log(e)
15 | })
16 | peerConnection.addEventListener('icecandidate',e=>{
17 | console.log("Found ice candidate...")
18 | if(e.candidate){
19 | addIce(e.candidate)
20 | }
21 | })
22 | peerConnection.addEventListener('track',e=>{
23 | console.log("Got a track from the remote!")
24 | e.streams[0].getTracks().forEach(track=>{
25 | remoteStream.addTrack(track,remoteStream);
26 | console.log("Fingers crossed...")
27 | })
28 | })
29 |
30 | resolve({
31 | peerConnection,
32 | remoteStream,
33 | })
34 | })
35 |
36 | }
37 |
38 | export default createPeerConnection
39 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/webRTCutilities/proSocketListeners.js:
--------------------------------------------------------------------------------
1 | import updateCallStatus from '../redux-elements/actions/updateCallStatus';
2 |
3 | const proDashabordSocketListeners = (socket,setApptInfo,dispatch)=>{
4 | socket.on('apptData',apptData=>{
5 | console.log(apptData)
6 | setApptInfo(apptData)
7 | })
8 |
9 | socket.on('newOfferWaiting',offerData=>{
10 | //dispatch the offer to redux so that it is available for later
11 | dispatch(updateCallStatus('offer',offerData.offer))
12 | dispatch(updateCallStatus('myRole','answerer'))
13 | })
14 | }
15 |
16 | const proVideoSocketListeners = (socket,addIceCandidateToPc)=>{
17 | socket.on('iceToClient',iceC=>{
18 | addIceCandidateToPc(iceC)
19 | })
20 | }
21 |
22 | export default { proDashabordSocketListeners,proVideoSocketListeners }
23 |
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/webRTCutilities/socketConnection.js:
--------------------------------------------------------------------------------
1 | import { io } from 'socket.io-client';
2 |
3 | let socket;
4 | const socketConnection = (jwt)=>{
5 | //check to see if the socket is already connected
6 | if(socket && socket.connected){
7 | //if so, then just return it so whoever needs it, can use it
8 | return socket;
9 | }else{
10 | //its not connected... connect!
11 | socket = io.connect('https://api.deploying-javascript.com',{
12 | auth: {
13 | jwt
14 | }
15 | });
16 | return socket;
17 | }
18 | }
19 |
20 | export default socketConnection;
--------------------------------------------------------------------------------
/teleLegalSite/telelegal-front-end/src/webRTCutilities/stunServers.js:
--------------------------------------------------------------------------------
1 | let peerConfiguration = {
2 | iceServers:[
3 | {
4 | urls:[
5 | 'stun:stun.l.google.com:19302',
6 | 'stun:stun1.l.google.com:19302'
7 | ]
8 | }
9 | ]
10 | }
11 |
12 | export default peerConfiguration
--------------------------------------------------------------------------------