├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── iceservers.json ├── package-lock.json ├── package.json ├── server.js ├── updateserver.sh └── web ├── css ├── fontawesome5.min.css └── main.css ├── endcall.html ├── images └── picInPic.png ├── index.html ├── js ├── adapter.min.js ├── ezWebRTC.js ├── jquery-3.1.1.min.js ├── main.js ├── socket.io.min.js └── utils.js └── webfonts ├── fa-brands-400.eot ├── fa-brands-400.svg ├── fa-brands-400.ttf ├── fa-brands-400.woff ├── fa-brands-400.woff2 ├── fa-regular-400.eot ├── fa-regular-400.svg ├── fa-regular-400.ttf ├── fa-regular-400.woff ├── fa-regular-400.woff2 ├── fa-solid-900.eot ├── fa-solid-900.svg ├── fa-solid-900.ttf ├── fa-solid-900.woff └── fa-solid-900.woff2 /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Node 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | /iceservers.json 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim 2 | 3 | MAINTAINER cracker0dks 4 | 5 | # Create app directory 6 | RUN mkdir -p /opt/app 7 | WORKDIR /opt/app 8 | 9 | # Install app dependencies 10 | COPY ./package.json /opt/app 11 | RUN npm install 12 | 13 | # Bundle app source 14 | COPY . /opt/app 15 | 16 | EXPOSE 3001 17 | CMD [ "npm", "start" ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # basicWebRTC 2 | 3 | Setup your own Videoconference Server for 1on1 and group calls! All Calls are encrypted Peer 2 Peer! 4 | 5 | ## Functions 6 | * Audio 7 | * Video 8 | * 1on1 and group Calls 9 | * Screenshare 10 | 11 | ## Install ## 12 | 13 | 1. Install node and clone this repo 14 | 2. run: npm i 15 | 3. run: node server.js 16 | 4. surf to: http://IP:3001 17 | 18 | ### All User parameters ### 19 | * `username` -> Change your username shown 20 | * `roomname` -> Change the name of the room 21 | * `camon` -> Default is on. Set to false to start session audio only 22 | * `socketdomain` -> Change if you want to use a different socketServer (Can also include path: `https://domainname.tld/path/sub/`) 23 | * `base64domain` -> true if socketDomain is given in base64 format 24 | 25 | Example: change the roomname: https://IP:3001/#roomname=yourSecretRoom 26 | 27 | ### Config Server Listen IP & Port 28 | 29 | Change the env variables: listen_ip ("0.0.0.0" default) and listen_port (3001 default) 30 | 31 | ### Behind a nginx reverse Proxy 32 | ``` 33 | location /basicwebrtc/ { 34 | resolver 127.0.0.1 valid=30s; 35 | proxy_set_header HOST $host; 36 | proxy_http_version 1.1; 37 | proxy_set_header Upgrade $http_upgrade; 38 | proxy_set_header Connection upgrade; 39 | proxy_pass http://127.0.0.1:8080/; 40 | } 41 | ``` 42 | ## STUN and TURN Configuration ## 43 | If your clients are behind firewalls you might need to setup a TURN Server so the connection can fallback to that (Connection is e2e encrypted in any case). 44 | 45 | If you have your STUN/TURN Server, isert the urls into: 46 | /iceservers.json 47 | 48 | ### Setup your own TURN Server with docker ### 49 | This setup is using COTURN inside docker. 50 | The server is listening on Ports 443 and 4433 because on many firewalls only webtraffic is allowed. So you need to set this up on a second server. 51 | 52 | If your server ip is 10.10.10.10 and your want to name it "myturnserver" run it like this: 53 | 54 | Run `docker run -d --net=host --restart=always rofl256/turnserver usernameAdmin passwordAdmin realm "10.10.10.10" "10.10.10.10" "10.10.10.10" authSecret` 55 | 56 | Don't forget to change the admin username, password and authSecret. 57 | 58 | For more configurations of this take a look at repo of the container (https://github.com/cracker0dks/turn-server-docker-image) and the COTURN repo itself: https://github.com/coturn/coturn 59 | 60 | If you have the turn server running, make a new entry into /iceservers.json 61 | ``` 62 | [ 63 | { 64 | "url": "stun:10.10.10.10:443" 65 | }, 66 | { 67 | "url": "turn:10.10.10.10:443", 68 | "turnServerCredential": "authSecret", 69 | "username": "webrtcuser" 70 | } 71 | ] 72 | ``` 73 | Change the ips and authSecret as defined on docker run. The username can be set to anything you want or leave it like this then restart the basicwebrtc server. 74 | -------------------------------------------------------------------------------- /iceservers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "stun:134.103.176.133:443" 4 | }, 5 | { 6 | "url": "stun:stun.l.google.com:19302" 7 | }, 8 | { 9 | "url": "turn:134.103.176.133:443", 10 | "turnServerCredential": "cuu3Hs2ksl39", 11 | "username": "webrtcuser" 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basicwebrtc", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "basicwebrtc", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.19.2", 13 | "socket.io": "^4.7.5" 14 | } 15 | }, 16 | "node_modules/@socket.io/component-emitter": { 17 | "version": "3.1.2", 18 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", 19 | "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" 20 | }, 21 | "node_modules/@types/cors": { 22 | "version": "2.8.17", 23 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", 24 | "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", 25 | "dependencies": { 26 | "@types/node": "*" 27 | } 28 | }, 29 | "node_modules/@types/node": { 30 | "version": "22.13.10", 31 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", 32 | "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", 33 | "dependencies": { 34 | "undici-types": "~6.20.0" 35 | } 36 | }, 37 | "node_modules/accepts": { 38 | "version": "1.3.8", 39 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 40 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 41 | "dependencies": { 42 | "mime-types": "~2.1.34", 43 | "negotiator": "0.6.3" 44 | }, 45 | "engines": { 46 | "node": ">= 0.6" 47 | } 48 | }, 49 | "node_modules/array-flatten": { 50 | "version": "1.1.1", 51 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 52 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 53 | }, 54 | "node_modules/base64id": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 57 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", 58 | "engines": { 59 | "node": "^4.5.0 || >= 5.9" 60 | } 61 | }, 62 | "node_modules/body-parser": { 63 | "version": "1.20.3", 64 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 65 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 66 | "dependencies": { 67 | "bytes": "3.1.2", 68 | "content-type": "~1.0.5", 69 | "debug": "2.6.9", 70 | "depd": "2.0.0", 71 | "destroy": "1.2.0", 72 | "http-errors": "2.0.0", 73 | "iconv-lite": "0.4.24", 74 | "on-finished": "2.4.1", 75 | "qs": "6.13.0", 76 | "raw-body": "2.5.2", 77 | "type-is": "~1.6.18", 78 | "unpipe": "1.0.0" 79 | }, 80 | "engines": { 81 | "node": ">= 0.8", 82 | "npm": "1.2.8000 || >= 1.4.16" 83 | } 84 | }, 85 | "node_modules/bytes": { 86 | "version": "3.1.2", 87 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 88 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 89 | "engines": { 90 | "node": ">= 0.8" 91 | } 92 | }, 93 | "node_modules/call-bind-apply-helpers": { 94 | "version": "1.0.2", 95 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 96 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 97 | "dependencies": { 98 | "es-errors": "^1.3.0", 99 | "function-bind": "^1.1.2" 100 | }, 101 | "engines": { 102 | "node": ">= 0.4" 103 | } 104 | }, 105 | "node_modules/call-bound": { 106 | "version": "1.0.4", 107 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 108 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 109 | "dependencies": { 110 | "call-bind-apply-helpers": "^1.0.2", 111 | "get-intrinsic": "^1.3.0" 112 | }, 113 | "engines": { 114 | "node": ">= 0.4" 115 | }, 116 | "funding": { 117 | "url": "https://github.com/sponsors/ljharb" 118 | } 119 | }, 120 | "node_modules/content-disposition": { 121 | "version": "0.5.4", 122 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 123 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 124 | "dependencies": { 125 | "safe-buffer": "5.2.1" 126 | }, 127 | "engines": { 128 | "node": ">= 0.6" 129 | } 130 | }, 131 | "node_modules/content-type": { 132 | "version": "1.0.5", 133 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 134 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 135 | "engines": { 136 | "node": ">= 0.6" 137 | } 138 | }, 139 | "node_modules/cookie": { 140 | "version": "0.7.1", 141 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 142 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 143 | "engines": { 144 | "node": ">= 0.6" 145 | } 146 | }, 147 | "node_modules/cookie-signature": { 148 | "version": "1.0.6", 149 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 150 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 151 | }, 152 | "node_modules/cors": { 153 | "version": "2.8.5", 154 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 155 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 156 | "dependencies": { 157 | "object-assign": "^4", 158 | "vary": "^1" 159 | }, 160 | "engines": { 161 | "node": ">= 0.10" 162 | } 163 | }, 164 | "node_modules/debug": { 165 | "version": "2.6.9", 166 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 167 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 168 | "dependencies": { 169 | "ms": "2.0.0" 170 | } 171 | }, 172 | "node_modules/depd": { 173 | "version": "2.0.0", 174 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 175 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 176 | "engines": { 177 | "node": ">= 0.8" 178 | } 179 | }, 180 | "node_modules/destroy": { 181 | "version": "1.2.0", 182 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 183 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 184 | "engines": { 185 | "node": ">= 0.8", 186 | "npm": "1.2.8000 || >= 1.4.16" 187 | } 188 | }, 189 | "node_modules/dunder-proto": { 190 | "version": "1.0.1", 191 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 192 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 193 | "dependencies": { 194 | "call-bind-apply-helpers": "^1.0.1", 195 | "es-errors": "^1.3.0", 196 | "gopd": "^1.2.0" 197 | }, 198 | "engines": { 199 | "node": ">= 0.4" 200 | } 201 | }, 202 | "node_modules/ee-first": { 203 | "version": "1.1.1", 204 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 205 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 206 | }, 207 | "node_modules/encodeurl": { 208 | "version": "2.0.0", 209 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 210 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 211 | "engines": { 212 | "node": ">= 0.8" 213 | } 214 | }, 215 | "node_modules/engine.io": { 216 | "version": "6.6.4", 217 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", 218 | "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", 219 | "dependencies": { 220 | "@types/cors": "^2.8.12", 221 | "@types/node": ">=10.0.0", 222 | "accepts": "~1.3.4", 223 | "base64id": "2.0.0", 224 | "cookie": "~0.7.2", 225 | "cors": "~2.8.5", 226 | "debug": "~4.3.1", 227 | "engine.io-parser": "~5.2.1", 228 | "ws": "~8.17.1" 229 | }, 230 | "engines": { 231 | "node": ">=10.2.0" 232 | } 233 | }, 234 | "node_modules/engine.io-parser": { 235 | "version": "5.2.3", 236 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", 237 | "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", 238 | "engines": { 239 | "node": ">=10.0.0" 240 | } 241 | }, 242 | "node_modules/engine.io/node_modules/cookie": { 243 | "version": "0.7.2", 244 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 245 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 246 | "engines": { 247 | "node": ">= 0.6" 248 | } 249 | }, 250 | "node_modules/engine.io/node_modules/debug": { 251 | "version": "4.3.7", 252 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 253 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 254 | "dependencies": { 255 | "ms": "^2.1.3" 256 | }, 257 | "engines": { 258 | "node": ">=6.0" 259 | }, 260 | "peerDependenciesMeta": { 261 | "supports-color": { 262 | "optional": true 263 | } 264 | } 265 | }, 266 | "node_modules/engine.io/node_modules/ms": { 267 | "version": "2.1.3", 268 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 269 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 270 | }, 271 | "node_modules/es-define-property": { 272 | "version": "1.0.1", 273 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 274 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 275 | "engines": { 276 | "node": ">= 0.4" 277 | } 278 | }, 279 | "node_modules/es-errors": { 280 | "version": "1.3.0", 281 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 282 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 283 | "engines": { 284 | "node": ">= 0.4" 285 | } 286 | }, 287 | "node_modules/es-object-atoms": { 288 | "version": "1.1.1", 289 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 290 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 291 | "dependencies": { 292 | "es-errors": "^1.3.0" 293 | }, 294 | "engines": { 295 | "node": ">= 0.4" 296 | } 297 | }, 298 | "node_modules/escape-html": { 299 | "version": "1.0.3", 300 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 301 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 302 | }, 303 | "node_modules/etag": { 304 | "version": "1.8.1", 305 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 306 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 307 | "engines": { 308 | "node": ">= 0.6" 309 | } 310 | }, 311 | "node_modules/express": { 312 | "version": "4.21.2", 313 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 314 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 315 | "dependencies": { 316 | "accepts": "~1.3.8", 317 | "array-flatten": "1.1.1", 318 | "body-parser": "1.20.3", 319 | "content-disposition": "0.5.4", 320 | "content-type": "~1.0.4", 321 | "cookie": "0.7.1", 322 | "cookie-signature": "1.0.6", 323 | "debug": "2.6.9", 324 | "depd": "2.0.0", 325 | "encodeurl": "~2.0.0", 326 | "escape-html": "~1.0.3", 327 | "etag": "~1.8.1", 328 | "finalhandler": "1.3.1", 329 | "fresh": "0.5.2", 330 | "http-errors": "2.0.0", 331 | "merge-descriptors": "1.0.3", 332 | "methods": "~1.1.2", 333 | "on-finished": "2.4.1", 334 | "parseurl": "~1.3.3", 335 | "path-to-regexp": "0.1.12", 336 | "proxy-addr": "~2.0.7", 337 | "qs": "6.13.0", 338 | "range-parser": "~1.2.1", 339 | "safe-buffer": "5.2.1", 340 | "send": "0.19.0", 341 | "serve-static": "1.16.2", 342 | "setprototypeof": "1.2.0", 343 | "statuses": "2.0.1", 344 | "type-is": "~1.6.18", 345 | "utils-merge": "1.0.1", 346 | "vary": "~1.1.2" 347 | }, 348 | "engines": { 349 | "node": ">= 0.10.0" 350 | }, 351 | "funding": { 352 | "type": "opencollective", 353 | "url": "https://opencollective.com/express" 354 | } 355 | }, 356 | "node_modules/finalhandler": { 357 | "version": "1.3.1", 358 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 359 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 360 | "dependencies": { 361 | "debug": "2.6.9", 362 | "encodeurl": "~2.0.0", 363 | "escape-html": "~1.0.3", 364 | "on-finished": "2.4.1", 365 | "parseurl": "~1.3.3", 366 | "statuses": "2.0.1", 367 | "unpipe": "~1.0.0" 368 | }, 369 | "engines": { 370 | "node": ">= 0.8" 371 | } 372 | }, 373 | "node_modules/forwarded": { 374 | "version": "0.2.0", 375 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 376 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 377 | "engines": { 378 | "node": ">= 0.6" 379 | } 380 | }, 381 | "node_modules/fresh": { 382 | "version": "0.5.2", 383 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 384 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 385 | "engines": { 386 | "node": ">= 0.6" 387 | } 388 | }, 389 | "node_modules/function-bind": { 390 | "version": "1.1.2", 391 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 392 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 393 | "funding": { 394 | "url": "https://github.com/sponsors/ljharb" 395 | } 396 | }, 397 | "node_modules/get-intrinsic": { 398 | "version": "1.3.0", 399 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 400 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 401 | "dependencies": { 402 | "call-bind-apply-helpers": "^1.0.2", 403 | "es-define-property": "^1.0.1", 404 | "es-errors": "^1.3.0", 405 | "es-object-atoms": "^1.1.1", 406 | "function-bind": "^1.1.2", 407 | "get-proto": "^1.0.1", 408 | "gopd": "^1.2.0", 409 | "has-symbols": "^1.1.0", 410 | "hasown": "^2.0.2", 411 | "math-intrinsics": "^1.1.0" 412 | }, 413 | "engines": { 414 | "node": ">= 0.4" 415 | }, 416 | "funding": { 417 | "url": "https://github.com/sponsors/ljharb" 418 | } 419 | }, 420 | "node_modules/get-proto": { 421 | "version": "1.0.1", 422 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 423 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 424 | "dependencies": { 425 | "dunder-proto": "^1.0.1", 426 | "es-object-atoms": "^1.0.0" 427 | }, 428 | "engines": { 429 | "node": ">= 0.4" 430 | } 431 | }, 432 | "node_modules/gopd": { 433 | "version": "1.2.0", 434 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 435 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 436 | "engines": { 437 | "node": ">= 0.4" 438 | }, 439 | "funding": { 440 | "url": "https://github.com/sponsors/ljharb" 441 | } 442 | }, 443 | "node_modules/has-symbols": { 444 | "version": "1.1.0", 445 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 446 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 447 | "engines": { 448 | "node": ">= 0.4" 449 | }, 450 | "funding": { 451 | "url": "https://github.com/sponsors/ljharb" 452 | } 453 | }, 454 | "node_modules/hasown": { 455 | "version": "2.0.2", 456 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 457 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 458 | "dependencies": { 459 | "function-bind": "^1.1.2" 460 | }, 461 | "engines": { 462 | "node": ">= 0.4" 463 | } 464 | }, 465 | "node_modules/http-errors": { 466 | "version": "2.0.0", 467 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 468 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 469 | "dependencies": { 470 | "depd": "2.0.0", 471 | "inherits": "2.0.4", 472 | "setprototypeof": "1.2.0", 473 | "statuses": "2.0.1", 474 | "toidentifier": "1.0.1" 475 | }, 476 | "engines": { 477 | "node": ">= 0.8" 478 | } 479 | }, 480 | "node_modules/iconv-lite": { 481 | "version": "0.4.24", 482 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 483 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 484 | "dependencies": { 485 | "safer-buffer": ">= 2.1.2 < 3" 486 | }, 487 | "engines": { 488 | "node": ">=0.10.0" 489 | } 490 | }, 491 | "node_modules/inherits": { 492 | "version": "2.0.4", 493 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 494 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 495 | }, 496 | "node_modules/ipaddr.js": { 497 | "version": "1.9.1", 498 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 499 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 500 | "engines": { 501 | "node": ">= 0.10" 502 | } 503 | }, 504 | "node_modules/math-intrinsics": { 505 | "version": "1.1.0", 506 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 507 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 508 | "engines": { 509 | "node": ">= 0.4" 510 | } 511 | }, 512 | "node_modules/media-typer": { 513 | "version": "0.3.0", 514 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 515 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 516 | "engines": { 517 | "node": ">= 0.6" 518 | } 519 | }, 520 | "node_modules/merge-descriptors": { 521 | "version": "1.0.3", 522 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 523 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 524 | "funding": { 525 | "url": "https://github.com/sponsors/sindresorhus" 526 | } 527 | }, 528 | "node_modules/methods": { 529 | "version": "1.1.2", 530 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 531 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 532 | "engines": { 533 | "node": ">= 0.6" 534 | } 535 | }, 536 | "node_modules/mime": { 537 | "version": "1.6.0", 538 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 539 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 540 | "bin": { 541 | "mime": "cli.js" 542 | }, 543 | "engines": { 544 | "node": ">=4" 545 | } 546 | }, 547 | "node_modules/mime-db": { 548 | "version": "1.52.0", 549 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 550 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 551 | "engines": { 552 | "node": ">= 0.6" 553 | } 554 | }, 555 | "node_modules/mime-types": { 556 | "version": "2.1.35", 557 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 558 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 559 | "dependencies": { 560 | "mime-db": "1.52.0" 561 | }, 562 | "engines": { 563 | "node": ">= 0.6" 564 | } 565 | }, 566 | "node_modules/ms": { 567 | "version": "2.0.0", 568 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 569 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 570 | }, 571 | "node_modules/negotiator": { 572 | "version": "0.6.3", 573 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 574 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 575 | "engines": { 576 | "node": ">= 0.6" 577 | } 578 | }, 579 | "node_modules/object-assign": { 580 | "version": "4.1.1", 581 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 582 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 583 | "engines": { 584 | "node": ">=0.10.0" 585 | } 586 | }, 587 | "node_modules/object-inspect": { 588 | "version": "1.13.4", 589 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 590 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 591 | "engines": { 592 | "node": ">= 0.4" 593 | }, 594 | "funding": { 595 | "url": "https://github.com/sponsors/ljharb" 596 | } 597 | }, 598 | "node_modules/on-finished": { 599 | "version": "2.4.1", 600 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 601 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 602 | "dependencies": { 603 | "ee-first": "1.1.1" 604 | }, 605 | "engines": { 606 | "node": ">= 0.8" 607 | } 608 | }, 609 | "node_modules/parseurl": { 610 | "version": "1.3.3", 611 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 612 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 613 | "engines": { 614 | "node": ">= 0.8" 615 | } 616 | }, 617 | "node_modules/path-to-regexp": { 618 | "version": "0.1.12", 619 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 620 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" 621 | }, 622 | "node_modules/proxy-addr": { 623 | "version": "2.0.7", 624 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 625 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 626 | "dependencies": { 627 | "forwarded": "0.2.0", 628 | "ipaddr.js": "1.9.1" 629 | }, 630 | "engines": { 631 | "node": ">= 0.10" 632 | } 633 | }, 634 | "node_modules/qs": { 635 | "version": "6.13.0", 636 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 637 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 638 | "dependencies": { 639 | "side-channel": "^1.0.6" 640 | }, 641 | "engines": { 642 | "node": ">=0.6" 643 | }, 644 | "funding": { 645 | "url": "https://github.com/sponsors/ljharb" 646 | } 647 | }, 648 | "node_modules/range-parser": { 649 | "version": "1.2.1", 650 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 651 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 652 | "engines": { 653 | "node": ">= 0.6" 654 | } 655 | }, 656 | "node_modules/raw-body": { 657 | "version": "2.5.2", 658 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 659 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 660 | "dependencies": { 661 | "bytes": "3.1.2", 662 | "http-errors": "2.0.0", 663 | "iconv-lite": "0.4.24", 664 | "unpipe": "1.0.0" 665 | }, 666 | "engines": { 667 | "node": ">= 0.8" 668 | } 669 | }, 670 | "node_modules/safe-buffer": { 671 | "version": "5.2.1", 672 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 673 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 674 | "funding": [ 675 | { 676 | "type": "github", 677 | "url": "https://github.com/sponsors/feross" 678 | }, 679 | { 680 | "type": "patreon", 681 | "url": "https://www.patreon.com/feross" 682 | }, 683 | { 684 | "type": "consulting", 685 | "url": "https://feross.org/support" 686 | } 687 | ] 688 | }, 689 | "node_modules/safer-buffer": { 690 | "version": "2.1.2", 691 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 692 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 693 | }, 694 | "node_modules/send": { 695 | "version": "0.19.0", 696 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 697 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 698 | "dependencies": { 699 | "debug": "2.6.9", 700 | "depd": "2.0.0", 701 | "destroy": "1.2.0", 702 | "encodeurl": "~1.0.2", 703 | "escape-html": "~1.0.3", 704 | "etag": "~1.8.1", 705 | "fresh": "0.5.2", 706 | "http-errors": "2.0.0", 707 | "mime": "1.6.0", 708 | "ms": "2.1.3", 709 | "on-finished": "2.4.1", 710 | "range-parser": "~1.2.1", 711 | "statuses": "2.0.1" 712 | }, 713 | "engines": { 714 | "node": ">= 0.8.0" 715 | } 716 | }, 717 | "node_modules/send/node_modules/encodeurl": { 718 | "version": "1.0.2", 719 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 720 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 721 | "engines": { 722 | "node": ">= 0.8" 723 | } 724 | }, 725 | "node_modules/send/node_modules/ms": { 726 | "version": "2.1.3", 727 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 728 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 729 | }, 730 | "node_modules/serve-static": { 731 | "version": "1.16.2", 732 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 733 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 734 | "dependencies": { 735 | "encodeurl": "~2.0.0", 736 | "escape-html": "~1.0.3", 737 | "parseurl": "~1.3.3", 738 | "send": "0.19.0" 739 | }, 740 | "engines": { 741 | "node": ">= 0.8.0" 742 | } 743 | }, 744 | "node_modules/setprototypeof": { 745 | "version": "1.2.0", 746 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 747 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 748 | }, 749 | "node_modules/side-channel": { 750 | "version": "1.1.0", 751 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 752 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 753 | "dependencies": { 754 | "es-errors": "^1.3.0", 755 | "object-inspect": "^1.13.3", 756 | "side-channel-list": "^1.0.0", 757 | "side-channel-map": "^1.0.1", 758 | "side-channel-weakmap": "^1.0.2" 759 | }, 760 | "engines": { 761 | "node": ">= 0.4" 762 | }, 763 | "funding": { 764 | "url": "https://github.com/sponsors/ljharb" 765 | } 766 | }, 767 | "node_modules/side-channel-list": { 768 | "version": "1.0.0", 769 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 770 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 771 | "dependencies": { 772 | "es-errors": "^1.3.0", 773 | "object-inspect": "^1.13.3" 774 | }, 775 | "engines": { 776 | "node": ">= 0.4" 777 | }, 778 | "funding": { 779 | "url": "https://github.com/sponsors/ljharb" 780 | } 781 | }, 782 | "node_modules/side-channel-map": { 783 | "version": "1.0.1", 784 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 785 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 786 | "dependencies": { 787 | "call-bound": "^1.0.2", 788 | "es-errors": "^1.3.0", 789 | "get-intrinsic": "^1.2.5", 790 | "object-inspect": "^1.13.3" 791 | }, 792 | "engines": { 793 | "node": ">= 0.4" 794 | }, 795 | "funding": { 796 | "url": "https://github.com/sponsors/ljharb" 797 | } 798 | }, 799 | "node_modules/side-channel-weakmap": { 800 | "version": "1.0.2", 801 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 802 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 803 | "dependencies": { 804 | "call-bound": "^1.0.2", 805 | "es-errors": "^1.3.0", 806 | "get-intrinsic": "^1.2.5", 807 | "object-inspect": "^1.13.3", 808 | "side-channel-map": "^1.0.1" 809 | }, 810 | "engines": { 811 | "node": ">= 0.4" 812 | }, 813 | "funding": { 814 | "url": "https://github.com/sponsors/ljharb" 815 | } 816 | }, 817 | "node_modules/socket.io": { 818 | "version": "4.8.1", 819 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", 820 | "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", 821 | "dependencies": { 822 | "accepts": "~1.3.4", 823 | "base64id": "~2.0.0", 824 | "cors": "~2.8.5", 825 | "debug": "~4.3.2", 826 | "engine.io": "~6.6.0", 827 | "socket.io-adapter": "~2.5.2", 828 | "socket.io-parser": "~4.2.4" 829 | }, 830 | "engines": { 831 | "node": ">=10.2.0" 832 | } 833 | }, 834 | "node_modules/socket.io-adapter": { 835 | "version": "2.5.5", 836 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", 837 | "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", 838 | "dependencies": { 839 | "debug": "~4.3.4", 840 | "ws": "~8.17.1" 841 | } 842 | }, 843 | "node_modules/socket.io-adapter/node_modules/debug": { 844 | "version": "4.3.7", 845 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 846 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 847 | "dependencies": { 848 | "ms": "^2.1.3" 849 | }, 850 | "engines": { 851 | "node": ">=6.0" 852 | }, 853 | "peerDependenciesMeta": { 854 | "supports-color": { 855 | "optional": true 856 | } 857 | } 858 | }, 859 | "node_modules/socket.io-adapter/node_modules/ms": { 860 | "version": "2.1.3", 861 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 862 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 863 | }, 864 | "node_modules/socket.io-parser": { 865 | "version": "4.2.4", 866 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", 867 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", 868 | "dependencies": { 869 | "@socket.io/component-emitter": "~3.1.0", 870 | "debug": "~4.3.1" 871 | }, 872 | "engines": { 873 | "node": ">=10.0.0" 874 | } 875 | }, 876 | "node_modules/socket.io-parser/node_modules/debug": { 877 | "version": "4.3.7", 878 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 879 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 880 | "dependencies": { 881 | "ms": "^2.1.3" 882 | }, 883 | "engines": { 884 | "node": ">=6.0" 885 | }, 886 | "peerDependenciesMeta": { 887 | "supports-color": { 888 | "optional": true 889 | } 890 | } 891 | }, 892 | "node_modules/socket.io-parser/node_modules/ms": { 893 | "version": "2.1.3", 894 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 895 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 896 | }, 897 | "node_modules/socket.io/node_modules/debug": { 898 | "version": "4.3.7", 899 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 900 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 901 | "dependencies": { 902 | "ms": "^2.1.3" 903 | }, 904 | "engines": { 905 | "node": ">=6.0" 906 | }, 907 | "peerDependenciesMeta": { 908 | "supports-color": { 909 | "optional": true 910 | } 911 | } 912 | }, 913 | "node_modules/socket.io/node_modules/ms": { 914 | "version": "2.1.3", 915 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 916 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 917 | }, 918 | "node_modules/statuses": { 919 | "version": "2.0.1", 920 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 921 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 922 | "engines": { 923 | "node": ">= 0.8" 924 | } 925 | }, 926 | "node_modules/toidentifier": { 927 | "version": "1.0.1", 928 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 929 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 930 | "engines": { 931 | "node": ">=0.6" 932 | } 933 | }, 934 | "node_modules/type-is": { 935 | "version": "1.6.18", 936 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 937 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 938 | "dependencies": { 939 | "media-typer": "0.3.0", 940 | "mime-types": "~2.1.24" 941 | }, 942 | "engines": { 943 | "node": ">= 0.6" 944 | } 945 | }, 946 | "node_modules/undici-types": { 947 | "version": "6.20.0", 948 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", 949 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" 950 | }, 951 | "node_modules/unpipe": { 952 | "version": "1.0.0", 953 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 954 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 955 | "engines": { 956 | "node": ">= 0.8" 957 | } 958 | }, 959 | "node_modules/utils-merge": { 960 | "version": "1.0.1", 961 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 962 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 963 | "engines": { 964 | "node": ">= 0.4.0" 965 | } 966 | }, 967 | "node_modules/vary": { 968 | "version": "1.1.2", 969 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 970 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 971 | "engines": { 972 | "node": ">= 0.8" 973 | } 974 | }, 975 | "node_modules/ws": { 976 | "version": "8.17.1", 977 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", 978 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", 979 | "engines": { 980 | "node": ">=10.0.0" 981 | }, 982 | "peerDependencies": { 983 | "bufferutil": "^4.0.1", 984 | "utf-8-validate": ">=5.0.2" 985 | }, 986 | "peerDependenciesMeta": { 987 | "bufferutil": { 988 | "optional": true 989 | }, 990 | "utf-8-validate": { 991 | "optional": true 992 | } 993 | } 994 | } 995 | } 996 | } 997 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basicwebrtc", 3 | "version": "1.0.0", 4 | "description": "Simple 1on1 P2P Talking example!", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"No tests needed!\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://git.cloud13.de/raphael/basicwebrtc" 12 | }, 13 | "keywords": [ 14 | "p2p", 15 | "webrtc" 16 | ], 17 | "dependencies": { 18 | "express": "^4.19.2", 19 | "socket.io": "^4.7.5" 20 | }, 21 | "author": "Cracker0dks", 22 | "license": "MIT" 23 | } 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | //------------------------------ 4 | // BASIC WEBRTC SIGNALING SERVER 5 | //------------------------------ 6 | 7 | //Define https & websocket Port 8 | const HTTP_PORT = parseInt(process.env.listen_port) > 0 ? parseInt(process.env.listen_port) : 3001; 9 | const HTTP_IP = process.env.listen_ip ? process.env.listen_ip : "0.0.0.0"; 10 | 11 | //Define API Version 12 | const API_VERSION = 1.2; 13 | 14 | //Get dummy cert files for https 15 | var fs = require('fs'); 16 | 17 | //SpinUP Webserver with socketIO 18 | var express = require('express'); 19 | var handler = express(); 20 | 21 | handler.use(express.static(__dirname + '/web', { 22 | setHeaders: function (res, path) { 23 | res.append('Access-Control-Allow-Origin', ['*']); 24 | res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 25 | res.append('Access-Control-Allow-Headers', 'Content-Type'); 26 | } 27 | })); 28 | 29 | var app = require('http').createServer(handler) 30 | 31 | var ioServer = require('socket.io')(app, { 32 | cors: { 33 | origin: function (origin, callback) { 34 | callback(null, true) // allow all origins 35 | }, 36 | credentials: false, 37 | methods: ["GET", "POST"] 38 | } 39 | }); 40 | var crypto = require('crypto'); 41 | 42 | app.listen(HTTP_PORT, HTTP_IP); 43 | 44 | var icesevers = JSON.parse(fs.readFileSync("./iceservers.json", 'utf8')); 45 | 46 | console.log("--------------------------------------------"); 47 | console.log("SIGNALINGSERVER RUNNING ON IP:PORT: " + HTTP_IP + ':' + HTTP_PORT); 48 | console.log("--------------------------------------------"); 49 | 50 | var registerdUUIDs = {}; 51 | var socketID_UUIDMatch = {}; 52 | 53 | //Listen for IO connections and do signaling 54 | ioServer.sockets.on('connection', function (socket) { 55 | socket.emit('API_VERSION', API_VERSION); 56 | 57 | let roomOfUser = null; 58 | let nameOfUser = "NA"; 59 | let MY_UUID = null; 60 | console.log("NEW USER!"); 61 | 62 | socket.on("registerUUID", function (content, callback) { 63 | const UUID = content["UUID"] || null; 64 | const UUID_KEY = content["UUID"] || null; 65 | if (UUID && UUID_KEY) { 66 | if (!registerdUUIDs[UUID] || registerdUUIDs[UUID] == UUID_KEY) { 67 | const alreadyRegistred = registerdUUIDs[UUID] == UUID_KEY; 68 | registerdUUIDs[UUID] = UUID_KEY; 69 | socketID_UUIDMatch[UUID] = socket.id; 70 | MY_UUID = UUID; 71 | callback(null, alreadyRegistred); 72 | } else { 73 | callback("UUID_KEY was not correct!") 74 | } 75 | } else { 76 | callback("UUID or UUID_KEY was empty on registerUUID!") 77 | } 78 | }); 79 | 80 | socket.on("closeConnection", function () { 81 | socket.to(roomOfUser).emit('userDiscconected', MY_UUID); 82 | }); 83 | 84 | socket.on('disconnect', function () { 85 | socket.to(roomOfUser).emit('userDiscconected', MY_UUID); 86 | }); 87 | 88 | var roomname; 89 | var username = ""; 90 | socket.on("joinRoom", function (content, callback) { 91 | roomname = content["roomname"] || ""; 92 | username = content["username"] || ""; 93 | console.log(username) 94 | if (!roomOfUser) { 95 | roomOfUser = roomname; 96 | nameOfUser = username; 97 | socket.to(roomname).emit('userJoined', { UUID: MY_UUID }); 98 | console.log("joinRoom", roomname, MY_UUID); 99 | socket.join(roomname); 100 | } 101 | }) 102 | 103 | socket.on("sendMsg", function (msg) { 104 | if (typeof (msg) == "string") { 105 | msg = msg.replace(/\\/g, "\\\\") 106 | .replace(/\$/g, "\\$") 107 | .replace(/'/g, "\\'") 108 | .replace(/"/g, "\\\""); 109 | if (msg != "") { 110 | if (username != "" && username != "NA") { 111 | msg = username + ': ' + msg; 112 | } 113 | socket.to(roomname).emit('msg', msg); 114 | socket.emit('msg', msg); 115 | } 116 | } 117 | }); 118 | 119 | socket.on("currentAudioLvl", function (currentAudioLvl) { 120 | socket.to(roomname).emit('currentAudioLvl', { currentAudioLvl: currentAudioLvl, fromUUID: MY_UUID }); 121 | }); 122 | 123 | socket.on("signaling", function (content) { 124 | var destSocketId = socketID_UUIDMatch[content.destUUID]; 125 | var signalingData = content.signalingData; 126 | 127 | ioServer.to(destSocketId).emit('signaling', { signalingData: signalingData, fromUUID: MY_UUID, username: nameOfUser }); 128 | }); 129 | 130 | //Return the current iceServers 131 | var returnIce = []; 132 | for (var i in icesevers) { 133 | if (icesevers[i].turnServerCredential) { //Generate a temp user and password with this turn server creds if given 134 | var turnCredentials = getTURNCredentials(icesevers[i].username, icesevers[i].turnServerCredential); 135 | returnIce.push({ 136 | url: icesevers[i].url, 137 | credential: turnCredentials.password, 138 | username: turnCredentials.username, 139 | }); 140 | } else { 141 | returnIce.push(icesevers[i]); 142 | } 143 | } 144 | socket.emit('currentIceServers', returnIce); 145 | }) 146 | 147 | function getTURNCredentials(name, secret) { 148 | var unixTimeStamp = parseInt((Date.now() / 1000) + "") + 12 * 3600, // this credential would be valid for the next 12 hours 149 | username = [unixTimeStamp, name].join(':'), 150 | password, 151 | hmac = crypto.createHmac('sha1', secret); 152 | hmac.setEncoding('base64'); 153 | hmac.write(username); 154 | hmac.end(); 155 | password = hmac.read(); 156 | return { 157 | username: username, 158 | password: password 159 | }; 160 | } -------------------------------------------------------------------------------- /updateserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "GOGO FOR THE LIVE BUILD!" 3 | cd /home/basicwebrtc 4 | git pull origin master 5 | docker build -t basicwebrtc . 6 | docker rm -f basicwebrtc 7 | docker run --name=basicwebrtc --net=dockernet --restart=always -d basicwebrtc 8 | echo "-<-<-< DONE ->->->" 9 | -------------------------------------------------------------------------------- /web/css/fontawesome5.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | .fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\f959"}.fa-bacterium:before{content:"\f95a"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-deezer:before{content:"\f977"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\f978"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\f979"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-rust:before{content:"\f97a"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sink:before{content:"\f96d"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-tiktok:before{content:"\f97b"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-unsplash:before{content:"\f97c"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-users-slash:before{content:"\f973"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} -------------------------------------------------------------------------------- /web/css/main.css: -------------------------------------------------------------------------------- 1 | audio { 2 | max-width: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0em; 8 | word-break: break-word; 9 | font-family: Monospace; 10 | overflow: hidden; 11 | background: rgb(43, 42, 42);; 12 | } 13 | 14 | div#container { 15 | margin: 0 auto 0 auto; 16 | max-width: 40em; 17 | padding: 0em 1.5em 1.3em 1.5em; 18 | } 19 | 20 | video { 21 | background: #222; 22 | width: 100%; 23 | } 24 | 25 | .htext { 26 | color: #fff; 27 | text-shadow: 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000, -1px 0 0 #000; 28 | position: relative; 29 | top: 24px; 30 | left: 10px; 31 | font-size: 0.9em; 32 | z-index: 10; 33 | } 34 | 35 | .userPlaceholder { 36 | width: 150px; 37 | height: 140px; 38 | border-radius: 50%; 39 | margin-left: auto; 40 | margin-right: auto; 41 | background: #8e8effa1; 42 | font-size: 8em; 43 | padding-top: 10px; 44 | color: #1a1a1a; 45 | text-align: center; 46 | position: relative; 47 | top: 25%; 48 | } 49 | 50 | ul { 51 | list-style-type: none; 52 | margin: 0; 53 | padding: 0; 54 | } 55 | 56 | ul>li { 57 | display: inline-block; 58 | margin: 0px; 59 | padding: 0; 60 | } 61 | 62 | .callBtn { 63 | padding: 10px; 64 | padding-left: 15px; 65 | padding-right: 15px; 66 | width: 45px; 67 | cursor: pointer; 68 | } 69 | 70 | .callBtn:hover { 71 | background: #ababad40; 72 | } 73 | 74 | #cancelCallBtn { 75 | color: #710000; 76 | } 77 | 78 | .fa-microphone-alt { 79 | color: #044e04; 80 | } 81 | 82 | div.screenshare-select-dialog-backdrop { 83 | position: absolute; 84 | z-index:100; 85 | background: rgba(0,0,0,0.6); 86 | width: 100%; 87 | height: 100%; 88 | } 89 | 90 | div.screenshare-select-dialog { 91 | background-color: darkslategray; 92 | border-radius: 7px; 93 | padding: 5pt; 94 | margin: auto; 95 | width: 50%; 96 | } 97 | 98 | div.screenshare-options { 99 | display: flex; 100 | flex-wrap: wrap; 101 | } 102 | 103 | div.screenshare-option { 104 | margin: 4px; 105 | padding:2px; 106 | display: flex; 107 | flex-direction: column; 108 | align-items: center; 109 | border: 1px solid transparent; 110 | max-width: 100px; 111 | } 112 | 113 | div.screenshare-option:hover { 114 | border: 1px solid grey; 115 | } 116 | 117 | div.screenshare-option > img.thumbnail { 118 | width: 100%; 119 | max-width: 90px; 120 | max-height: 90px; 121 | } 122 | 123 | #chatDiv { 124 | width: 250px; 125 | height: 330px; 126 | position: absolute; 127 | right: 20px; 128 | bottom: 130px; 129 | border-radius: 7px; 130 | background: #7b79798c; 131 | border: 1px solid #80808087; 132 | color: #c7c6c6; 133 | display: none; 134 | } 135 | 136 | #chatInputText { 137 | width: 100%; 138 | position: absolute; 139 | bottom: 0; 140 | background: #0000ff00; 141 | border: 0px; 142 | border-top: 1px solid #8080804f; 143 | outline: none; 144 | color: #bbb; 145 | padding: 5px; 146 | padding-right: 30px; 147 | } 148 | 149 | #chatSendBtn { 150 | font-size: 1em; 151 | position: absolute; 152 | bottom: 5px; 153 | right: 7px; 154 | color: #c1bebe; 155 | width: 20px; 156 | height: 17px; 157 | cursor: pointer; 158 | } 159 | 160 | #chatText { 161 | height: 303px; 162 | overflow-y: auto; 163 | padding-left: 3px; 164 | } 165 | 166 | /* SCROLLBAR */ 167 | /* width */ 168 | ::-webkit-scrollbar { 169 | width: 10px; 170 | } 171 | 172 | /* Track */ 173 | ::-webkit-scrollbar-track { 174 | background: #f1f1f1; 175 | } 176 | 177 | /* Handle */ 178 | ::-webkit-scrollbar-thumb { 179 | background: #888; 180 | } 181 | 182 | /* Handle on hover */ 183 | ::-webkit-scrollbar-thumb:hover { 184 | background: #555; 185 | } 186 | 187 | 188 | /* TV EFFECT */ 189 | 190 | 191 | div#topDiv{ 192 | width:100%; 193 | height:0%; 194 | opacity:0.9; 195 | background:black; 196 | position:absolute; 197 | top: 0%; 198 | } 199 | div#bottomDiv{ 200 | width:100%; 201 | height:0%; 202 | opacity:0.9; 203 | background:black; 204 | position:absolute; 205 | bottom: 0%; 206 | } 207 | div#centerDiv{ 208 | position:absolute; 209 | height: 1px; 210 | top: 50%; 211 | width:100%; 212 | background: white; 213 | display:none; 214 | z-index:1; 215 | } 216 | -------------------------------------------------------------------------------- /web/endcall.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
Call ended. You may close this tab/window now.
7 | 8 | -------------------------------------------------------------------------------- /web/images/picInPic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/images/picInPic.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Video Chat! 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 |
    46 |
  • 47 |
    48 |
  • 49 |
  • 50 |
    51 |
  • 52 |
  • 53 |
    54 |
  • 55 |
  • 56 |
    57 |
  • 58 |
  • 59 |
    60 |
  • 61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /web/js/ezWebRTC.js: -------------------------------------------------------------------------------- 1 | var wrtc = window; 2 | //@ts-check 3 | function initEzWebRTC(initiator, config) { 4 | var _this = this; 5 | this.isConnected = false; 6 | this.gotOffer = false; 7 | this.makingOffer = false; 8 | 9 | var rtcConfig = { //Default Values 10 | offerOptions: { 11 | offerToReceiveAudio: true, //- depricated - want audio 12 | offerToReceiveVideo: true //- depricated - want video 13 | }, 14 | stream: null, 15 | 'iceServers': [ 16 | { 17 | "urls": "stun:stun.l.google.com:19302" 18 | } 19 | ], 20 | sdpSemantics: 'unified-plan', 21 | iceConnectionTimeoutInSec: 10, 22 | preferH264Codec: false 23 | } 24 | if (config) { 25 | for (var i in config) { 26 | rtcConfig[i] = config[i]; 27 | } 28 | } 29 | 30 | //Make new peer 31 | var pc = new wrtc.RTCPeerConnection(rtcConfig); 32 | 33 | pc.onsignalingstatechange = function (event) { 34 | _this.emitEvent("onsignalingstatechange", event); 35 | } 36 | 37 | pc.onicecandidate = function (e) { 38 | if (!pc || !e || !e.candidate) return; 39 | _this.emitEvent("signaling", e.candidate) 40 | }; 41 | 42 | var knownStreams = {}; 43 | pc.ontrack = function (event) { 44 | event.streams.forEach(eventStream => { 45 | _this.emitEvent('track', event.track, eventStream); 46 | if (!knownStreams[eventStream.id]) { //emit onStream event 47 | _this.emitEvent("stream", eventStream); 48 | eventStream.onremovetrack = (event) => { 49 | _this.emitEvent('trackremoved', event.track, eventStream); 50 | let tracks = eventStream.getTracks(); 51 | if (tracks.length == 0) { //If no tracks left 52 | _this.emitEvent("streamremoved", eventStream, event.track.kind); 53 | } 54 | delete trackSenders[event.track.id] 55 | }; 56 | } 57 | knownStreams[eventStream.id] = true; 58 | }); 59 | } 60 | 61 | var iceConnectionTimeout; 62 | pc.oniceconnectionstatechange = async function (e) { 63 | //console.log('ICE state: ' + pc.iceConnectionState); 64 | if (pc.iceConnectionState == "connected" || pc.iceConnectionState == "completed") { 65 | if (iceConnectionTimeout) { clearTimeout(iceConnectionTimeout); }; 66 | if (!_this.isConnected) { 67 | _this.isConnected = true; 68 | _this.emitEvent("connect", true) 69 | } 70 | } else if (pc.iceConnectionState == 'disconnected') { 71 | setTimeout(async function () { //lets wait if connection switches back to connected in a few seconds 72 | if (_this.isConnected && pc.iceConnectionState == "disconnected" && initiator) { //if still in disconnected and not closed state try to restart ice 73 | //console.log("Try to recover ice connection form disconnected state!") 74 | await pc.setLocalDescription(await pc.createOffer({ iceRestart: true })); 75 | _this.emitEvent("signaling", pc.localDescription); 76 | } 77 | }, 3000); 78 | iceConnectionTimeout = setTimeout(function () { //Close the connection if state not changes to connected in a given time 79 | if (_this.isConnected) { 80 | _this.isConnected = false; 81 | _this.emitEvent("closed", true) 82 | } 83 | }, rtcConfig.iceConnectionTimeoutInSec * 1000); 84 | } else if (pc.iceConnectionState == 'closed') { 85 | if (_this.isConnected) { 86 | _this.isConnected = false; 87 | _this.emitEvent("closed", true) 88 | } 89 | } else if (pc.iceConnectionState == 'failed' && initiator) { //Try to reconnect from initator side 90 | //console.log("Try to recover ice connection form failed state!") 91 | await pc.setLocalDescription(await pc.createOffer({ iceRestart: true })) 92 | _this.emitEvent("signaling", pc.localDescription) 93 | _this.emitEvent("iceFailed", true) 94 | } 95 | }; 96 | 97 | pc.onnegotiationneeded = function () { 98 | negotiate(); 99 | } 100 | 101 | this.signaling = async function (signalData) { //Handle signaling 102 | if (signalData == "renegotiate" && initiator) { //Got renegotiate request, so do it 103 | negotiate(); 104 | } else if (signalData && signalData.type == "offer") { //Got an offer -> Create Answer) 105 | _this.gotOffer = true; 106 | if (pc.signalingState != "stable") { //If not stable ask for renegotiation 107 | await Promise.all([ 108 | pc.setLocalDescription({ type: "rollback" }), //Be polite 109 | await pc.setRemoteDescription(new wrtc.RTCSessionDescription(signalData)) 110 | ]); 111 | } else { 112 | await pc.setRemoteDescription(new wrtc.RTCSessionDescription(signalData)) 113 | } 114 | await pc.setLocalDescription(await pc.createAnswer(rtcConfig.offerOptions)); 115 | var a_desc = pc.localDescription; 116 | a_desc.sdp = rtcConfig.preferH264Codec ? preferH264Codec(a_desc.sdp) : a_desc.sdp; 117 | a_desc.sdp = removeDoubleSSRC(a_desc.sdp); 118 | _this.emitEvent("signaling", a_desc) 119 | if (!initiator) 120 | requestMissingTransceivers() 121 | } else if (signalData && signalData.type == "answer" && initiator) { //Initiator: Setting answer and starting connection 122 | _this.makingOffer = false; 123 | pc.setRemoteDescription(new wrtc.RTCSessionDescription(signalData)) 124 | } else if (signalData && signalData.type == "transceive" && initiator) { //Got an request to transrecive 125 | _this.addTransceiver(signalData.kind, signalData.init) 126 | } else if (signalData && signalData.candidate) { //is a icecandidate thing 127 | pc.addIceCandidate(new wrtc.RTCIceCandidate(signalData)); 128 | } else { 129 | console.log("Some unused signaling data???", signalData) 130 | } 131 | } 132 | 133 | var trackSenders = {}; 134 | this.addStream = function (stream) { 135 | stream.getTracks().forEach(track => { 136 | trackSenders[track.id] = pc.addTrack(track, stream); //Add all tracks to pc 137 | }) 138 | } 139 | 140 | this.removeStream = function (stream) { 141 | stream.getTracks().forEach(track => { 142 | pc.removeTrack(trackSenders[track.id]) 143 | }); 144 | } 145 | 146 | this.addTrack = function (track, stream) { 147 | pc.addTrack(track, stream); 148 | } 149 | 150 | this.removeTrack = function (track) { 151 | pc.removeTrack(trackSenders[track.id]) 152 | } 153 | 154 | this.addTransceiver = function (kind, init) { 155 | if (initiator) { 156 | try { 157 | pc.addTransceiver(kind, init) 158 | } catch (err) { 159 | console.log("addTransceiver Error", err) 160 | _this.destroy() 161 | } 162 | } else { 163 | _this.emitEvent("signaling", { // request initiator add a transceiver 164 | type: "transceive", 165 | kind: kind, 166 | init: init 167 | }) 168 | } 169 | } 170 | 171 | this.destroy = function () { 172 | pc.close(); 173 | pc.oniceconnectionstatechange = null 174 | pc.onicegatheringstatechange = null 175 | pc.onsignalingstatechange = null 176 | pc.onicecandidate = null 177 | pc.ontrack = null 178 | _this.isConnected = false; 179 | } 180 | 181 | if (rtcConfig.stream) { 182 | this.addStream(rtcConfig.stream); //Add stream at start, this will trigger negotiation 183 | } else if (initiator) { //start negotiation if we are initiator anyway if we have no stream 184 | negotiate(); 185 | } 186 | 187 | async function negotiate() { 188 | if (_this.makingOffer) //Dont make an offer twice before answer is received 189 | return; 190 | //console.log("negotiate", initiator) 191 | if (initiator) { 192 | _this.makingOffer = true; 193 | const offer = await pc.createOffer(rtcConfig.offerOptions); //Create offer 194 | if (pc.signalingState != "stable") return; 195 | await pc.setLocalDescription(offer); 196 | var o_desc = pc.localDescription; 197 | o_desc.sdp = rtcConfig.preferH264Codec ? preferH264Codec(o_desc.sdp) : o_desc.sdp; 198 | _this.emitEvent("signaling", o_desc) 199 | } else if (_this.gotOffer) { //Dont send renegotiate req before getting at least one offer 200 | _this.emitEvent("signaling", "renegotiate"); 201 | } 202 | } 203 | 204 | function requestMissingTransceivers() { 205 | if (pc.getTransceivers) { 206 | try { 207 | pc.getTransceivers().forEach(transceiver => { 208 | if (!transceiver.mid && transceiver.sender.track && !transceiver.requested) { 209 | transceiver.requested = true // HACK: Safari returns negotiated transceivers with a null mid 210 | _this.addTransceiver(transceiver.sender.track.kind) 211 | } 212 | }) 213 | } catch (e) { 214 | console.log("Faild to add transriver!", e) 215 | } 216 | } 217 | } 218 | 219 | this.mappedEvents = {}; 220 | this.on = function (eventname, callback) { 221 | if (_this.mappedEvents[eventname]) { 222 | _this.mappedEvents[eventname].push(callback) 223 | } else { 224 | _this.mappedEvents[eventname] = [callback]; 225 | } 226 | }; 227 | 228 | this.emitEvent = function (eventname) { 229 | for (var i in this.mappedEvents[eventname]) { 230 | _this.mappedEvents[eventname][i](arguments[1], arguments[2], arguments[3]) 231 | } 232 | }; 233 | return this; 234 | } 235 | 236 | function preferH264Codec(sdp) { 237 | var lineSplit = sdp.split("\n") 238 | console.log(lineSplit) 239 | var videoLinesIndexs = []; 240 | var h264ids = []; 241 | for (var i in lineSplit) { 242 | if (lineSplit[i].startsWith("m=video")) { //find the video line 243 | videoLinesIndexs.push(i); 244 | } else if (lineSplit[i].startsWith("a=rtpmap")) { //find all codec lines 245 | if (lineSplit[i].indexOf("H264") !== -1) { 246 | h264ids.push(lineSplit[i].split("rtpmap:")[1].split(" ")[0]) 247 | } 248 | } 249 | } 250 | 251 | for (var k in videoLinesIndexs) { 252 | var videoLineIndex = videoLinesIndexs[k]; 253 | for (var i = h264ids.length; i--; i >= 0) { //Change codec order 254 | var h264id = h264ids[i]; 255 | lineSplit[videoLineIndex] = lineSplit[videoLineIndex].replace(" " + h264id, "") 256 | lineSplit[videoLineIndex] = lineSplit[videoLineIndex].replace("SAVPF ", "SAVPF " + h264id + " ") 257 | } 258 | } 259 | return lineSplit.join("\n"); 260 | } 261 | 262 | function removeDoubleSSRC(sdp) { 263 | var lineSplit = sdp.split("\n"); 264 | var mediaWithSSRC = null; 265 | var mediaCnt = 0; 266 | var readyToRemove = false; 267 | var res = []; 268 | for (var i in lineSplit) { 269 | if (lineSplit[i].startsWith("m=")) { //find the video line 270 | mediaCnt++; 271 | } 272 | if (lineSplit[i].startsWith("a=ssrc")) { //find the video line 273 | if (!mediaWithSSRC) { 274 | mediaWithSSRC = mediaCnt; 275 | } 276 | if (mediaWithSSRC < mediaCnt) { 277 | readyToRemove = true; 278 | } 279 | } 280 | if (readyToRemove && lineSplit[i].startsWith("a=ssrc")) { 281 | //Do nothing 282 | } else { 283 | res.push(lineSplit[i]); 284 | } 285 | } 286 | return res.join("\n"); 287 | } 288 | 289 | function calcCurrentVolumeLevel(stream, callback) { //Returns audio levels for audio stream from 0 - silent; to 2 loud 290 | //Calc the current volume! 291 | var audioAontext = window.AudioContext || window.webkitAudioContext; 292 | var context = new audioAontext(); 293 | var microphone = context.createMediaStreamSource(stream); 294 | var dest = context.createMediaStreamDestination(); 295 | 296 | gainNode = context.createGain(); 297 | 298 | var analyser = context.createAnalyser(); 299 | analyser.fftSize = 2048; 300 | var bufferLength = analyser.frequencyBinCount; 301 | var dataArray = new Uint8Array(bufferLength); 302 | analyser.getByteTimeDomainData(dataArray); 303 | 304 | var audioVolume = 0; 305 | var oldAudioVolume = 0; 306 | function calcVolume() { 307 | requestAnimationFrame(calcVolume); 308 | analyser.getByteTimeDomainData(dataArray); 309 | var mean = 0; 310 | for (var i = 0; i < dataArray.length; i++) { 311 | mean += Math.abs(dataArray[i] - 127); 312 | } 313 | mean /= dataArray.length; 314 | mean = Math.round(mean); 315 | if (mean < 1.2) 316 | audioVolume = 0; 317 | else if (mean < 2.5) 318 | audioVolume = 1; 319 | else 320 | audioVolume = 2; 321 | 322 | if (audioVolume != oldAudioVolume) { 323 | callback(audioVolume); 324 | oldAudioVolume = audioVolume; 325 | } 326 | } 327 | calcVolume(); 328 | microphone.connect(gainNode); 329 | gainNode.connect(analyser); //get sound 330 | analyser.connect(dest); 331 | } -------------------------------------------------------------------------------- /web/js/main.js: -------------------------------------------------------------------------------- 1 | const API_VERSION = 1.2; 2 | 3 | const MY_UUID = uuidv4(); 4 | const MY_UUID_KEY = uuidv4(); 5 | 6 | var subdir = window.location.pathname.endsWith("/") ? window.location.pathname : window.location.pathname + "/"; 7 | 8 | var base64Domain = getUrlParam("base64domain", false); 9 | 10 | //ALL # PARAMETERS 11 | var socketDomain = getUrlParam("socketdomain", false); //Domainname with path 12 | var camOnAtStart = getUrlParam("camon", false) == false ? false : true; //Defines if cam should be on at start (On is default) 13 | var username = getUrlParam("username", "NA"); 14 | var roomname = getUrlParam("roomname", false); 15 | 16 | if (!roomname) { 17 | roomname = "r" + Math.random().toString().replace(".", "") 18 | window.location = location.href + "#roomname=" + roomname 19 | } 20 | 21 | if (base64Domain && socketDomain) { 22 | socketDomain = atob(socketDomain); 23 | } 24 | 25 | var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 26 | if (isMobile) { //No Screenshare on mobile devices 27 | $("#screenBtnContainer").hide(); 28 | $("#mediaControll").css({ width: "270px" }) 29 | } 30 | 31 | const SocketIO_Options = { withCredentials: false } 32 | 33 | var socket; 34 | if (socketDomain) { 35 | socketDomain = socketDomain.replace('https://', '').replace('http://', '').split("#")[0]; 36 | var domainSplit = socketDomain.split('/'); 37 | socketDomain = 'https://' + domainSplit[0]; 38 | domainSplit.shift(); 39 | subdir = '/' + domainSplit.join('/'); 40 | subdir = subdir.endsWith('/') ? subdir : subdir + '/'; 41 | console.log('socketDomain', socketDomain); 42 | console.log('subdir', subdir); 43 | socket = io(socketDomain, { "path": subdir + "socket.io", ...SocketIO_Options }) 44 | } else { 45 | socket = subdir == "/" ? io("", SocketIO_Options) : io("", { "path": subdir + "/socket.io", ...SocketIO_Options }); //Connect to socketIi even on subpaths 46 | } 47 | 48 | var webRTCConfig = {}; 49 | 50 | var allUserStreams = {}; 51 | var pcs = {}; //Peer connections to all remotes 52 | var socketConnected = false; 53 | var micMuted = false; 54 | var camActive = false; 55 | var screenActive = false; 56 | var chatActive = false; 57 | 58 | socket.on("msg", function (msg) { 59 | var msg = msg.replace(/((.*)<\/a>)?/gi, function () { //Replace link in text with real link 60 | return '' + (arguments[7] || arguments[2]) + '' 61 | }); 62 | $("#chatText").append(`
${msg}
`) 63 | $("#chatText").find("a").attr("target", "_blank") 64 | $("#chatText").animate({ scrollTop: $("#chatText")[0].scrollHeight }, 1); 65 | if (!$("#chatText").is(":visible")) { 66 | $("#addRemoveChatBtn").css({ "color": "#730303" }); 67 | } 68 | }) 69 | 70 | socket.on('connect_failed', function () { 71 | alert("Connection to socketserver failed! Please check the logs!") 72 | socketConnected = false; 73 | }); 74 | 75 | socket.on("connect", function () { 76 | socketConnected = true; 77 | 78 | socket.on("currentIceServers", function (newIceServers) { 79 | console.log("got newIceServers", newIceServers) 80 | webRTCConfig["iceServers"] = newIceServers; 81 | }) 82 | 83 | socket.emit("registerUUID", { "UUID": MY_UUID, "UUID_KEY": MY_UUID_KEY }, function (err, alreadyRegistered) { 84 | if (err) { 85 | return console.log(err) 86 | } 87 | 88 | if (alreadyRegistered) { 89 | return console.log("We are alreadyregistered so don't do it again!") 90 | } 91 | 92 | socket.on("API_VERSION", function (serverAPI_VERSION) { 93 | if (API_VERSION != serverAPI_VERSION) { 94 | alert("SERVER has a different API Version (Client: v" + API_VERSION + " Server: v" + serverAPI_VERSION + ")! This can cause problems, so be warned!") 95 | } 96 | }) 97 | 98 | socket.on("signaling", function (data) { 99 | var signalingData = data.signalingData; 100 | var fromUUID = data.fromUUID; 101 | if (!pcs[fromUUID]) { 102 | createRemoteSocket(false, fromUUID) 103 | } 104 | pcs[fromUUID].signaling(signalingData); 105 | 106 | if (data.username) { 107 | allUserStreams[fromUUID] = allUserStreams[fromUUID] ? allUserStreams[fromUUID] : {}; 108 | allUserStreams[fromUUID]["username"] = data.username; 109 | } 110 | }) 111 | 112 | socket.on("userJoined", function (content) { 113 | var userUUID = content["UUID"] || null; 114 | createRemoteSocket(true, userUUID) 115 | }) 116 | 117 | socket.on("currentAudioLvl", function (content) { 118 | var fromUUID = content["fromUUID"] || null; 119 | let currentAudioLvl = content["currentAudioLvl"] || 0; 120 | 121 | $("#" + fromUUID).find(".audioMuted").remove(); 122 | 123 | if (currentAudioLvl < 0) { //Muted 124 | $("#" + fromUUID).append('
') 125 | } else { 126 | let perCent = currentAudioLvl * 50; 127 | $("#" + fromUUID).find(".userPlaceholder").css({ "border": "2px solid rgb(255 255 255 / " + perCent + "%)" }); 128 | } 129 | }) 130 | 131 | 132 | 133 | socket.on("userDiscconected", function (userUUID) { 134 | delete allUserStreams[userUUID]; 135 | $('audio' + userUUID).remove(); 136 | updateUserLayout(); 137 | }) 138 | 139 | if (camOnAtStart) { 140 | navigator.getUserMedia({ 141 | video: true, 142 | audio: true 143 | }, function (stream) { //OnSuccess 144 | startUserMedia() 145 | }, function (error) { //OnError 146 | console.log('getUserMedia error! Got this error: ', error); 147 | }); 148 | } else { 149 | startUserMedia() 150 | } 151 | 152 | function startUserMedia() { 153 | navigator.getUserMedia({ 154 | video: false, // { 'facingMode': "user" } 155 | audio: { 'echoCancellation': true, 'noiseSuppression': true } 156 | }, function (stream) { //OnSuccess 157 | webRTCConfig["stream"] = stream; 158 | console.log('getUserMedia success! Stream: ', stream); 159 | 160 | var audioTracks = stream.getAudioTracks(); 161 | 162 | if (audioTracks.length >= 1) { 163 | allUserStreams[MY_UUID] = { 164 | audiostream: stream, 165 | username: username 166 | } 167 | } 168 | 169 | if (audioTracks.length > 0) { 170 | console.log('Using audio device: ' + audioTracks[0].label); 171 | calcCurrentVolumeLevel(stream, function (currentAudioLvl) { 172 | if (!micMuted) { 173 | socket.emit('currentAudioLvl', currentAudioLvl); 174 | var fromUUID = MY_UUID || null; 175 | let perCent = currentAudioLvl * 50; 176 | $("#" + fromUUID).find(".userPlaceholder").css({ "border": "2px solid rgb(255 255 255 / " + perCent + "%)" }); 177 | } 178 | }); 179 | } 180 | 181 | joinRoom(); 182 | updateUserLayout(); 183 | if (camOnAtStart) { //enable cam on start if set 184 | setTimeout(function () { 185 | $("#addRemoveCameraBtn").click(); 186 | }, 1000) 187 | } 188 | 189 | }, function (error) { //OnError 190 | alert("Could not get your Mic! You need at least one Mic!") 191 | console.log('getUserMedia error! Got this error: ', error); 192 | }); 193 | } 194 | }) 195 | }); 196 | 197 | $(window).on("beforeunload", function () { 198 | if (socketConnected) { 199 | socket.emit('closeConnection', null); 200 | } 201 | }) 202 | 203 | document.addEventListener('keydown', ev => { 204 | if (ev.key === "Escape") { 205 | const screenshareDialog = document.querySelector('div.screenshare-select-dialog-backdrop') 206 | if (!screenshareDialog.hidden) { 207 | const cancelButton = screenshareDialog.querySelector('#cancel-screenshare-select') 208 | cancelButton.click() 209 | } 210 | } 211 | }) 212 | 213 | $(document).ready(function () { 214 | $("#muteUnmuteMicBtn").click(function () { 215 | if (!micMuted) { 216 | $("#muteUnmuteMicBtn").html(''); 217 | if (allUserStreams[MY_UUID] && allUserStreams[MY_UUID]["audiostream"]) { 218 | allUserStreams[MY_UUID]["audiostream"].getAudioTracks()[0].enabled = false; 219 | socket.emit('currentAudioLvl', -1); 220 | } 221 | } else { 222 | $("#muteUnmuteMicBtn").html(''); 223 | if (allUserStreams[MY_UUID] && allUserStreams[MY_UUID]["audiostream"]) { 224 | allUserStreams[MY_UUID]["audiostream"].getAudioTracks()[0].enabled = true; 225 | } 226 | } 227 | micMuted = !micMuted; 228 | }) 229 | 230 | $("#addRemoveChatBtn").click(function () { 231 | 232 | if (chatActive) { 233 | $("#chatDiv").hide(); 234 | $("#addRemoveChatBtn").css({ color: "black" }); 235 | chatActive = false; 236 | } else { 237 | $("#addRemoveChatBtn").css({ color: "#030356" }); 238 | $("#chatDiv").show(); 239 | chatActive = true; 240 | $("#chatInputText").focus(); 241 | } 242 | 243 | }) 244 | 245 | $("#chatSendBtn").click(function () { 246 | sendMsg() 247 | }) 248 | 249 | $(document).on('keypress', function (e) { 250 | if (e.which == 13 && $("#chatSendBtn").is(":visible")) { 251 | sendMsg(); 252 | } 253 | }); 254 | 255 | function sendMsg() { 256 | console.log("send") 257 | let msg = $("#chatInputText").val().trim(); 258 | socket.emit('sendMsg', msg); 259 | $("#chatInputText").val("") 260 | } 261 | 262 | $("#addRemoveScreenBtn").click(async function () { 263 | if (camActive) { 264 | $("#addRemoveCameraBtn").click(); 265 | } 266 | if (!screenActive) { 267 | $("#addRemoveScreenBtn").css({ color: "#030356" }); 268 | 269 | var config = { 270 | screen: true 271 | }; 272 | 273 | try { 274 | if (window.x_extended && window.x_extended.desktopCapturer) { 275 | var desktopCapturer = window.x_extended.desktopCapturer; 276 | desktopCapturer.getSources({ types: ['window', 'screen'] }).then(async sources => { 277 | try { 278 | const sourceid = await electron_select_screen_to_share(sources) 279 | const source = sources.find(({ id }) => id == sourceid) 280 | const stream = await navigator.mediaDevices.getUserMedia({ 281 | audio: false, 282 | video: { 283 | mandatory: { 284 | chromeMediaSource: 'desktop', 285 | chromeMediaSourceId: source.id 286 | } 287 | } 288 | }) 289 | handleScreenStream(stream) 290 | } catch (e) { 291 | console.error(e) 292 | handleError(e) 293 | } 294 | }) 295 | 296 | } else { 297 | stream = await _startScreenCapture(); 298 | handleScreenStream(stream) 299 | } 300 | 301 | async function handleScreenStream(stream) { 302 | for (var i in pcs) { //Add stream to all peers 303 | pcs[i].addStream(stream); 304 | } 305 | 306 | console.log('getUserMedia success! Stream: ', stream); 307 | console.log('LocalStream', stream.getVideoTracks()); 308 | 309 | var videoTracks = stream.getVideoTracks(); 310 | 311 | if (videoTracks.length >= 1) { 312 | allUserStreams[MY_UUID] = allUserStreams[MY_UUID] ? allUserStreams[MY_UUID] : {}; 313 | allUserStreams[MY_UUID]["videostream"] = stream; 314 | } 315 | 316 | if (videoTracks.length > 0) { 317 | console.log('Using video device: ' + videoTracks[0].label); 318 | } 319 | screenActive = true; 320 | updateUserLayout(); 321 | } 322 | 323 | } catch (e) { 324 | console.log('getUserMedia error! Got this error: ', e); 325 | alert("Could not get your Screen!") 326 | $("#addRemoveScreenBtn").css({ color: "black" }); 327 | return; 328 | } 329 | 330 | function _startScreenCapture() { 331 | if (navigator.getDisplayMedia) { 332 | return navigator.getDisplayMedia(config); 333 | } else if (navigator.mediaDevices.getDisplayMedia) { 334 | return navigator.mediaDevices.getDisplayMedia(config); 335 | } else { 336 | return navigator.mediaDevices.getUserMedia(config); 337 | } 338 | } 339 | 340 | } else { 341 | $("#addRemoveScreenBtn").css({ color: "black" }); 342 | for (var i in pcs) { //remove stream from all peers 343 | pcs[i].removeStream(allUserStreams[MY_UUID]["videostream"]); 344 | } 345 | const tracks = allUserStreams[MY_UUID]["videostream"].getVideoTracks() 346 | tracks.forEach(track => track.stop()) 347 | delete allUserStreams[MY_UUID]["videostream"]; 348 | socket.emit('removeCamera', true) 349 | updateUserLayout(); 350 | screenActive = false; 351 | } 352 | }); 353 | 354 | $("#addRemoveCameraBtn").click(function () { 355 | if (screenActive) { 356 | $("#addRemoveScreenBtn").click(); 357 | } 358 | 359 | 360 | if (!camActive) { 361 | $("#addRemoveCameraBtn").css({ color: "#030356" }); 362 | navigator.getUserMedia({ 363 | video: { 'facingMode': "user" }, 364 | audio: false 365 | }, function (stream) { //OnSuccess 366 | for (var i in pcs) { //Add stream to all peers 367 | pcs[i].addStream(stream); 368 | } 369 | 370 | console.log('getUserMedia success! Stream: ', stream); 371 | console.log('LocalStream', stream.getVideoTracks()); 372 | 373 | var videoTracks = stream.getVideoTracks(); 374 | 375 | if (videoTracks.length >= 1) { 376 | allUserStreams[MY_UUID] = allUserStreams[MY_UUID] ? allUserStreams[MY_UUID] : {}; 377 | allUserStreams[MY_UUID]["videostream"] = stream; 378 | } 379 | 380 | if (videoTracks.length > 0) { 381 | console.log('Using video device: ' + videoTracks[0].label); 382 | } 383 | 384 | updateUserLayout(); 385 | camActive = true; 386 | }, function (error) { //OnError 387 | alert("Could not get your Camera! Be sure you have one connected and it is not used by any other process!") 388 | console.log('getUserMedia error! Got this error: ', error); 389 | $("#addRemoveCameraBtn").css({ color: "black" }); 390 | }); 391 | } else { 392 | $("#addRemoveCameraBtn").css({ color: "black" }); 393 | for (var i in pcs) { //remove stream from all peers 394 | pcs[i].removeStream(allUserStreams[MY_UUID]["videostream"]); 395 | } 396 | if (allUserStreams[MY_UUID]["videostream"]) { 397 | 398 | const tracks = allUserStreams[MY_UUID]["videostream"].getVideoTracks() 399 | tracks.forEach(track => track.stop()) 400 | } 401 | delete allUserStreams[MY_UUID]["videostream"]; 402 | socket.emit('removeCamera', true) 403 | updateUserLayout(); 404 | camActive = false; 405 | } 406 | }); 407 | 408 | 409 | $("#cancelCallBtn").click(function () { 410 | if (window.x_extended && typeof window.x_extended.close === "function") { //Close window if we run in electron app 411 | window.x_extended.close() 412 | } else { 413 | $('body').append('
'); 414 | $('body').append('
'); 415 | $('body').append('
'); 416 | 417 | $('div#topDiv').animate({ 418 | //51% for chrome 419 | height: "50%" 420 | , opacity: 1 421 | }, 500); 422 | $('div#bottomDiv').animate({ 423 | //51% for chrome 424 | height: "50%" 425 | , opacity: 1 426 | }, 500, function () { 427 | $('div#centerDiv').css({ display: "block" }).animate({ 428 | width: "0%", 429 | left: "50%" 430 | }, 400, function () { 431 | setTimeout(function () { 432 | location = "./endcall.html"; 433 | }, 500) 434 | }); 435 | } 436 | ); 437 | } 438 | }) 439 | }) 440 | 441 | //This is where the WEBRTC Magic happens!!! 442 | function createRemoteSocket(initiator, UUID) { 443 | pcs[UUID] = new initEzWebRTC(initiator, webRTCConfig); //initiator 444 | pcs[UUID].on("signaling", function (data) { 445 | socket.emit("signaling", { destUUID: UUID, signalingData: data }) 446 | }) 447 | pcs[UUID].on("stream", function (stream) { 448 | gotRemoteStream(stream, UUID) 449 | }); 450 | pcs[UUID].on("streamremoved", function (stream, kind) { 451 | console.log("STREAMREMOVED!") 452 | if (kind == "video") { 453 | delete allUserStreams[UUID]["videostream"]; 454 | updateUserLayout(); 455 | } 456 | }); 457 | pcs[UUID].on("closed", function (stream) { 458 | delete allUserStreams[UUID]; 459 | $('audio' + UUID).remove(); 460 | updateUserLayout(); 461 | console.log("disconnected!"); 462 | }); 463 | pcs[UUID].on("connect", function () { 464 | if (allUserStreams[MY_UUID]["videostream"]) { 465 | setTimeout(function () { 466 | pcs[UUID].addStream(allUserStreams[MY_UUID]["videostream"]) 467 | }, 500) 468 | } 469 | }); 470 | pcs[UUID].on("iceFailed", function () { 471 | console.log("Error: Ice failed to to UUID: ", UUID); 472 | }); 473 | } 474 | 475 | function gotRemoteStream(stream, UUID) { 476 | var videoTracks = stream.getVideoTracks(); 477 | var audioTracks = stream.getAudioTracks(); 478 | 479 | console.log("videoTracks", videoTracks) 480 | console.log("audioTracks", audioTracks) 481 | 482 | $("#" + UUID).remove(); 483 | allUserStreams[UUID] = allUserStreams[UUID] ? allUserStreams[UUID] : {}; 484 | if (videoTracks.length >= 1) { //Videosteam 485 | allUserStreams[UUID]["videostream"] = stream; 486 | } else { 487 | allUserStreams[UUID]["audiostream"] = stream; 488 | } 489 | 490 | updateUserLayout(); 491 | }; 492 | 493 | function updateUserLayout() { 494 | if (document.fullscreenElement) { //Dont do things on fullscreen 495 | return; 496 | } 497 | var streamCnt = 0; 498 | var allUserDivs = {}; 499 | for (var i in allUserStreams) { 500 | var userStream = allUserStreams[i]; 501 | streamCnt++; 502 | 503 | var uDisplay = userStream["username"] && userStream["username"] != "NA" ? userStream["username"].substr(0, 2).toUpperCase() : i.substr(0, 2).toUpperCase(); 504 | var userDiv = $('
' + 505 | '
' + 506 | '
' + uDisplay + '
' + 507 | '
' + 508 | '
') 509 | 510 | if (userStream["audiostream"] && i !== MY_UUID) { 511 | if ($("#audioStreams").find('#audio' + i).length == 0) { 512 | let audioDiv = $(''); 513 | audioDiv.find("audio")[0].srcObject = userStream["audiostream"]; 514 | $("#audioStreams").append(audioDiv); 515 | } 516 | } 517 | 518 | if (userStream["videostream"]) { 519 | var mirrorStyle = "" 520 | if (i == MY_UUID && !screenActive) { 521 | mirrorStyle = "transform: scaleX(-1);" 522 | } 523 | var userDisplayName = userStream["username"] && userStream["username"] != "NA" ? (userStream["username"].charAt(0).toUpperCase() + userStream["username"].slice(1)) : i.substr(0, 2).toUpperCase(); 524 | userDiv.append(`
525 |
526 |
527 | ${userDisplayName} 528 |
529 | 530 | 533 |
534 |
`); 535 | userDiv.find("video")[0].srcObject = userStream["videostream"]; 536 | userDiv.find(".userPlaceholderContainer").hide(); 537 | 538 | if (mirrorStyle != "" || !document.pictureInPictureEnabled) { 539 | userDiv.find(".pipBtn").hide(); 540 | } 541 | 542 | userDiv.find(".pipBtn").click(function () { 543 | if (document.pictureInPictureElement) { 544 | document.exitPictureInPicture(); 545 | } else { 546 | if (document.pictureInPictureEnabled) { 547 | userDiv.find("video")[0].requestPictureInPicture(); 548 | } 549 | } 550 | }); 551 | 552 | userDiv.on 553 | 554 | if (i != MY_UUID) { 555 | userDiv.find("video").css({ "cursor": "pointer" }) 556 | userDiv.find("video").click(function () { 557 | openFullscreen(this); 558 | }) 559 | } 560 | } 561 | 562 | allUserDivs[i] = userDiv; 563 | } 564 | 565 | $("#mediaDiv").empty(); 566 | 567 | if (streamCnt == 2) { //Display 2 users side by side 568 | for (var i in allUserDivs) { 569 | if (i == MY_UUID) { 570 | allUserDivs[i].css({ width: '20%', height: '30%', position: 'absolute', left: '20px', bottom: '30px', 'z-index': '1' }); 571 | } else { 572 | allUserDivs[i].css({ width: '100%', height: '100%', float: 'left' }); 573 | } 574 | 575 | $("#mediaDiv").append(allUserDivs[i]) 576 | } 577 | 578 | } else { 579 | var lineCnt = Math.round(Math.sqrt(streamCnt)); 580 | for (var i = 1; i < lineCnt + 1; i++) { 581 | $("#mediaDiv").append('
') 582 | } 583 | let userPerLine = streamCnt <= 2 ? 1 : Math.ceil(streamCnt / lineCnt); 584 | let cucnt = 1; 585 | for (var i in allUserDivs) { 586 | var cLineNr = Math.ceil(cucnt / userPerLine); 587 | allUserDivs[i].css({ width: 100 / userPerLine + '%', height: 100 / lineCnt + '%', float: 'left' }); 588 | $("#line" + cLineNr).append(allUserDivs[i]) 589 | cucnt++; 590 | } 591 | 592 | var lastLineElsCnt = $("#line" + lineCnt).find(".videoplaceholder").length; 593 | // console.log(lastLineElsCnt, userPerLine) 594 | if (lastLineElsCnt != userPerLine) { 595 | var p = (100 / userPerLine) / 2; 596 | $("#line" + lineCnt).find(".videoplaceholder").css({ "left": p + "%" }) 597 | } 598 | } 599 | 600 | $.each($(".userCont"), function () { 601 | var w = $(this).width(); 602 | var h = $(this).height(); 603 | $(this).find("video").css({ "max-width": w + 'px', "max-height": h + 'px' }) 604 | $(this).find("video")[0].play(); 605 | }) 606 | } 607 | 608 | function joinRoom() { 609 | //Only join the room if local media is active! 610 | var roomname = getUrlParam("roomname", "unknown"); 611 | socket.emit("joinRoom", { roomname: roomname, username: username }, function () { 612 | console.log("joined room", roomname) 613 | }); 614 | } 615 | 616 | var resizeTimeout = null; 617 | window.onresize = function (event) { 618 | if (resizeTimeout) { 619 | clearTimeout(resizeTimeout); 620 | } 621 | 622 | resizeTimeout = setTimeout(function () { 623 | updateUserLayout(); 624 | }, 2000) 625 | }; 626 | 627 | //Secreenshare On Browser 628 | function openFullscreen(elem) { 629 | if (elem.requestFullscreen) { 630 | elem.requestFullscreen(); 631 | } else if (elem.mozRequestFullScreen) { /* Firefox */ 632 | elem.mozRequestFullScreen(); 633 | } else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */ 634 | elem.webkitRequestFullscreen(); 635 | } else if (elem.msRequestFullscreen) { /* IE/Edge */ 636 | elem.msRequestFullscreen(); 637 | } 638 | } 639 | 640 | //Screenshare in Electron 641 | /** 642 | * @returns Promise 643 | */ 644 | async function electron_select_screen_to_share(sources) { 645 | const screenshareDialog = document.querySelector('div.screenshare-select-dialog-backdrop') 646 | 647 | let fail, success; 648 | const resultPromise = new Promise((resolve, reject) => { 649 | fail = reject 650 | success = resolve 651 | }) 652 | 653 | const options = screenshareDialog.querySelector("div.screenshare-options") 654 | // remove old options 655 | while (options.firstChild) { 656 | options.removeChild(options.firstChild); 657 | } 658 | const closeCallback = (screenid) => { 659 | screenshareDialog.hidden = true 660 | success(screenid) 661 | } 662 | // add new options 663 | for (let source of sources) { 664 | console.log(source) 665 | const option = document.createElement('div') 666 | option.classList.add('screenshare-option') 667 | const thumbnail = document.createElement('img') 668 | thumbnail.classList.add('thumbnail') 669 | //thumbnail.style = 'background-image: url(' + source.thumbnail.toDataURL() + ');' 670 | thumbnail.src = source.thumbnail.toDataURL() 671 | thumbnail.title = source.id 672 | option.appendChild(thumbnail) 673 | const name = document.createElement('p') 674 | name.innerText = source.name 675 | name.title = source.name 676 | option.appendChild(name) 677 | option.onclick = closeCallback.bind(null, source.id) 678 | options.appendChild(option) 679 | } 680 | 681 | const cancelButton = screenshareDialog.querySelector('#cancel-screenshare-select') 682 | cancelButton.onclick = _ => { 683 | screenshareDialog.hidden = true 684 | fail(new Error("User canceled")) 685 | } 686 | 687 | screenshareDialog.hidden = false 688 | return resultPromise 689 | } -------------------------------------------------------------------------------- /web/js/socket.io.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Socket.IO v4.7.5 3 | * (c) 2014-2024 Guillermo Rauch 4 | * Released under the MIT License. 5 | */ 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).io=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){a=!0,o=e},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw o}}}}var v=Object.create(null);v.open="0",v.close="1",v.ping="2",v.pong="3",v.message="4",v.upgrade="5",v.noop="6";var g=Object.create(null);Object.keys(v).forEach((function(e){g[v[e]]=e}));var m,b={type:"error",data:"parser error"},k="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),w="function"==typeof ArrayBuffer,_=function(e){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer},E=function(e,t,n){var r=e.type,i=e.data;return k&&i instanceof Blob?t?n(i):A(i,n):w&&(i instanceof ArrayBuffer||_(i))?t?n(i):A(new Blob([i]),n):n(v[r]+(i||""))},A=function(e,t){var n=new FileReader;return n.onload=function(){var e=n.result.split(",")[1];t("b"+(e||""))},n.readAsDataURL(e)};function O(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}for(var T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",R="undefined"==typeof Uint8Array?[]:new Uint8Array(256),C=0;C<64;C++)R[T.charCodeAt(C)]=C;var B,S="function"==typeof ArrayBuffer,N=function(e,t){if("string"!=typeof e)return{type:"message",data:x(e,t)};var n=e.charAt(0);return"b"===n?{type:"message",data:L(e.substring(1),t)}:g[n]?e.length>1?{type:g[n],data:e.substring(1)}:{type:g[n]}:b},L=function(e,t){if(S){var n=function(e){var t,n,r,i,o,s=.75*e.length,a=e.length,c=0;"="===e[e.length-1]&&(s--,"="===e[e.length-2]&&s--);var u=new ArrayBuffer(s),h=new Uint8Array(u);for(t=0;t>4,h[c++]=(15&r)<<4|i>>2,h[c++]=(3&i)<<6|63&o;return u}(e);return x(n,t)}return{base64:!0,data:e}},x=function(e,t){return"blob"===t?e instanceof Blob?e:new Blob([e]):e instanceof ArrayBuffer?e:e.buffer},P=String.fromCharCode(30);function j(){return new TransformStream({transform:function(e,t){!function(e,t){k&&e.data instanceof Blob?e.data.arrayBuffer().then(O).then(t):w&&(e.data instanceof ArrayBuffer||_(e.data))?t(O(e.data)):E(e,!1,(function(e){m||(m=new TextEncoder),t(m.encode(e))}))}(e,(function(n){var r,i=n.length;if(i<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,i);else if(i<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,i)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(i))}e.data&&"string"!=typeof e.data&&(r[0]|=128),t.enqueue(r),t.enqueue(n)}))}})}function q(e){return e.reduce((function(e,t){return e+t.length}),0)}function D(e,t){if(e[0].length===t)return e.shift();for(var n=new Uint8Array(t),r=0,i=0;i1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return e+"://"+this._hostname()+this._port()+this.opts.path+this._query(t)}},{key:"_hostname",value:function(){var e=this.opts.hostname;return-1===e.indexOf(":")?e:"["+e+"]"}},{key:"_port",value:function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""}},{key:"_query",value:function(e){var t=function(e){var t="";for(var n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t}(e);return t.length?"?"+t:""}}]),i}(U),z="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),J=64,$={},Q=0,X=0;function G(e){var t="";do{t=z[e%J]+t,e=Math.floor(e/J)}while(e>0);return t}function Z(){var e=G(+new Date);return e!==K?(Q=0,K=e):e+"."+G(Q++)}for(;X0&&void 0!==arguments[0]?arguments[0]:{};return i(e,{xd:this.xd,cookieJar:this.cookieJar},this.opts),new se(this.uri(),e)}},{key:"doWrite",value:function(e,t){var n=this,r=this.request({method:"POST",data:e});r.on("success",t),r.on("error",(function(e,t){n.onError("xhr post error",e,t)}))}},{key:"doPoll",value:function(){var e=this,t=this.request();t.on("data",this.onData.bind(this)),t.on("error",(function(t,n){e.onError("xhr poll error",t,n)})),this.pollXhr=t}}]),s}(W),se=function(e){o(i,e);var n=l(i);function i(e,r){var o;return t(this,i),H(f(o=n.call(this)),r),o.opts=r,o.method=r.method||"GET",o.uri=e,o.data=void 0!==r.data?r.data:null,o.create(),o}return r(i,[{key:"create",value:function(){var e,t=this,n=F(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this.opts.xd;var r=this.xhr=new ne(n);try{r.open(this.method,this.uri,!0);try{if(this.opts.extraHeaders)for(var o in r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&r.setRequestHeader(o,this.opts.extraHeaders[o])}catch(e){}if("POST"===this.method)try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(e){}try{r.setRequestHeader("Accept","*/*")}catch(e){}null===(e=this.opts.cookieJar)||void 0===e||e.addCookies(r),"withCredentials"in r&&(r.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(r.timeout=this.opts.requestTimeout),r.onreadystatechange=function(){var e;3===r.readyState&&(null===(e=t.opts.cookieJar)||void 0===e||e.parseCookies(r)),4===r.readyState&&(200===r.status||1223===r.status?t.onLoad():t.setTimeoutFn((function(){t.onError("number"==typeof r.status?r.status:0)}),0))},r.send(this.data)}catch(e){return void this.setTimeoutFn((function(){t.onError(e)}),0)}"undefined"!=typeof document&&(this.index=i.requestsCount++,i.requests[this.index]=this)}},{key:"onError",value:function(e){this.emitReserved("error",e,this.xhr),this.cleanup(!0)}},{key:"cleanup",value:function(e){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=re,e)try{this.xhr.abort()}catch(e){}"undefined"!=typeof document&&delete i.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var e=this.xhr.responseText;null!==e&&(this.emitReserved("data",e),this.emitReserved("success"),this.cleanup())}},{key:"abort",value:function(){this.cleanup()}}]),i}(U);if(se.requestsCount=0,se.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",ae);else if("function"==typeof addEventListener){addEventListener("onpagehide"in I?"pagehide":"unload",ae,!1)}function ae(){for(var e in se.requests)se.requests.hasOwnProperty(e)&&se.requests[e].abort()}var ce="function"==typeof Promise&&"function"==typeof Promise.resolve?function(e){return Promise.resolve().then(e)}:function(e,t){return t(e,0)},ue=I.WebSocket||I.MozWebSocket,he="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),fe=function(e){o(i,e);var n=l(i);function i(e){var r;return t(this,i),(r=n.call(this,e)).supportsBinary=!e.forceBase64,r}return r(i,[{key:"name",get:function(){return"websocket"}},{key:"doOpen",value:function(){if(this.check()){var e=this.uri(),t=this.opts.protocols,n=he?{}:F(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=he?new ue(e,t,n):t?new ue(e,t):new ue(e)}catch(e){return this.emitReserved("error",e)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}}},{key:"addEventListeners",value:function(){var e=this;this.ws.onopen=function(){e.opts.autoUnref&&e.ws._socket.unref(),e.onOpen()},this.ws.onclose=function(t){return e.onClose({description:"websocket connection closed",context:t})},this.ws.onmessage=function(t){return e.onData(t.data)},this.ws.onerror=function(t){return e.onError("websocket error",t)}}},{key:"write",value:function(e){var t=this;this.writable=!1;for(var n=function(){var n=e[r],i=r===e.length-1;E(n,t.supportsBinary,(function(e){try{t.ws.send(e)}catch(e){}i&&ce((function(){t.writable=!0,t.emitReserved("drain")}),t.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){a.enqueue(b);break}i=l*Math.pow(2,32)+f.getUint32(4),r=3}else{if(q(n)e){a.enqueue(b);break}}}})}(Number.MAX_SAFE_INTEGER,e.socket.binaryType),r=t.readable.pipeThrough(n).getReader(),i=j();i.readable.pipeTo(t.writable),e.writer=i.writable.getWriter();!function t(){r.read().then((function(n){var r=n.done,i=n.value;r||(e.onPacket(i),t())})).catch((function(e){}))}();var o={type:"open"};e.query.sid&&(o.data='{"sid":"'.concat(e.query.sid,'"}')),e.writer.write(o).then((function(){return e.onOpen()}))}))})))}},{key:"write",value:function(e){var t=this;this.writable=!1;for(var n=function(){var n=e[r],i=r===e.length-1;t.writer.write(n).then((function(){i&&ce((function(){t.writable=!0,t.emitReserved("drain")}),t.setTimeoutFn)}))},r=0;r1&&void 0!==arguments[1]?arguments[1]:{};return t(this,a),(r=s.call(this)).binaryType="arraybuffer",r.writeBuffer=[],n&&"object"===e(n)&&(o=n,n=null),n?(n=ve(n),o.hostname=n.host,o.secure="https"===n.protocol||"wss"===n.protocol,o.port=n.port,n.query&&(o.query=n.query)):o.host&&(o.hostname=ve(o.host).host),H(f(r),o),r.secure=null!=o.secure?o.secure:"undefined"!=typeof location&&"https:"===location.protocol,o.hostname&&!o.port&&(o.port=r.secure?"443":"80"),r.hostname=o.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=o.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=o.transports||["polling","websocket","webtransport"],r.writeBuffer=[],r.prevBufferLen=0,r.opts=i({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},o),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(e){for(var t={},n=e.split("&"),r=0,i=n.length;r1))return this.writeBuffer;for(var e,t=1,n=0;n=57344?n+=3:(r++,n+=4);return n}(e):Math.ceil(1.33*(e.byteLength||e.size))),n>0&&t>this.maxPayload)return this.writeBuffer.slice(0,n);t+=2}return this.writeBuffer}},{key:"write",value:function(e,t,n){return this.sendPacket("message",e,t,n),this}},{key:"send",value:function(e,t,n){return this.sendPacket("message",e,t,n),this}},{key:"sendPacket",value:function(e,t,n,r){if("function"==typeof t&&(r=t,t=void 0),"function"==typeof n&&(r=n,n=null),"closing"!==this.readyState&&"closed"!==this.readyState){(n=n||{}).compress=!1!==n.compress;var i={type:e,data:t,options:n};this.emitReserved("packetCreate",i),this.writeBuffer.push(i),r&&this.once("flush",r),this.flush()}}},{key:"close",value:function(){var e=this,t=function(){e.onClose("forced close"),e.transport.close()},n=function n(){e.off("upgrade",n),e.off("upgradeError",n),t()},r=function(){e.once("upgrade",n),e.once("upgradeError",n)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){e.upgrading?r():t()})):this.upgrading?r():t()),this}},{key:"onError",value:function(e){a.priorWebsocketSuccess=!1,this.emitReserved("error",e),this.onClose("transport error",e)}},{key:"onClose",value:function(e,t){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(this.clearTimeoutFn(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&(removeEventListener("beforeunload",this.beforeunloadEventListener,!1),removeEventListener("offline",this.offlineEventListener,!1)),this.readyState="closed",this.id=null,this.emitReserved("close",e,t),this.writeBuffer=[],this.prevBufferLen=0)}},{key:"filterUpgrades",value:function(e){for(var t=[],n=0,r=e.length;n=0&&t.num1?t-1:0),r=1;r1?n-1:0),i=1;in._opts.retries&&(n._queue.shift(),t&&t(e));else if(n._queue.shift(),t){for(var i=arguments.length,o=new Array(i>1?i-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this._queue.length){var t=this._queue[0];t.pending&&!e||(t.pending=!0,t.tryCount++,this.flags=t.flags,this.emit.apply(this,t.args))}}},{key:"packet",value:function(e){e.nsp=this.nsp,this.io._packet(e)}},{key:"onopen",value:function(){var e=this;"function"==typeof this.auth?this.auth((function(t){e._sendConnectPacket(t)})):this._sendConnectPacket(this.auth)}},{key:"_sendConnectPacket",value:function(e){this.packet({type:Be.CONNECT,data:this._pid?i({pid:this._pid,offset:this._lastOffset},e):e})}},{key:"onerror",value:function(e){this.connected||this.emitReserved("connect_error",e)}},{key:"onclose",value:function(e,t){this.connected=!1,delete this.id,this.emitReserved("disconnect",e,t),this._clearAcks()}},{key:"_clearAcks",value:function(){var e=this;Object.keys(this.acks).forEach((function(t){if(!e.sendBuffer.some((function(e){return String(e.id)===t}))){var n=e.acks[t];delete e.acks[t],n.withError&&n.call(e,new Error("socket has been disconnected"))}}))}},{key:"onpacket",value:function(e){if(e.nsp===this.nsp)switch(e.type){case Be.CONNECT:e.data&&e.data.sid?this.onconnect(e.data.sid,e.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Be.EVENT:case Be.BINARY_EVENT:this.onevent(e);break;case Be.ACK:case Be.BINARY_ACK:this.onack(e);break;case Be.DISCONNECT:this.ondisconnect();break;case Be.CONNECT_ERROR:this.destroy();var t=new Error(e.data.message);t.data=e.data.data,this.emitReserved("connect_error",t)}}},{key:"onevent",value:function(e){var t=e.data||[];null!=e.id&&t.push(this.ack(e.id)),this.connected?this.emitEvent(t):this.receiveBuffer.push(Object.freeze(t))}},{key:"emitEvent",value:function(e){if(this._anyListeners&&this._anyListeners.length){var t,n=y(this._anyListeners.slice());try{for(n.s();!(t=n.n()).done;){t.value.apply(this,e)}}catch(e){n.e(e)}finally{n.f()}}p(s(a.prototype),"emit",this).apply(this,e),this._pid&&e.length&&"string"==typeof e[e.length-1]&&(this._lastOffset=e[e.length-1])}},{key:"ack",value:function(e){var t=this,n=!1;return function(){if(!n){n=!0;for(var r=arguments.length,i=new Array(r),o=0;o0&&e.jitter<=1?e.jitter:0,this.attempts=0}Ie.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=0==(1&Math.floor(10*t))?e-n:e+n}return 0|Math.min(e,this.max)},Ie.prototype.reset=function(){this.attempts=0},Ie.prototype.setMin=function(e){this.ms=e},Ie.prototype.setMax=function(e){this.max=e},Ie.prototype.setJitter=function(e){this.jitter=e};var Fe=function(n){o(s,n);var i=l(s);function s(n,r){var o,a;t(this,s),(o=i.call(this)).nsps={},o.subs=[],n&&"object"===e(n)&&(r=n,n=void 0),(r=r||{}).path=r.path||"/socket.io",o.opts=r,H(f(o),r),o.reconnection(!1!==r.reconnection),o.reconnectionAttempts(r.reconnectionAttempts||1/0),o.reconnectionDelay(r.reconnectionDelay||1e3),o.reconnectionDelayMax(r.reconnectionDelayMax||5e3),o.randomizationFactor(null!==(a=r.randomizationFactor)&&void 0!==a?a:.5),o.backoff=new Ie({min:o.reconnectionDelay(),max:o.reconnectionDelayMax(),jitter:o.randomizationFactor()}),o.timeout(null==r.timeout?2e4:r.timeout),o._readyState="closed",o.uri=n;var c=r.parser||je;return o.encoder=new c.Encoder,o.decoder=new c.Decoder,o._autoConnect=!1!==r.autoConnect,o._autoConnect&&o.open(),o}return r(s,[{key:"reconnection",value:function(e){return arguments.length?(this._reconnection=!!e,this):this._reconnection}},{key:"reconnectionAttempts",value:function(e){return void 0===e?this._reconnectionAttempts:(this._reconnectionAttempts=e,this)}},{key:"reconnectionDelay",value:function(e){var t;return void 0===e?this._reconnectionDelay:(this._reconnectionDelay=e,null===(t=this.backoff)||void 0===t||t.setMin(e),this)}},{key:"randomizationFactor",value:function(e){var t;return void 0===e?this._randomizationFactor:(this._randomizationFactor=e,null===(t=this.backoff)||void 0===t||t.setJitter(e),this)}},{key:"reconnectionDelayMax",value:function(e){var t;return void 0===e?this._reconnectionDelayMax:(this._reconnectionDelayMax=e,null===(t=this.backoff)||void 0===t||t.setMax(e),this)}},{key:"timeout",value:function(e){return arguments.length?(this._timeout=e,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}},{key:"open",value:function(e){var t=this;if(~this._readyState.indexOf("open"))return this;this.engine=new ge(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var i=qe(n,"open",(function(){r.onopen(),e&&e()})),o=function(n){t.cleanup(),t._readyState="closed",t.emitReserved("error",n),e?e(n):t.maybeReconnectOnOpen()},s=qe(n,"error",o);if(!1!==this._timeout){var a=this._timeout,c=this.setTimeoutFn((function(){i(),o(new Error("timeout")),n.close()}),a);this.opts.autoUnref&&c.unref(),this.subs.push((function(){t.clearTimeoutFn(c)}))}return this.subs.push(i),this.subs.push(s),this}},{key:"connect",value:function(e){return this.open(e)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var e=this.engine;this.subs.push(qe(e,"ping",this.onping.bind(this)),qe(e,"data",this.ondata.bind(this)),qe(e,"error",this.onerror.bind(this)),qe(e,"close",this.onclose.bind(this)),qe(this.decoder,"decoded",this.ondecoded.bind(this)))}},{key:"onping",value:function(){this.emitReserved("ping")}},{key:"ondata",value:function(e){try{this.decoder.add(e)}catch(e){this.onclose("parse error",e)}}},{key:"ondecoded",value:function(e){var t=this;ce((function(){t.emitReserved("packet",e)}),this.setTimeoutFn)}},{key:"onerror",value:function(e){this.emitReserved("error",e)}},{key:"socket",value:function(e,t){var n=this.nsps[e];return n?this._autoConnect&&!n.active&&n.connect():(n=new Ue(this,e,t),this.nsps[e]=n),n}},{key:"_destroy",value:function(e){for(var t=0,n=Object.keys(this.nsps);t=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=this.setTimeoutFn((function(){t.skipReconnect||(e.emitReserved("reconnect_attempt",t.backoff.attempts),t.skipReconnect||t.open((function(n){n?(t._reconnecting=!1,t.reconnect(),e.emitReserved("reconnect_error",n)):t.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){e.clearTimeoutFn(r)}))}}},{key:"onreconnect",value:function(){var e=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",e)}}]),s}(U),Me={};function Ve(t,n){"object"===e(t)&&(n=t,t=void 0);var r,i=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,r=e;n=n||"undefined"!=typeof location&&location,null==e&&(e=n.protocol+"//"+n.host),"string"==typeof e&&("/"===e.charAt(0)&&(e="/"===e.charAt(1)?n.protocol+e:n.host+e),/^(https?|wss?):\/\//.test(e)||(e=void 0!==n?n.protocol+"//"+e:"https://"+e),r=ve(e)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var i=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+i+":"+r.port+t,r.href=r.protocol+"://"+i+(n&&n.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),o=i.source,s=i.id,a=i.path,c=Me[s]&&a in Me[s].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||c?r=new Fe(o,n):(Me[s]||(Me[s]=new Fe(o,n)),r=Me[s]),i.query&&!n.query&&(n.query=i.queryKey),r.socket(i.path,n)}return i(Ve,{Manager:Fe,Socket:Ue,io:Ve,connect:Ve}),Ve})); 7 | //# sourceMappingURL=socket.io.min.js.map -------------------------------------------------------------------------------- /web/js/utils.js: -------------------------------------------------------------------------------- 1 | function getUrlParam(parameter, defaultvalue) { 2 | var urlparameter = defaultvalue; 3 | if (window.location.href.indexOf(parameter) > -1) { 4 | urlparameter = getUrlVars()[parameter]; 5 | } 6 | let ret = decodeURIComponent(urlparameter); 7 | ret = ret == "false" ? false : ret; 8 | return ret; 9 | } 10 | 11 | function getUrlVars() { 12 | const parseVars = (str) => { 13 | if (str.length <= 1) { 14 | return {} 15 | } 16 | const keyValuePairs = str.substring(1).split("&") 17 | const res = {} 18 | for (let i = 0; i < keyValuePairs.length; i++) { 19 | const keyValuePair = keyValuePairs[i]; 20 | const [key, value] = keyValuePair.split('=') 21 | res[key] = value 22 | } 23 | return res 24 | } 25 | 26 | return Object.assign( 27 | {}, 28 | parseVars(window.location.search), 29 | parseVars(window.location.hash) 30 | ) 31 | } 32 | 33 | function uuidv4() { 34 | if (crypto) { //Generate uuid with crypto if possible 35 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 36 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 37 | ); 38 | } else { //Fallback to random if crypto is not implemented in the browser 39 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 40 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 41 | return v.toString(16); 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cracker0dks/basicwebrtc/ef69cc3ca5f0b33c74424b300e4d9ba0c47242c4/web/webfonts/fa-solid-900.woff2 --------------------------------------------------------------------------------