1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-22 10:22:39 +01:00

it doesnt work

This commit is contained in:
Puyodead1 2024-09-13 17:33:52 -04:00
parent 224d4020d0
commit 22a7fa3247
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
21 changed files with 1389 additions and 262 deletions

389
package-lock.json generated
View File

@ -33,6 +33,7 @@
"json-bigint": "^1.0.0",
"jsonwebtoken": "^9.0.1",
"lambert-server": "^1.2.12",
"mediasoup": "^3.14.14",
"missing-native-js-functions": "^1.4.3",
"module-alias": "^2.2.3",
"morgan": "^1.10.0",
@ -46,6 +47,8 @@
"probe-image-size": "^7.2.3",
"proxy-agent": "^6.3.0",
"reflect-metadata": "^0.1.13",
"sdp-transform": "^2.14.2",
"semantic-sdp": "^3.30.0",
"ts-node": "^10.9.1",
"tslib": "^2.6.1",
"typeorm": "^0.3.17",
@ -70,6 +73,7 @@
"@types/node-os-utils": "^1.3.1",
"@types/nodemailer": "^6.4.9",
"@types/probe-image-size": "^7.2.0",
"@types/sdp-transform": "^2.4.9",
"@types/sharp": "^0.31.1",
"@types/ws": "^8.5.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
@ -1306,6 +1310,27 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@isaacs/fs-minipass/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/@jimp/bmp": {
"version": "0.22.12",
"resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz",
@ -2745,6 +2770,15 @@
"@types/express": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"license": "MIT",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@ -2784,6 +2818,12 @@
"i18next": ">=17.0.11"
}
},
"node_modules/@types/ini": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz",
"integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==",
"license": "MIT"
},
"node_modules/@types/json-bigint": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz",
@ -2819,6 +2859,12 @@
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "1.4.11",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz",
@ -2906,6 +2952,13 @@
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
"node_modules/@types/sdp-transform": {
"version": "2.4.9",
"resolved": "https://registry.npmjs.org/@types/sdp-transform/-/sdp-transform-2.4.9.tgz",
"integrity": "sha512-bVr+/OoZZy7wrHlNcEAAa6PAgKA4BoXPYVN2EijMC5WnGgQ4ZEuixmKnVs2roiAvr7RhIFVH17QD27cojgIZCg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
@ -4265,11 +4318,12 @@
"integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg=="
},
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@ -4609,6 +4663,12 @@
"node": ">=6"
}
},
"node_modules/equals-ignore-case": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/equals-ignore-case/-/equals-ignore-case-1.0.1.tgz",
"integrity": "sha512-krgK/Px09jhcc7wK5/lxApRv7XmIT/fSgrMwdaW/V1FmPJEIJMNGEMhe0U9tJ/97rPe75MHKPRqi7/8Tqz6NMA==",
"license": "ISC"
},
"node_modules/erlpack": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/erlpack/-/erlpack-0.1.4.tgz",
@ -5156,6 +5216,29 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fido2-lib": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/fido2-lib/-/fido2-lib-3.5.3.tgz",
@ -5280,6 +5363,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flatbuffers": {
"version": "24.3.25",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.3.25.tgz",
"integrity": "sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ==",
"license": "Apache-2.0"
},
"node_modules/flatted": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
@ -5345,6 +5434,18 @@
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/formidable": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
@ -5642,6 +5743,23 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/h264-profile-level-id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-2.0.0.tgz",
"integrity": "sha512-X4CLryVbVA0CtjTExS4G5U1gb2Z4wa32AF8ukVmFuLdw2JRq2aHisor7SY5SYTUUrUSqq0KdPIO18sql6IWIQw==",
"license": "ISC",
"dependencies": {
"@types/debug": "^4.1.12",
"debug": "^4.3.4"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mediasoup"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -6562,6 +6680,200 @@
"node": ">= 0.6"
}
},
"node_modules/mediasoup": {
"version": "3.14.14",
"resolved": "https://registry.npmjs.org/mediasoup/-/mediasoup-3.14.14.tgz",
"integrity": "sha512-BagHzOgtPUPxpPAHSMdFKICjtDuLNHznuk3YUUDDTFIwyTOYIXeX7NRklzMRAiRRKA9luXmjCjTNwkMQ4sb0aA==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@types/ini": "^4.1.1",
"debug": "^4.3.7",
"flatbuffers": "^24.3.25",
"h264-profile-level-id": "^2.0.0",
"ini": "^5.0.0",
"node-fetch": "^3.3.2",
"supports-color": "^9.4.0",
"tar": "^7.4.3"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mediasoup"
}
},
"node_modules/mediasoup/node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/mediasoup/node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/mediasoup/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mediasoup/node_modules/ini": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
"integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/mediasoup/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mediasoup/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mediasoup/node_modules/minizlib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz",
"integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==",
"license": "MIT",
"dependencies": {
"minipass": "^7.0.4",
"rimraf": "^5.0.5"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mediasoup/node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mediasoup/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/mediasoup/node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"license": "ISC",
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mediasoup/node_modules/supports-color": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz",
"integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/mediasoup/node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mediasoup/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@ -6850,9 +7162,10 @@
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multer": {
"version": "1.4.5-lts.1",
@ -7026,6 +7339,25 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -8222,6 +8554,15 @@
}
]
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -8446,6 +8787,26 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"node_modules/sdp-transform": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
"license": "MIT",
"bin": {
"sdp-verify": "checker.js"
}
},
"node_modules/semantic-sdp": {
"version": "3.30.0",
"resolved": "https://registry.npmjs.org/semantic-sdp/-/semantic-sdp-3.30.0.tgz",
"integrity": "sha512-HxChWm8UMQG3qakFR/BidJGMtP1xk9KcOLDsSXZU01c9IIU1QnA1RODMYfST3MwnHsby4q7vu//m4qUDy8J+Dg==",
"license": "MIT",
"dependencies": {
"equals-ignore-case": "^1.0.0",
"randombytes": "^2.0.3",
"sdp-transform": "^2"
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@ -8493,11 +8854,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@ -10026,6 +10382,15 @@
"node": ">=6.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webcrypto-core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.0.tgz",

View File

@ -54,6 +54,7 @@
"@types/node-os-utils": "^1.3.1",
"@types/nodemailer": "^6.4.9",
"@types/probe-image-size": "^7.2.0",
"@types/sdp-transform": "^2.4.9",
"@types/sharp": "^0.31.1",
"@types/ws": "^8.5.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
@ -89,6 +90,7 @@
"json-bigint": "^1.0.0",
"jsonwebtoken": "^9.0.1",
"lambert-server": "^1.2.12",
"mediasoup": "^3.14.14",
"missing-native-js-functions": "^1.4.3",
"module-alias": "^2.2.3",
"morgan": "^1.10.0",
@ -102,6 +104,8 @@
"probe-image-size": "^7.2.3",
"proxy-agent": "^6.3.0",
"reflect-metadata": "^0.1.13",
"sdp-transform": "^2.14.2",
"semantic-sdp": "^3.30.0",
"ts-node": "^10.9.1",
"tslib": "^2.6.1",
"typeorm": "^0.3.17",
@ -113,7 +117,8 @@
"@spacebar/api": "dist/api",
"@spacebar/cdn": "dist/cdn",
"@spacebar/gateway": "dist/gateway",
"@spacebar/util": "dist/util"
"@spacebar/util": "dist/util",
"@spacebar/webrtc": "dist/webrtc"
},
"optionalDependencies": {
"erlpack": "^0.1.4",

View File

@ -19,13 +19,14 @@
process.on("unhandledRejection", console.error);
process.on("uncaughtException", console.error);
import http from "http";
import * as Api from "@spacebar/api";
import * as Gateway from "@spacebar/gateway";
import { CDNServer } from "@spacebar/cdn";
import express from "express";
import { green, bold } from "picocolors";
import * as Gateway from "@spacebar/gateway";
import { Config, initDatabase, Sentry } from "@spacebar/util";
import * as Webrtc from "@spacebar/webrtc";
import express from "express";
import http from "http";
import { bold, green } from "picocolors";
const app = express();
const server = http.createServer();
@ -36,9 +37,11 @@ server.on("request", app);
const api = new Api.SpacebarServer({ server, port, production, app });
const cdn = new CDNServer({ server, port, production, app });
const gateway = new Gateway.Server({ server, port, production });
const webrtc = new Webrtc.Server({ server: undefined, port: 3004, production });
process.on("SIGTERM", async () => {
console.log("Shutting down due to SIGTERM");
await webrtc.stop();
await gateway.stop();
await cdn.stop();
await api.stop();
@ -54,7 +57,12 @@ async function main() {
await new Promise((resolve) =>
server.listen({ port }, () => resolve(undefined)),
);
await Promise.all([api.start(), cdn.start(), gateway.start()]);
await Promise.all([
api.start(),
cdn.start(),
gateway.start(),
webrtc.start(),
]);
Sentry.errorHandler(app);

View File

@ -17,15 +17,16 @@
*/
// process.env.MONGOMS_DEBUG = "true";
process.env.DEBUG = "mediasoup*";
require("module-alias/register");
import "reflect-metadata";
import cluster, { Worker } from "cluster";
import os from "os";
import { red, bold, yellow, cyan } from "picocolors";
import { initStats } from "./stats";
import { config } from "dotenv";
config();
import { execSync } from "child_process";
import cluster, { Worker } from "cluster";
import { config } from "dotenv";
import os from "os";
import { bold, cyan, red, yellow } from "picocolors";
import "reflect-metadata";
import { initStats } from "./stats";
config();
const cores = process.env.THREADS ? parseInt(process.env.THREADS) : 1;

View File

@ -17,19 +17,19 @@
*/
import { Payload, WebSocket } from "@spacebar/gateway";
import { genVoiceToken } from "../util/SessionUtils";
import { check } from "./instanceOf";
import {
Config,
emitEvent,
Guild,
Member,
Region,
VoiceServerUpdateEvent,
VoiceState,
VoiceStateUpdateEvent,
VoiceStateUpdateSchema,
Region,
} from "@spacebar/util";
import { genVoiceToken } from "../util/SessionUtils";
import { check } from "./instanceOf";
// TODO: check if a voice server is setup
// Notice: Bot users respect the voice channel's user limit, if set.
@ -136,6 +136,7 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) {
endpoint: guildRegion.endpoint,
},
guild_id: voiceState.guild_id,
user_id: voiceState.user_id,
} as VoiceServerUpdateEvent);
}
}

View File

@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { VoiceOPCodes } from "../../webrtc";
// import { VoiceOPCodes } from "@spacebar/webrtc";
export enum OPCODES {
@ -63,7 +65,7 @@ export enum CLOSECODES {
}
export interface Payload {
op: OPCODES /* | VoiceOPCodes */;
op: OPCODES | VoiceOPCodes;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
d?: any;
s?: number;

View File

@ -17,10 +17,10 @@
*/
import { Intents, ListenEventOpts, Permissions } from "@spacebar/util";
import WS from "ws";
import { Client } from "@spacebar/webrtc";
import { Deflate, Inflate } from "fast-zlib";
import WS from "ws";
import { Capabilities } from "./Capabilities";
// import { Client } from "@spacebar/webrtc";
export interface WebSocket extends WS {
version: number;
@ -42,6 +42,6 @@ export interface WebSocket extends WS {
member_events: Record<string, () => unknown>;
listen_options: ListenEventOpts;
capabilities?: Capabilities;
// client?: Client;
client?: Client;
large_threshold: number;
}

View File

@ -28,4 +28,5 @@ export interface GuildCreateSchema {
channels?: ChannelModifySchema[];
system_channel_id?: string;
rules_channel_id?: string;
guild_template_code?: string; // TODO: move this
}

View File

@ -41,6 +41,7 @@ export const ajv = new Ajv({
strict: true,
strictRequired: true,
allowUnionTypes: true,
removeAdditional: true,
});
addFormats(ajv);

View File

@ -0,0 +1,25 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export interface VoiceReadySchema {
experiments: string[];
ip: string;
modes: string[];
port: number;
ssrc: number; // seems to be a "base", first stream ssrc will be +1 with rtx +2
}

View File

@ -59,6 +59,7 @@ export * from "./RegisterSchema";
export * from "./RelationshipPostSchema";
export * from "./RelationshipPutSchema";
export * from "./RequestGuildMembersSchema";
export * from "./responses";
export * from "./RoleModifySchema";
export * from "./RolePositionUpdateSchema";
export * from "./SelectProtocolSchema";
@ -77,10 +78,10 @@ export * from "./UserSettingsSchema";
export * from "./Validator";
export * from "./VanityUrlSchema";
export * from "./VoiceIdentifySchema";
export * from "./VoiceReadySchema";
export * from "./VoiceStateUpdateSchema";
export * from "./VoiceVideoSchema";
export * from "./WebAuthnSchema";
export * from "./WebhookCreateSchema";
export * from "./WebhookExecuteSchema";
export * from "./WidgetModifySchema";
export * from "./responses";

View File

@ -21,6 +21,7 @@ import dotenv from "dotenv";
import http from "http";
import ws from "ws";
import { Connection } from "./events/Connection";
import { createWorkers } from "./util";
dotenv.config();
export class Server {
@ -69,6 +70,8 @@ export class Server {
await initDatabase();
await Config.init();
await initEvent();
await createWorkers();
if (!this.server.listening) {
this.server.listen(this.port);
console.log(`[WebRTC] online on 0.0.0.0:${this.port}`);

View File

@ -18,10 +18,34 @@
import { WebSocket } from "@spacebar/gateway";
import { Session } from "@spacebar/util";
import { getClients } from "../util";
export async function onClose(this: WebSocket, code: number, reason: string) {
console.log("[WebRTC] closed", code, reason.toString());
if (this.session_id) await Session.delete({ session_id: this.session_id });
// we need to find all consumers on all clients that have a producer in our client
const clients = getClients(this.client?.channel_id!);
for (const client of clients) {
if (client.websocket.user_id === this.user_id) continue;
// if any consumer on this client has a producer id that is in our client, close it
client.consumers.forEach((consumer) => {
// check if any producers in our client have the same producer id
this.client?.producers.forEach((producer) => {
if (producer.id === consumer.producerId) {
console.log("[WebRTC] closing consumer", consumer.id);
consumer.close();
}
});
});
}
this.client?.producers.forEach((producer) => producer.close());
this.client?.consumers.forEach((consumer) => consumer.close());
this.client?.transports.producer.close();
this.removeAllListeners();
}

View File

@ -17,11 +17,12 @@
*/
import { Payload, Send, WebSocket } from "@spacebar/gateway";
import * as mediasoup from "mediasoup";
import { VoiceOPCodes } from "../util";
export async function onBackendVersion(this: WebSocket, data: Payload) {
await Send(this, {
op: VoiceOPCodes.VOICE_BACKEND_VERSION,
d: { voice: "0.8.43", rtc_worker: "0.3.26" },
d: { voice: "0.1.0", rtc_worker: mediasoup.version },
});
}

View File

@ -20,13 +20,30 @@ import { CLOSECODES, Payload, Send, WebSocket } from "@spacebar/gateway";
import {
validateSchema,
VoiceIdentifySchema,
VoiceReadySchema,
VoiceState,
} from "@spacebar/util";
import { endpoint, getClients, VoiceOPCodes, PublicIP } from "@spacebar/webrtc";
import SemanticSDP from "semantic-sdp";
const defaultSDP = require("./sdp.json");
import {
getClients,
getOrCreateRouter,
getWorker,
Stream,
VoiceOPCodes,
} from "@spacebar/webrtc";
export async function onIdentify(this: WebSocket, data: Payload) {
export interface IdentifyPayload extends Payload {
d: {
server_id: string; //guild id
session_id: string; //gateway session
streams: Stream[];
token: string; //voice_states token
user_id: string;
video: boolean;
max_dave_protocol_version?: number; // present in v8, not sure what version added it
};
}
export async function onIdentify(this: WebSocket, data: IdentifyPayload) {
clearTimeout(this.readyTimeout);
const { server_id, user_id, session_id, token, streams, video } =
validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema;
@ -38,27 +55,59 @@ export async function onIdentify(this: WebSocket, data: Payload) {
this.user_id = user_id;
this.session_id = session_id;
const sdp = SemanticSDP.SDPInfo.expand(defaultSDP);
sdp.setDTLS(
SemanticSDP.DTLSInfo.expand({
setup: "actpass",
hash: "sha-256",
fingerprint: endpoint.getDTLSFingerprint(),
}),
);
const worker = getWorker();
const router = await getOrCreateRouter(voiceState.channel_id);
console.debug(`onIdentify(router)`, router.id);
const producerTransport = await router.createWebRtcTransport({
webRtcServer: worker.appData.webRtcServer!,
enableUdp: true,
} as any);
// producerTransport.enableTraceEvent(["bwe", "probation"]);
// listen to any events
for (const event of producerTransport.eventNames()) {
if (typeof event !== "string") continue;
producerTransport.on(event as any, (...args) => {
console.debug(`producerTransport event: ${event}`, args);
});
}
// listen to any events
for (const event of producerTransport.observer.eventNames()) {
if (typeof event !== "string") continue;
producerTransport.observer.on(event as any, (...args) => {
console.debug(`producerTransport observer event: ${event}`, args);
});
}
// const consumerTransport = await router.createWebRtcTransport({
// webRtcServer: worker.appData.webRtcServer!,
// enableUdp: true,
// });
// consumerTransport.enableTraceEvent(["bwe", "probation"]);
// // listen to any events
// for (const event of consumerTransport.eventNames()) {
// if (typeof event !== "string") continue;
// consumerTransport.on(event as any, (...args) => {
// console.debug(`consumerTransport event: ${event}`, args);
// });
// }
this.client = {
websocket: this,
out: {
tracks: new Map(),
},
in: {
audio_ssrc: 0,
video_ssrc: 0,
rtx_ssrc: 0,
},
sdp,
ssrc: 1,
channel_id: voiceState.channel_id,
codecs: [],
streams: streams!,
headerExtensions: [],
producers: new Map(),
consumers: new Map(),
transports: {
producer: producerTransport,
},
};
const clients = getClients(voiceState.channel_id)!;
@ -68,24 +117,30 @@ export async function onIdentify(this: WebSocket, data: Payload) {
clients.delete(this.client!);
});
await Send(this, {
const d = {
op: VoiceOPCodes.READY,
d: {
streams: [
// { type: "video", ssrc: this.ssrc + 1, rtx_ssrc: this.ssrc + 2, rid: "100", quality: 100, active: false }
],
ssrc: -1,
port: endpoint.getLocalPort(),
streams: streams?.map((x) => ({
...x,
ssrc: ++this.client!.ssrc, // first stream should be 2
rtx_ssrc: ++this.client!.ssrc, // first stream should be 3
})),
ssrc: this.client.ssrc, // this is just a base, first stream ssrc will be +1 with rtx +2
ip: "192.168.10.112",
port: 20000,
modes: [
"aead_aes256_gcm_rtpsize",
"aead_aes256_gcm",
"aead_xchacha20_poly1305_rtpsize",
"xsalsa20_poly1305_lite_rtpsize",
"xsalsa20_poly1305_lite",
"xsalsa20_poly1305_suffix",
"xsalsa20_poly1305",
],
ip: PublicIP,
experiments: [],
},
});
experiments: ["fixed_keyframe_interval"],
} as VoiceReadySchema,
};
console.debug(`onIdentify(ready packet)`, d);
await Send(this, d);
}

View File

@ -18,8 +18,55 @@
import { Payload, Send, WebSocket } from "@spacebar/gateway";
import { SelectProtocolSchema, validateSchema } from "@spacebar/util";
import { PublicIP, VoiceOPCodes, endpoint } from "@spacebar/webrtc";
import SemanticSDP, { MediaInfo, SDPInfo } from "semantic-sdp";
import { types as MediaSoupTypes } from "mediasoup";
import * as sdpTransform from "sdp-transform";
import { getRouter, SUPPORTED_EXTENTIONS, VoiceOPCodes } from "../util";
// request:
// {
// "codecs": [
// {
// "name": "opus",
// "payload_type": 109,
// "priority": 1000,
// "rtx_payload_type": null,
// "type": "audio"
// },
// {
// "name": "H264",
// "payload_type": 126,
// "priority": 1000,
// "rtx_payload_type": 127,
// "type": "video"
// },
// {
// "name": "VP8",
// "payload_type": 120,
// "priority": 2000,
// "rtx_payload_type": 124,
// "type": "video"
// },
// {
// "name": "VP9",
// "payload_type": 121,
// "priority": 3000,
// "rtx_payload_type": 125,
// "type": "video"
// }
// ],
// "data": "a=fingerprint:sha-256 F1:31:51:8B:E9:C8:3F:33:61:41:5C:BA:7A:59:07:4A:DA:53:40:88:62:0B:DA:B0:4D:0C:58:9B:16:D8:9F:25\na=ice-options:trickle\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\na=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\na=ice-pwd:5ac55fdfff3ac50fbb6c2852baea62bf\na=ice-ufrag:5e6e9e9c\na=rtpmap:109 opus/48000/2\na=extmap:4 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=extmap:5 urn:ietf:params:rtp-hdrext:toffset\na=extmap:6/recvonly http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\na=extmap:7 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=rtpmap:120 VP8/90000\na=rtpmap:124 rtx/90000",
// "protocol": "webrtc",
// "rtc_connection_id": "70a69cc2-14d7-496e-ba4b-d16570a95ade",
// "sdp": "a=fingerprint:sha-256 F1:31:51:8B:E9:C8:3F:33:61:41:5C:BA:7A:59:07:4A:DA:53:40:88:62:0B:DA:B0:4D:0C:58:9B:16:D8:9F:25\na=ice-options:trickle\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\na=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\na=ice-pwd:5ac55fdfff3ac50fbb6c2852baea62bf\na=ice-ufrag:5e6e9e9c\na=rtpmap:109 opus/48000/2\na=extmap:4 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=extmap:5 urn:ietf:params:rtp-hdrext:toffset\na=extmap:6/recvonly http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\na=extmap:7 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=rtpmap:120 VP8/90000\na=rtpmap:124 rtx/90000"
// }
// response:
// {
// "audio_codec": "opus",
// "media_session_id": "40984f1105901745530fb81cfe5f5633",
// "sdp": "m=audio 50021 ICE/SDP\na=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87\nc=IN IP4 66.22.206.164\na=rtcp:50021\na=ice-ufrag:iLG8\na=ice-pwd:qMfFrCD0PcC/TxyfQM9H7t\na=fingerprint:sha-256 4A:79:94:16:44:3F:BD:05:41:5A:C7:20:F3:12:54:70:00:73:5D:33:00:2D:2C:80:9B:39:E1:9F:2D:A7:49:87\na=candidate:1 1 UDP 4261412862 66.22.206.164 50021 typ host\n",
// "video_codec": "H264"
// }
export async function onSelectProtocol(this: WebSocket, payload: Payload) {
if (!this.client) return;
@ -29,39 +76,120 @@ export async function onSelectProtocol(this: WebSocket, payload: Payload) {
payload.d,
) as SelectProtocolSchema;
const offer = SemanticSDP.SDPInfo.parse("m=audio\n" + data.sdp!);
this.client.sdp!.setICE(offer.getICE());
this.client.sdp!.setDTLS(offer.getDTLS());
await Send(this, { op: VoiceOPCodes.MEDIA_SINK_WANTS, d: { any: 100 } });
const transport = endpoint.createTransport(this.client.sdp!);
this.client.transport = transport;
transport.setRemoteProperties(this.client.sdp!);
transport.setLocalProperties(this.client.sdp!);
const router = getRouter(this.client.channel_id);
if (!router) {
console.error("Could not find router");
this.close();
return;
}
const dtls = transport.getLocalDTLSInfo();
const ice = transport.getLocalICEInfo();
const port = endpoint.getLocalPort();
const fingerprint = dtls.getHash() + " " + dtls.getFingerprint();
const candidates = transport.getLocalCandidates();
const candidate = candidates[0];
const clientAudioCodecs = data
.codecs!.filter((x) => x.type === "audio")
.sort((a, b) => a.priority - b.priority);
const answer =
`m=audio ${port} ICE/SDP` +
`a=fingerprint:${fingerprint}` +
`c=IN IP4 ${PublicIP}` +
`a=rtcp:${port}` +
`a=ice-ufrag:${ice.getUfrag()}` +
`a=ice-pwd:${ice.getPwd()}` +
`a=fingerprint:${fingerprint}` +
`a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host`;
const clientVideoCodecs = data
.codecs!.filter((x) => x.type === "video")
.sort((a, b) => a.priority - b.priority);
const serverAudioCodecs = router.rtpCapabilities.codecs!.filter(
(x) => x.kind === "audio",
);
const serverVideoCodecs = router.rtpCapabilities.codecs!.filter(
(x) => x.kind === "video",
);
const audioCodec = serverAudioCodecs.find((x) => {
return clientAudioCodecs.some(
(y) => y.name === x.mimeType.split("/")[1],
);
});
const videoCodec = serverVideoCodecs.find((x) => {
return clientVideoCodecs.some(
(y) => y.name === x.mimeType.split("/")[1],
);
});
if (!audioCodec || !videoCodec) {
console.error("Could not agree on a codec");
this.close();
return;
}
const sdp = sdpTransform.parse(data.sdp!);
this.client.sdp = sdp;
this.client.codecs = data.codecs!;
this.client.headerExtensions =
sdp.ext
?.filter((x) => SUPPORTED_EXTENTIONS.includes(x.uri))
.map((x) => ({
uri: x.uri as MediaSoupTypes.RtpHeaderExtensionUri,
id: x.value,
})) ?? [];
await this.client.transports.producer.connect({
dtlsParameters: {
fingerprints: [
{
algorithm: sdp.fingerprint!
.type as MediaSoupTypes.FingerprintAlgorithm,
value: sdp.fingerprint!.hash,
},
],
role: "client",
},
});
console.debug("producer transport connected");
// await this.client.transports.consumer.connect({
// dtlsParameters: {
// fingerprints: [
// {
// algorithm: sdp.fingerprint!
// .type as MediaSoupTypes.FingerprintAlgorithm,
// value: sdp.fingerprint!.hash,
// },
// ],
// role: "client",
// },
// });
// console.debug("consumer transport connected");
const iceParameters = this.client.transports.producer.iceParameters;
const iceCandidates = this.client.transports.producer.iceCandidates;
const iceCandidate = iceCandidates[0];
const dltsParamters = this.client.transports.producer.dtlsParameters;
const fingerprint = dltsParamters.fingerprints.find(
(x) => x.algorithm === "sha-256",
)!;
const sdpAnswer =
`m=audio ${iceCandidate.port} ICE/SDP\n` +
`a=fingerprint:sha-256 ${fingerprint.value}\n` +
`c=IN IP4 ${iceCandidate.ip}\n` +
`a=rtcp:${iceCandidate.port}\n` +
`a=ice-ufrag:${iceParameters.usernameFragment}\n` +
`a=ice-pwd:${iceParameters.password}\n` +
`a=fingerprint:sha-256 ${fingerprint.value}\n` +
`a=candidate:1 1 ${iceCandidate.protocol.toUpperCase()} ${
iceCandidate.priority
} ${iceCandidate.ip} ${iceCandidate.port} typ ${iceCandidate.type}\n`;
console.debug("onSelectProtocol sdp serialized\n", sdpAnswer);
await Send(this, {
op: VoiceOPCodes.SESSION_DESCRIPTION,
op: VoiceOPCodes.SELECT_PROTOCOL_ACK,
d: {
video_codec: "H264",
sdp: answer,
// audio_codec: audioCodec.mimeType.split("/")[1],
// video_codec: videoCodec.mimeType.split("/")[1],
audioCodec: "opus",
videoCodec: "H264",
media_session_id: this.session_id,
audio_codec: "opus",
sdp: sdpAnswer,
},
});
}

View File

@ -25,15 +25,13 @@ export async function onSpeaking(this: WebSocket, data: Payload) {
if (!this.client) return;
getClients(this.client.channel_id).forEach((client) => {
if (client === this.client) return;
const ssrc = this.client!.out.tracks.get(client.websocket.user_id);
if (client.websocket.user_id === this.user_id) return;
Send(client.websocket, {
op: VoiceOPCodes.SPEAKING,
d: {
user_id: client.websocket.user_id,
speaking: data.d.speaking,
ssrc: ssrc?.audio_ssrc || 0,
ssrc: data.d.ssrc,
},
});
});

View File

@ -18,134 +18,305 @@
import { Payload, Send, WebSocket } from "@spacebar/gateway";
import { validateSchema, VoiceVideoSchema } from "@spacebar/util";
import { channels, getClients, VoiceOPCodes } from "@spacebar/webrtc";
import { IncomingStreamTrack, SSRCs } from "medooze-media-server";
import SemanticSDP from "semantic-sdp";
import { types as MediaSoupTypes } from "mediasoup";
import { getClients, getRouter, VoiceOPCodes } from "../util";
// request:
// {
// "audio_ssrc": 0,
// "rtx_ssrc": 197,
// "streams": [
// {
// "active": false,
// "max_bitrate": 2500000,
// "max_framerate": 30,
// "max_resolution": {
// "height": 720,
// "type": "fixed",
// "width": 1280
// },
// "quality": 100,
// "rid": "100",
// "rtx_ssrc": 197,
// "ssrc": 196,
// "type": "video"
// }
// ],
// "video_ssrc": 196
// }
export async function onVideo(this: WebSocket, payload: Payload) {
if (!this.client) return;
const { transport, channel_id } = this.client;
if (!transport) return;
const { channel_id } = this.client;
const d = validateSchema("VoiceVideoSchema", payload.d) as VoiceVideoSchema;
console.log(d);
await Send(this, { op: VoiceOPCodes.MEDIA_SINK_WANTS, d: { any: 100 } });
const id = "stream" + this.user_id;
var stream = this.client.in.stream!;
if (!stream) {
stream = this.client.transport!.createIncomingStream(
// @ts-ignore
SemanticSDP.StreamInfo.expand({
id,
// @ts-ignore
tracks: [],
}),
);
this.client.in.stream = stream;
const interval = setInterval(() => {
for (const track of stream.getTracks()) {
for (const layer of Object.values(track.getStats())) {
console.log(track.getId(), layer.total);
const router = getRouter(channel_id);
if (!router) {
console.error(`router not found`);
return;
}
}
}, 5000);
stream.on("stopped", () => {
console.log("stream stopped");
clearInterval(interval);
this.client.producers.forEach((producer) => producer.close());
this.client.consumers.forEach((consumer) => consumer.close());
// const producer = await this.client.transports.producer.produce({
// kind: "audio",
// rtpParameters: {
// codecs: this.client.codecs
// .filter((x) => x.type === "audio")
// .map((x) => {
// const a = MEDIA_CODECS.find(
// (y) =>
// y.kind === "audio" && y.mimeType.endsWith(x.name),
// );
// return {
// clockRate: a!.clockRate,
// mimeType: a!.mimeType,
// payloadType: x.payload_type,
// channels: a?.channels,
// parameters: a?.parameters,
// rtcpFeedback: a?.rtcpFeedback,
// };
// }),
// encodings: [
// {
// ssrc: d.audio_ssrc,
// },
// ],
// headerExtensions: this.client.headerExtensions,
// },
// paused: false,
// });
let videoProducer: MediaSoupTypes.Producer | null = null;
// if (d.video_ssrc !== 0) {
// videoProducer = await this.client.transports.producer.produce({
// kind: "video",
// rtpParameters: {
// codecs: [
// ...this.client.codecs
// .filter((x) => x.type === "video")
// .map((x) => {
// const a = MEDIA_CODECS.find(
// (y) =>
// y.kind === "video" &&
// y.mimeType.endsWith(x.name),
// );
// return {
// clockRate: a!.clockRate,
// mimeType: a!.mimeType,
// payloadType: x.payload_type,
// channels: a?.channels,
// parameters: a?.parameters,
// rtcpFeedback: a?.rtcpFeedback,
// };
// }),
// // {
// // payloadType: 126,
// // mimeType: "video/H264",
// // clockRate: 90000,
// // parameters: {
// // "level-asymmetry-allowed": 1,
// // },
// // rtcpFeedback: [
// // { type: "nack" },
// // { type: "nack", parameter: "pli" },
// // { type: "ccm", parameter: "fir" },
// // { type: "goog-remb" },
// // { type: "transport-cc" },
// // ],
// // },
// // {
// // payloadType: 120,
// // mimeType: "video/VP8",
// // clockRate: 90000,
// // rtcpFeedback: [
// // { type: "nack" },
// // { type: "nack", parameter: "pli" },
// // { type: "ccm", parameter: "fir" },
// // { type: "goog-remb" },
// // { type: "transport-cc" },
// // ],
// // },
// // ...this.client.codecs
// // .filter((x) => x.type === "video")
// // .map((x) => {
// // const a = MEDIA_CODECS.find(
// // (y) =>
// // y.kind === "video" &&
// // y.mimeType.endsWith(x.name),
// // );
// // return {
// // mimeType: "video/rtx",
// // clockRate: a!.clockRate,
// // payloadType: x.rtx_payload_type!,
// // parameters: {
// // apt: x.payload_type,
// // },
// // };
// // }),
// // {
// // payloadType: 127,
// // mimeType: "video/rtx",
// // clockRate: 90000,
// // parameters: {
// // apt: 126,
// // },
// // },
// ],
// encodings: [
// {
// ssrc: d.video_ssrc,
// // rtx: { ssrc: d.rtx_ssrc! },
// // codecPayloadType: 126,
// // rid: d.streams![0].rid,
// },
// // ...this.client.codecs.map((x) => ({
// // ssrc: d.video_ssrc,
// // codecPayloadType: x.payload_type,
// // })),
// ],
// headerExtensions: this.client.headerExtensions,
// },
// paused: false,
// });
// console.log(await videoProducer.dump());
// videoProducer.enableTraceEvent([
// "fir",
// "keyframe",
// "nack",
// "pli",
// "rtp",
// "sr",
// ]);
// // for (const event of videoProducer.eventNames()) {
// // if (typeof event !== "string") continue;
// // videoProducer.on(event as any, (...args) => {
// // console.debug(`videoProducer event: ${event}`, args);
// // });
// // }
// videoProducer.on("trace", (trace) => {
// console.debug(
// `videoproducer(trace):`,
// JSON.stringify(trace, null, 4),
// );
// });
// this.client.producers.set(d.video_ssrc, videoProducer);
// }
let audioProducer: MediaSoupTypes.Producer | null = null;
if (d.audio_ssrc !== 0) {
audioProducer = await this.client.transports.producer.produce({
kind: "audio",
rtpParameters: {
codecs: [
{
payloadType: 109,
mimeType: "audio/opus",
clockRate: 48000,
channels: 2,
rtcpFeedback: [
{ type: "nack" },
{ type: "transport-cc" },
],
},
],
encodings: [
{
ssrc: d.audio_ssrc,
},
],
headerExtensions: this.client.headerExtensions,
},
paused: false,
});
this.on("close", () => {
transport!.stop();
});
const out = transport.createOutgoingStream(
// @ts-ignore
SemanticSDP.StreamInfo.expand({
id: "out" + this.user_id,
// @ts-ignore
tracks: [],
}),
console.debug(
`audioProducer(dump)`,
JSON.stringify(await audioProducer.dump(), null, 4),
);
this.client.out.stream = out;
const clients = channels.get(channel_id)!;
audioProducer.enableTraceEvent([
"fir",
"keyframe",
"nack",
"pli",
"rtp",
"sr",
]);
clients.forEach((client) => {
audioProducer.on("trace", (trace) => {
console.debug(
`audioProducer(trace):`,
JSON.stringify(trace, null, 4),
);
});
// for (const event of audioProducer.eventNames()) {
// if (typeof event !== "string") continue;
// audioProducer.on(event as any, (...args) => {
// console.debug(`audioproducer event: ${event}`, args);
// });
// }
this.client.producers.set(d.audio_ssrc, audioProducer);
}
const clients = getClients(this.client.channel_id);
console.log(`there are ${clients.size} clients`);
clients.forEach(async (client) => {
if (client.websocket.user_id === this.user_id) return;
if (!client.in.stream) return;
client.in.stream?.getTracks().forEach((track) => {
attachTrack.call(this, track, client.websocket.user_id);
});
});
}
// if (videoProducer) {
// console.debug(`consuming video for ${client.websocket.user_id}`);
// const videoConsumer = await client.transports.producer.consume({
// producerId: videoProducer.id,
// rtpCapabilities: router.rtpCapabilities,
// paused: false,
// });
if (d.audio_ssrc) {
handleSSRC.call(this, "audio", {
media: d.audio_ssrc,
rtx: d.audio_ssrc + 1,
});
}
if (d.video_ssrc && d.rtx_ssrc) {
handleSSRC.call(this, "video", {
media: d.video_ssrc,
rtx: d.rtx_ssrc,
});
}
}
// client.consumers.set(videoConsumer.id, videoConsumer);
// }
function attachTrack(
this: WebSocket,
track: IncomingStreamTrack,
user_id: string,
) {
if (!this.client) return;
const outTrack = this.client.transport!.createOutgoingStreamTrack(
track.getMedia(),
let ssrc = d.audio_ssrc;
if (audioProducer) {
console.debug(`consuming audio for ${client.websocket.user_id}`);
const audioConsumer = await client.transports.producer.consume({
producerId: audioProducer.id,
rtpCapabilities: router.rtpCapabilities,
paused: false,
});
console.log(audioConsumer as any);
ssrc = (audioConsumer as any).consumableRtpEncodings[0].ssrc;
console.debug(
`audioConsumer(dump; ${client.websocket.user_id})`,
JSON.stringify(await audioConsumer.dump(), null, 4),
);
outTrack.attachTo(track);
this.client.out.stream!.addTrack(outTrack);
var ssrcs = this.client.out.tracks.get(user_id)!;
if (!ssrcs)
ssrcs = this.client.out.tracks
.set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 })
.get(user_id)!;
if (track.getMedia() === "audio") {
ssrcs.audio_ssrc = outTrack.getSSRCs().media!;
} else if (track.getMedia() === "video") {
ssrcs.video_ssrc = outTrack.getSSRCs().media!;
ssrcs.rtx_ssrc = outTrack.getSSRCs().rtx!;
client.consumers.set(audioConsumer.id, audioConsumer);
}
Send(this, {
console.log(`sending video payload to ${client.websocket.user_id}`);
Send(client.websocket, {
op: VoiceOPCodes.VIDEO,
d: {
user_id: user_id,
...ssrcs,
user_id: client.websocket.user_id,
streams: d.streams!,
audio_ssrc: ssrc,
// video_ssrc: d.video_ssrc,
} as VoiceVideoSchema,
});
}
function handleSSRC(this: WebSocket, type: "audio" | "video", ssrcs: SSRCs) {
if (!this.client) return;
const stream = this.client.in.stream!;
const transport = this.client.transport!;
const id = type + ssrcs.media;
var track = stream.getTrack(id);
if (!track) {
console.log("createIncomingStreamTrack", id);
track = transport.createIncomingStreamTrack(type, { id, ssrcs });
stream.addTrack(track);
const clients = getClients(this.client.channel_id)!;
clients.forEach((client) => {
if (client.websocket.user_id === this.user_id) return;
if (!client.out.stream) return;
attachTrack.call(this, track, client.websocket.user_id);
});
}
}

View File

@ -29,7 +29,7 @@ export enum VoiceOPCodes {
SELECT_PROTOCOL = 1,
READY = 2,
HEARTBEAT = 3,
SESSION_DESCRIPTION = 4,
SELECT_PROTOCOL_ACK = 4,
SPEAKING = 5,
HEARTBEAT_ACK = 6,
RESUME = 7,
@ -41,4 +41,17 @@ export enum VoiceOPCodes {
MEDIA_SINK_WANTS = 15,
VOICE_BACKEND_VERSION = 16,
CHANNEL_OPTIONS_UPDATE = 17,
FLAGS = 18,
SPEED_TEST = 19,
PLATFORM = 20,
SECURE_FRAMES_PREPARE_PROTOCOL_TRANSITION = 21,
SECURE_FRAMES_EXECUTE_TRANSITION = 22,
SECURE_FRAMES_READY_FOR_TRANSITION = 23,
SECURE_FRAMES_PREPARE_EPOCH = 24,
MLS_EXTERNAL_SENDER_PACKAGE = 25,
MLS_KEY_PACKAGE = 26,
MLS_PROPOSALS = 27,
MLS_COMMIT_WELCOME = 28,
MLS_PREPARE_COMMIT_TRANSITION = 29,
MLS_WELCOME = 30,
}

View File

@ -15,63 +15,387 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { WebSocket } from "@spacebar/gateway";
import MediaServer, {
IncomingStream,
OutgoingStream,
Transport,
} from "medooze-media-server";
import SemanticSDP from "semantic-sdp";
MediaServer.enableLog(true);
export const PublicIP = process.env.PUBLIC_IP || "127.0.0.1";
try {
const range = process.env.WEBRTC_PORT_RANGE || "4000";
var ports = range.split("-");
const min = Number(ports[0]);
const max = Number(ports[1]);
MediaServer.setPortRange(min, max);
} catch (error) {
console.error(
"Invalid env var: WEBRTC_PORT_RANGE",
process.env.WEBRTC_PORT_RANGE,
error,
);
process.exit(1);
}
export const endpoint = MediaServer.createEndpoint(PublicIP);
import * as mediasoup from "mediasoup";
import { types as MediaSoupTypes } from "mediasoup";
import * as sdpTransform from "sdp-transform";
export const channels = new Map<string, Set<Client>>();
export const workers: MediaSoupTypes.Worker<AppData>[] = [];
export const routers = new Map<string, MediaSoupTypes.Router>();
export let nextWorkerIdx = 0;
export interface Client {
transport?: Transport;
websocket: WebSocket;
out: {
stream?: OutgoingStream;
tracks: Map<
string,
{
audio_ssrc: number;
video_ssrc: number;
rtx_ssrc: number;
}
>;
};
in: {
stream?: IncomingStream;
audio_ssrc: number;
video_ssrc: number;
rtx_ssrc: number;
};
sdp: SemanticSDP.SDPInfo;
ssrc: number;
sdp?: sdpTransform.SessionDescription;
channel_id: string;
headerExtensions: MediaSoupTypes.RtpHeaderExtensionParameters[];
// secret_key?: Uint8Array;
codecs: Codec[];
streams: Stream[];
producers: Map<number, MediaSoupTypes.Producer>;
consumers: Map<string, MediaSoupTypes.Consumer>;
transports: {
producer: MediaSoupTypes.WebRtcTransport;
consumer?: MediaSoupTypes.WebRtcTransport;
};
}
export function getClients(channel_id: string) {
if (!channels.has(channel_id)) channels.set(channel_id, new Set());
return channels.get(channel_id)!;
}
export function getRouter(channelId: string) {
return routers.get(channelId);
}
export async function getOrCreateRouter(channel_id: string) {
if (!routers.has(channel_id)) {
const worker = getWorker();
const router = await worker.createRouter({
mediaCodecs: MEDIA_CODECS,
});
routers.set(channel_id, router);
return router;
}
return routers.get(channel_id)!;
}
export function getWorker() {
const worker = workers[nextWorkerIdx];
if (++nextWorkerIdx === workers.length) nextWorkerIdx = 0;
return worker;
}
export async function createWorkers() {
const numWorkers = 1;
for (let i = 0; i < numWorkers; i++) {
const worker = await mediasoup.createWorker({
logLevel: "debug",
logTags: [
"info",
"ice",
"dtls",
"rtp",
"srtp",
"rtcp",
"rtx",
"bwe",
"score",
"simulcast",
"svc",
"sctp",
],
});
worker.on("died", () => {
console.error(
"mediasoup Worker died, exiting in 2 seconds... [pid:%d]",
worker.pid,
);
setTimeout(() => process.exit(1), 2000);
});
worker.observer.on("newrouter", (router) => {
console.debug("new router [pid:%d]: %s", worker.pid, router.id);
router.observer.on("newrtpobserver", (rtpObserver) => {
console.debug(
"new RtpObserver [pid:%d]: %s",
worker.pid,
rtpObserver.id,
);
});
router.observer.on("newtransport", async (transport) => {
console.debug(
"new transport [pid:%d]: %s",
worker.pid,
transport.id,
);
await transport.enableTraceEvent();
(transport as MediaSoupTypes.WebRtcTransport).on(
"iceselectedtuplechange",
(tuple) => {
console.log(`transport(iceselectedtuplechange)`, tuple);
},
);
(transport as MediaSoupTypes.WebRtcTransport).on(
"icestatechange",
(icestate) => {
console.log(`transport(ice state change)`, icestate);
},
);
(transport as MediaSoupTypes.WebRtcTransport).on(
"trace",
(trace) => {
console.log(`transport(trace)`, trace);
},
);
(transport as MediaSoupTypes.WebRtcTransport).on(
"dtlsstatechange",
(dtlsstate) => {
console.log(`transport(dtls state change)`, dtlsstate);
},
);
(transport as MediaSoupTypes.WebRtcTransport).on(
"sctpstatechange",
(sctpstate) => {
console.log(`transport(sctp state change)`, sctpstate);
},
);
transport.observer.on("newproducer", (producer) => {
console.debug(
"new Producer [pid:%d]: %s",
worker.pid,
producer.id,
);
producer.on("score", (score) => {
console.log(`transport producer(score)`, score);
});
producer.on("trace", (trace) => {
console.log(`transport producer(trace)`, trace);
});
producer.on("videoorientationchange", (orientation) => {
console.log(
`transport producer(videoorientationchange)`,
orientation,
);
});
});
transport.observer.on("newconsumer", (consumer) => {
console.debug(
"new Consumer [pid:%d]: %s",
worker.pid,
consumer.id,
);
consumer.on("rtp", (rtpPacket) => {
console.log(`transport consumer(rtp)`, rtpPacket);
});
consumer.on("trace", (trace) => {
console.log(`transport consumer(trace)`, trace);
});
consumer.on("score", (score) => {
console.log(`transport consumer(score)`, score);
});
consumer.on("layerschange", (layers) => {
console.log(`transport consumer(layerschange)`, layers);
});
});
});
});
worker.observer.on("newwebrtcserver", (webRtcServer) => {
console.debug(
"new WebRtcServer [pid:%d]: %s",
worker.pid,
webRtcServer.id,
);
});
worker.observer.on("close", () => {
console.debug("mediasoup Worker closed [pid:%d]", worker.pid);
});
workers.push(worker);
// Create a WebRtcServer in this Worker.
// Each mediasoup Worker will run its own WebRtcServer, so those cannot
// share the same listening ports. Hence we increase the value in config.js
// for each Worker.
const webRtcServerOptions: MediaSoupTypes.WebRtcServerOptions = {
listenInfos: [
{
protocol: "udp",
ip: "0.0.0.0",
announcedAddress: "192.168.10.112",
port: 20000,
},
],
};
const webRtcServer = await worker.createWebRtcServer(
webRtcServerOptions,
);
worker.appData.webRtcServer = webRtcServer;
// Log worker resource usage every X seconds.
// setInterval(async () => {
// const usage = await worker.getResourceUsage();
// console.debug(
// "mediasoup Worker resource usage [pid:%d]: %o",
// worker.pid,
// usage,
// );
// const dump = await worker.dump();
// console.debug(
// "mediasoup Worker dump [pid:%d]: %o",
// worker.pid,
// dump,
// );
// }, 120000);
}
}
export interface AppData extends MediaSoupTypes.AppData {
webRtcServer?: MediaSoupTypes.WebRtcServer;
}
export interface Codec {
name: "opus" | "VP8" | "VP9" | "H264";
type: "audio" | "video";
priority: number;
payload_type: number;
rtx_payload_type?: number | null;
}
export const MEDIA_CODECS: MediaSoupTypes.RtpCodecCapability[] = [
{
kind: "audio",
mimeType: "audio/opus",
clockRate: 48000,
channels: 2,
rtcpFeedback: [{ type: "nack" }, { type: "transport-cc" }],
},
{
kind: "audio",
mimeType: "audio/multiopus",
clockRate: 48000,
channels: 4,
// Quad channel.
parameters: {
channel_mapping: "0,1,2,3",
num_streams: 2,
coupled_streams: 2,
},
rtcpFeedback: [{ type: "nack" }, { type: "transport-cc" }],
},
{
kind: "audio",
mimeType: "audio/multiopus",
clockRate: 48000,
channels: 6,
// 5.1.
parameters: {
channel_mapping: "0,4,1,2,3,5",
num_streams: 4,
coupled_streams: 2,
},
rtcpFeedback: [{ type: "nack" }, { type: "transport-cc" }],
},
{
kind: "audio",
mimeType: "audio/multiopus",
clockRate: 48000,
channels: 8,
// 7.1.
parameters: {
channel_mapping: "0,6,1,2,3,4,5,7",
num_streams: 5,
coupled_streams: 3,
},
rtcpFeedback: [{ type: "nack" }, { type: "transport-cc" }],
},
{
kind: "video",
mimeType: "video/VP8",
clockRate: 90000,
rtcpFeedback: [
{ type: "nack" },
{ type: "nack", parameter: "pli" },
{ type: "ccm", parameter: "fir" },
{ type: "goog-remb" },
{ type: "transport-cc" },
],
},
{
kind: "video",
mimeType: "video/VP9",
clockRate: 90000,
rtcpFeedback: [
{ type: "nack" },
{ type: "nack", parameter: "pli" },
{ type: "ccm", parameter: "fir" },
{ type: "goog-remb" },
{ type: "transport-cc" },
],
},
{
kind: "video",
mimeType: "video/H264",
clockRate: 90000,
parameters: {
"level-asymmetry-allowed": 1,
},
rtcpFeedback: [
{ type: "nack" },
{ type: "nack", parameter: "pli" },
{ type: "ccm", parameter: "fir" },
{ type: "goog-remb" },
{ type: "transport-cc" },
],
},
{
kind: "video",
mimeType: "video/H265",
clockRate: 90000,
parameters: {
"level-asymmetry-allowed": 1,
},
rtcpFeedback: [
{ type: "nack" },
{ type: "nack", parameter: "pli" },
{ type: "ccm", parameter: "fir" },
{ type: "goog-remb" },
{ type: "transport-cc" },
],
},
];
export interface Stream {
type: string;
rid: string; //number
quality: number;
}
export const SUPPORTED_EXTENTIONS = [
"urn:ietf:params:rtp-hdrext:sdes:mid",
"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
"http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07",
"urn:ietf:params:rtp-hdrext:framemarking",
"urn:ietf:params:rtp-hdrext:ssrc-audio-level",
"urn:3gpp:video-orientation",
"urn:ietf:params:rtp-hdrext:toffset",
"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01",
"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
"http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time",
"http://www.webrtc.org/experiments/rtp-hdrext/playout-delay",
];

View File

@ -1,5 +1,4 @@
{
"exclude": ["./src/webrtc"],
"include": ["./src"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
@ -37,7 +36,8 @@
"@spacebar/api*": ["./api"],
"@spacebar/gateway*": ["./gateway"],
"@spacebar/cdn*": ["./cdn"],
"@spacebar/util*": ["./util"]
"@spacebar/util*": ["./util"],
"@spacebar/webrtc*": ["./webrtc"]
} /* Specify a set of entries that re-map imports to additional lookup locations. */,
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */