diff --git a/package-lock.json b/package-lock.json
index e0fc485..000f067 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"decentraland-ui": "^4.0.0",
"ethers": "^6.6.5",
"livekit-client": "^1.12.3",
+ "livekit-server-sdk": "^1.2.5",
"p-queue": "^6.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -7256,6 +7257,11 @@
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
@@ -7366,6 +7372,45 @@
"node": ">=6"
}
},
+ "node_modules/camelcase-keys": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz",
+ "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==",
+ "dependencies": {
+ "camelcase": "^6.3.0",
+ "map-obj": "^4.1.0",
+ "quick-lru": "^5.1.1",
+ "type-fest": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase-keys/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase-keys/node_modules/type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001514",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001514.tgz",
@@ -8692,6 +8737,14 @@
"node": ">=4.0.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -14179,6 +14232,21 @@
"node": "*"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
+ "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash": "^4.17.21",
+ "ms": "^2.1.1",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
"node_modules/jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -14193,6 +14261,25 @@
"node": ">=0.6.0"
}
},
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/keccak": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.3.tgz",
@@ -14417,6 +14504,73 @@
"webrtc-adapter": "^8.1.1"
}
},
+ "node_modules/livekit-server-sdk": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-1.2.5.tgz",
+ "integrity": "sha512-QYHGEoilSAXUQQBAZE2SXU1oXW8z08VFp2UxcZzXdPt3u4E9xamghTMhgniLMWmpSCl7oqVObQ6XXnK9rkr0Pg==",
+ "dependencies": {
+ "axios": "^1.3.6",
+ "camelcase-keys": "^7.0.0",
+ "jsonwebtoken": "^9.0.0",
+ "protobufjs": "^7.2.4"
+ }
+ },
+ "node_modules/livekit-server-sdk/node_modules/@types/node": {
+ "version": "20.4.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.7.tgz",
+ "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g=="
+ },
+ "node_modules/livekit-server-sdk/node_modules/axios": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/livekit-server-sdk/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/livekit-server-sdk/node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
+ "node_modules/livekit-server-sdk/node_modules/protobufjs": {
+ "version": "7.2.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz",
+ "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -14608,6 +14762,17 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/map-obj": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
+ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/matchmediaquery": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz",
@@ -16017,6 +16182,11 @@
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.1.tgz",
"integrity": "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA=="
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -25071,6 +25241,11 @@
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
+ "buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
@@ -25153,6 +25328,29 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
+ "camelcase-keys": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz",
+ "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==",
+ "requires": {
+ "camelcase": "^6.3.0",
+ "map-obj": "^4.1.0",
+ "quick-lru": "^5.1.1",
+ "type-fest": "^1.2.1"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
+ },
+ "type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="
+ }
+ }
+ },
"caniuse-lite": {
"version": "1.0.30001514",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001514.tgz",
@@ -26172,6 +26370,14 @@
}
}
},
+ "ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -30320,6 +30526,17 @@
"through": ">=2.2.7 <3"
}
},
+ "jsonwebtoken": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
+ "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==",
+ "requires": {
+ "jws": "^3.2.2",
+ "lodash": "^4.17.21",
+ "ms": "^2.1.1",
+ "semver": "^7.3.8"
+ }
+ },
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -30331,6 +30548,25 @@
"verror": "1.10.0"
}
},
+ "jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "requires": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "requires": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"keccak": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.3.tgz",
@@ -30542,6 +30778,68 @@
"webrtc-adapter": "^8.1.1"
}
},
+ "livekit-server-sdk": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/livekit-server-sdk/-/livekit-server-sdk-1.2.5.tgz",
+ "integrity": "sha512-QYHGEoilSAXUQQBAZE2SXU1oXW8z08VFp2UxcZzXdPt3u4E9xamghTMhgniLMWmpSCl7oqVObQ6XXnK9rkr0Pg==",
+ "requires": {
+ "axios": "^1.3.6",
+ "camelcase-keys": "^7.0.0",
+ "jsonwebtoken": "^9.0.0",
+ "protobufjs": "^7.2.4"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "20.4.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.7.tgz",
+ "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g=="
+ },
+ "axios": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "requires": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
+ "protobufjs": {
+ "version": "7.2.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz",
+ "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
+ "requires": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ }
+ }
+ }
+ },
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -30701,6 +30999,11 @@
"tmpl": "1.0.5"
}
},
+ "map-obj": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
+ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="
+ },
"matchmediaquery": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz",
@@ -31771,6 +32074,11 @@
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.1.tgz",
"integrity": "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA=="
},
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
diff --git a/package.json b/package.json
index 0995064..6945c8a 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"decentraland-ui": "^4.0.0",
"ethers": "^6.6.5",
"livekit-client": "^1.12.3",
+ "livekit-server-sdk": "^1.2.5",
"p-queue": "^6.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/src/components/Pages/Conference/Conference.container.ts b/src/components/Pages/Conference/Conference.container.ts
new file mode 100644
index 0000000..8551f67
--- /dev/null
+++ b/src/components/Pages/Conference/Conference.container.ts
@@ -0,0 +1,24 @@
+import { connect } from "react-redux"
+import { getAddress, isConnecting } from "decentraland-dapps/dist/modules/wallet/selectors"
+import { isLoggingIn } from "../../../modules/identity/selector"
+import { getServer, getToken } from "../../../modules/conference/selector"
+import { RootState } from "../../../modules/reducer"
+import withRouter from "../../../utils/WithRouter"
+import Conference from "./Conference"
+import { MapDispatch, MapStateProps, OwnProps } from "./Conference.types"
+
+const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps => {
+ const addressFromPath = ownProps.router.params.profileAddress
+
+ return {
+ profileAddress: addressFromPath?.toLowerCase(),
+ isLoading: isLoggingIn(state) || isConnecting(state),
+ loggedInAddress: getAddress(state)?.toLowerCase(),
+ server: getServer(state),
+ token: getToken(state),
+ }
+}
+
+const mapDispatch = (dispatch: MapDispatch): any => ({})
+
+export default withRouter(connect(mapStateToProps, mapDispatch)(Conference))
diff --git a/src/components/Pages/Conference/Conference.tsx b/src/components/Pages/Conference/Conference.tsx
new file mode 100644
index 0000000..ae99abd
--- /dev/null
+++ b/src/components/Pages/Conference/Conference.tsx
@@ -0,0 +1,15 @@
+import React from "react"
+import { Props } from "./Conference.types"
+import { LiveKitRoom, VideoConference } from "@livekit/components-react"
+
+export default function Conference(props: Props) {
+ const { token, server } = props
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/components/Pages/Conference/Conference.types.ts b/src/components/Pages/Conference/Conference.types.ts
new file mode 100644
index 0000000..5038d25
--- /dev/null
+++ b/src/components/Pages/Conference/Conference.types.ts
@@ -0,0 +1,19 @@
+import { Dispatch } from "redux"
+import { RouterProps } from "../../../utils/WithRouter"
+
+export type Props = {
+ loggedInAddress?: string
+ profileAddress?: string
+ isLoading: boolean
+ server?: string
+ token?: string
+}
+
+export type MapStateProps = Pick
+export type MapDispatch = Dispatch
+type Params = {
+ profileAddress?: string
+}
+export type OwnProps = {
+ router: RouterProps
+}
diff --git a/src/components/Pages/Conference/index.ts b/src/components/Pages/Conference/index.ts
new file mode 100644
index 0000000..8316fe6
--- /dev/null
+++ b/src/components/Pages/Conference/index.ts
@@ -0,0 +1,2 @@
+import Conference from "./Conference.container"
+export default Conference
diff --git a/src/components/Pages/MainPage/MainPage.container.ts b/src/components/Pages/MainPage/MainPage.container.ts
index 7bab6c7..244051b 100644
--- a/src/components/Pages/MainPage/MainPage.container.ts
+++ b/src/components/Pages/MainPage/MainPage.container.ts
@@ -1,24 +1,29 @@
import { connect } from "react-redux"
-import { loadProfileRequest } from "decentraland-dapps/dist/modules/profile/actions"
import { getAddress, isConnecting } from "decentraland-dapps/dist/modules/wallet/selectors"
-import { isLoggingIn } from "../../../modules/identity/selector"
+import { getCurrentIdentity, isLoggingIn } from "../../../modules/identity/selector"
import { RootState } from "../../../modules/reducer"
import withRouter from "../../../utils/WithRouter"
import MainPage from "./MainPage"
import { MapDispatch, MapDispatchProps, MapStateProps, OwnProps } from "./MainPage.types"
+import { setServer, setToken } from "../../../modules/conference/action"
const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps => {
const addressFromPath = ownProps.router.params.profileAddress
+ const identity = getCurrentIdentity(state)
return {
profileAddress: addressFromPath?.toLowerCase(),
isLoading: isLoggingIn(state) || isConnecting(state),
loggedInAddress: getAddress(state)?.toLowerCase(),
+ identity,
}
}
const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
- onFetchProfile: (address) => dispatch(loadProfileRequest(address)),
+ onSubmitConnectForm: (server: string, token:string) => {
+ dispatch(setServer({ server }))
+ dispatch(setToken({ token }))
+ },
})
export default withRouter(connect(mapStateToProps, mapDispatch)(MainPage))
diff --git a/src/components/Pages/MainPage/MainPage.tsx b/src/components/Pages/MainPage/MainPage.tsx
index 36acc8d..d4feb8e 100644
--- a/src/components/Pages/MainPage/MainPage.tsx
+++ b/src/components/Pages/MainPage/MainPage.tsx
@@ -1,30 +1,26 @@
-import React, { useCallback, useEffect, useState } from "react"
+import React, { ChangeEvent, useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import Divider from "semantic-ui-react/dist/commonjs/elements/Divider/Divider"
import { t } from "decentraland-dapps/dist/modules/translation/utils"
-import { Tabs } from "decentraland-ui/dist/components/Tabs/Tabs"
import { Loader } from "decentraland-ui"
import { locations } from "../../../modules/routing/locations"
import { getView } from "../../../utils/view"
import { PageLayout } from "../../PageLayout"
import { Props } from "./MainPage.types"
import styles from "./MainPage.module.css"
-import { LiveKitRoom, VideoConference } from "@livekit/components-react"
+import { flatFetch } from "../../../utils/flat-fetch"
+import { signedFetch } from "../../../utils/auth"
+import { AuthIdentity } from "@dcl/crypto"
function MainPage(props: Props) {
- const { isLoading, onFetchProfile, profileAddress, loggedInAddress } = props
+ const [selectedServer, setSelectedServer] = useState("")
+ const [isConnectingToServer, setIsConnectingToServer] = useState(false)
+
+ const { isLoading, onFetchProfile, profileAddress, loggedInAddress, identity, onSubmitConnectForm } = props
const tabs: { displayValue: string; value: string }[] = [
{ displayValue: t("tabs.overview"), value: t("tabs.overview") },
]
- const [selectedTab, setSelectedTab] = useState(tabs[0].value)
-
- const handleTabChange = useCallback(
- (tab: string) => {
- setSelectedTab(tab)
- },
- [setSelectedTab]
- )
const navigate = useNavigate()
useEffect(() => {
@@ -40,24 +36,73 @@ function MainPage(props: Props) {
}, [isLoading, loggedInAddress, profileAddress])
const view = getView(loggedInAddress, profileAddress)
+ const handleChange = (e: ChangeEvent) => {
+ setSelectedServer(e.target.value)
+ }
+
+ async function livekitConnect(identity: AuthIdentity, worldServer: string, worldName: string) {
+ const aboutResponse = await flatFetch(`${worldServer}/world/${worldName}/about`)
+ console.log(aboutResponse.text)
+ if (aboutResponse.status === 200) {
+ const url = JSON.parse(aboutResponse.text!)
+ ["comms"]["fixedAdapter"].replace("signed-login:", "")
+ .replace("get-comms-adapter", "cast-adapter")
+ const response = await signedFetch(
+ url,
+ identity,
+ {
+ method: "POST",
+ },
+ {
+ signer: "dcl:explorer",
+ intent: "dcl:explorer:comms-handshake",
+ }
+ )
+
+ if (response.status === 200) {
+ console.log(response.text)
+ return JSON.parse(response.text!)
+ } else {
+ let message = ""
+ try {
+ message = JSON.parse(response.text || "")?.message
+ } catch (e) {
+ message = response.text || ""
+ }
+ throw Error(message)
+ }
+ // throw Error(`Failed to connect to LiveKit: ${JSON.stringify(response.text || response.json?.message)}`)
+ } else if (aboutResponse.status === 404) {
+ throw Error(`World ${worldName} not found`)
+ }
+ throw Error("An error has occurred")
+ }
+
+ const handleClick = () => {
+ livekitConnect(identity!, "https://worlds-content-server.decentraland.zone", selectedServer)
+ .then((response: { url: string; token: string }) => {
+ onSubmitConnectForm(response.url, response.token)
+ navigate(`/meet/${encodeURIComponent(response.url)}?token=${encodeURIComponent(response.token)}`)
+ })
+ .catch((err) => {
+ console.error("ERROR livekit connect", err)
+ })
+ }
+
return (
- {isLoading ? (
+ {isLoading || isConnectingToServer ? (
) : (
-
- {tabs.map((tab) => (
- handleTabChange(tab.value)}>
- {tab.displayValue}
-
- ))}
-
-
-
-
+
+
+
+
+
+
)}
diff --git a/src/components/Pages/MainPage/MainPage.types.ts b/src/components/Pages/MainPage/MainPage.types.ts
index 0447dad..7c478f8 100644
--- a/src/components/Pages/MainPage/MainPage.types.ts
+++ b/src/components/Pages/MainPage/MainPage.types.ts
@@ -1,17 +1,20 @@
-import { Dispatch } from 'redux'
-import { LoadProfileRequestAction, loadProfileRequest } from 'decentraland-dapps/dist/modules/profile/actions'
-import { RouterProps } from '../../../utils/WithRouter'
+import { Dispatch } from "redux"
+import { loadProfileRequest } from "decentraland-dapps/dist/modules/profile/actions"
+import { RouterProps } from "../../../utils/WithRouter"
+import { AuthIdentity } from "@dcl/crypto"
export type Props = {
onFetchProfile: typeof loadProfileRequest
loggedInAddress?: string
profileAddress?: string
isLoading: boolean
+ onSubmitConnectForm: (server: string, token: string) => void
+ identity: AuthIdentity | null
}
-export type MapStateProps = Pick
-export type MapDispatchProps = Pick
-export type MapDispatch = Dispatch
+export type MapStateProps = Pick
+export type MapDispatchProps = Pick
+export type MapDispatch = Dispatch
type Params = {
profileAddress?: string
}
diff --git a/src/main.tsx b/src/main.tsx
index 5bcbae4..a7db6c0 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -15,6 +15,7 @@ import * as locales from "./modules/translation/locales"
// These CSS styles must be defined last to avoid overriding other styles
import "decentraland-ui/dist/themes/alternative/dark-theme.css"
import "./index.css"
+import Conference from "./components/Pages/Conference"
const router = createBrowserRouter([
{
@@ -29,6 +30,10 @@ const router = createBrowserRouter([
path: "sign-in",
element: ,
},
+ {
+ path: "/meet/:server",
+ element: ,
+ },
])
const component = (
diff --git a/src/modules/conference/action.ts b/src/modules/conference/action.ts
new file mode 100644
index 0000000..399624e
--- /dev/null
+++ b/src/modules/conference/action.ts
@@ -0,0 +1,9 @@
+import { createAction } from "@reduxjs/toolkit"
+
+export const setServer = createAction<{ server: string }>("Set Server")
+
+export type SetServerAction = ReturnType
+
+export const setToken = createAction<{ token: string }>("Set Token")
+
+export type SetTokenAction = ReturnType
diff --git a/src/modules/conference/reducer.ts b/src/modules/conference/reducer.ts
new file mode 100644
index 0000000..57197e5
--- /dev/null
+++ b/src/modules/conference/reducer.ts
@@ -0,0 +1,22 @@
+import { createReducer } from "@reduxjs/toolkit"
+import { setServer, setToken } from "./action"
+
+export type ConferenceState = {
+ token: string
+ server: string
+}
+
+export const INITIAL_STATE: ConferenceState = {
+ token: "",
+ server: "",
+}
+
+export const conferenceReducer = createReducer(INITIAL_STATE, (builder) =>
+ builder
+ .addCase(setServer, (state, action) => {
+ state.server = action.payload.server
+ })
+ .addCase(setToken, (state, action) => {
+ state.token = action.payload.token
+ })
+)
diff --git a/src/modules/conference/selector.ts b/src/modules/conference/selector.ts
new file mode 100644
index 0000000..1fb1729
--- /dev/null
+++ b/src/modules/conference/selector.ts
@@ -0,0 +1,9 @@
+import { createSelector } from "@reduxjs/toolkit"
+import { RootState } from "../reducer"
+
+const getState = (state: RootState) => state.conference
+
+export const getToken = (state: RootState) => getState(state).token
+export const getServer = (state: RootState) => getState(state).server
+
+export const isLoading = createSelector([getToken], (token) => !!token)
diff --git a/src/modules/reducer.ts b/src/modules/reducer.ts
index 488db7a..eb6214a 100644
--- a/src/modules/reducer.ts
+++ b/src/modules/reducer.ts
@@ -13,6 +13,7 @@ import {
} from "decentraland-dapps/dist/modules/translation/reducer"
import { WalletState, walletReducer as wallet } from "decentraland-dapps/dist/modules/wallet/reducer"
import { IdentityState, identityReducer as identity } from "./identity/reducer"
+import { ConferenceState, conferenceReducer as conference } from "./conference/reducer"
export const createRootReducer = (middlewares: Middleware[], preloadedState = {}) =>
configureStore({
@@ -25,6 +26,7 @@ export const createRootReducer = (middlewares: Middleware[], preloadedState = {}
translation: translation as Reducer,
profile,
identity,
+ conference,
})
),
preloadedState,
@@ -54,6 +56,7 @@ export type RootState = {
translation: TranslationState
wallet: WalletState
features: FeaturesState
+ conference: ConferenceState
}
export type RootStore = Store
diff --git a/src/modules/routing/locations.ts b/src/modules/routing/locations.ts
index 86f5d81..27b8cb7 100644
--- a/src/modules/routing/locations.ts
+++ b/src/modules/routing/locations.ts
@@ -1,7 +1,8 @@
export const locations = {
- root: () => '/',
+ root: () => "/",
account: (address: string) => `/accounts/${address}`,
signIn: (redirectTo?: string) => {
- return `/sign-in${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ''}`
- }
+ return `/sign-in${redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : ""}`
+ },
+ meet: (server: string, token: string) => `/meet/${server}?token${token}`,
}
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
new file mode 100644
index 0000000..6abc146
--- /dev/null
+++ b/src/utils/auth.ts
@@ -0,0 +1,121 @@
+import { RequestManager } from "eth-connect"
+import { createUnsafeIdentity } from "@dcl/crypto/dist/crypto"
+import { FlatFetchInit, flatFetch } from "./flat-fetch"
+import { AuthChain, AuthIdentity, Authenticator } from "@dcl/crypto"
+
+const ephemeralLifespanMinutes = 10_000
+
+export type ExplorerIdentity = {
+ // public address of the first elemement of the authChain
+ address: string
+ // is the address an ephemeral address? used to determine if the user is a guest
+ isGuest: boolean
+ // the authChain is the chain of signatures that prove the ownership of the address
+ authChain: AuthIdentity
+ // the signer function will be used to sign messages using the last element of the authChain
+ signer: (message: string) => Promise
+}
+
+export async function getUserAccount(
+ requestManager: RequestManager,
+ returnChecksum: boolean
+): Promise {
+ try {
+ const accounts = await requestManager.eth_accounts()
+
+ if (!accounts || accounts.length === 0) {
+ return undefined
+ }
+
+ return returnChecksum ? accounts[0] : accounts[0].toLowerCase()
+ } catch (error: any) {
+ throw new Error(`Could not access eth_accounts: "${error.message}"`)
+ }
+}
+
+const AUTH_CHAIN_HEADER_PREFIX = "x-identity-auth-chain-"
+const AUTH_TIMESTAMP_HEADER = "x-identity-timestamp"
+const AUTH_METADATA_HEADER = "x-identity-metadata"
+
+export function getAuthChainSignature(
+ method: string,
+ path: string,
+ metadata: string,
+ chainProvider: (payload: string) => AuthChain
+) {
+ const timestamp = Date.now()
+ const payloadParts = [method.toLowerCase(), path.toLowerCase(), timestamp.toString(), metadata]
+ const payloadToSign = payloadParts.join(":").toLowerCase()
+ const authChain = chainProvider(payloadToSign)
+
+ return {
+ authChain,
+ metadata,
+ timestamp,
+ }
+}
+
+export function getSignedHeaders(
+ method: string,
+ path: string,
+ metadata: Record,
+ chainProvider: (payload: string) => AuthChain
+) {
+ const headers: Record = {}
+ const signature = getAuthChainSignature(method, path, JSON.stringify(metadata), chainProvider)
+ signature.authChain.forEach((link, index) => {
+ headers[`${AUTH_CHAIN_HEADER_PREFIX}${index}`] = JSON.stringify(link)
+ })
+
+ headers[AUTH_TIMESTAMP_HEADER] = signature.timestamp.toString()
+ headers[AUTH_METADATA_HEADER] = signature.metadata
+ return headers
+}
+
+export function signedFetch(
+ url: string,
+ identity: AuthIdentity,
+ init?: FlatFetchInit,
+ additionalMetadata: Record = {}
+) {
+ const path = new URL(url).pathname
+
+ const actualInit = {
+ ...init,
+ headers: {
+ ...getSignedHeaders(
+ init?.method ?? "get",
+ path,
+ {
+ origin: location.origin,
+ ...additionalMetadata,
+ },
+ (payload) => Authenticator.signPayload(identity, payload)
+ ),
+ ...init?.headers,
+ },
+ } as FlatFetchInit
+
+ return flatFetch(url, actualInit)
+}
+
+// this function creates a Decentraland AuthChain using a signer function.
+// the signer function is only used once, to sign the ephemeral private key. after that,
+// the ephemeral private key is used to sign the rest of the authChain and subsequent
+// messages. this is a good way to not over-expose the real user accounts to excessive
+// signing requests.
+export async function identityFromSigner(
+ address: string,
+ signer: (message: string) => Promise
+): Promise {
+ const ephemeral = createUnsafeIdentity()
+
+ const authChain = await Authenticator.initializeAuthChain(address, ephemeral, ephemeralLifespanMinutes, signer)
+
+ return {
+ address,
+ signer,
+ authChain,
+ isGuest: true,
+ }
+}
diff --git a/src/utils/client-utils.ts b/src/utils/client-utils.ts
new file mode 100644
index 0000000..cdee23f
--- /dev/null
+++ b/src/utils/client-utils.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from "react"
+
+export function useServerUrl(region?: string) {
+ const [serverUrl, setServerUrl] = useState()
+ useEffect(() => {
+ let endpoint = `/api/url`
+ if (region) {
+ endpoint += `?region=${region}`
+ }
+ fetch(endpoint).then(async (res) => {
+ if (res.ok) {
+ const body = await res.json()
+ console.log(body)
+ setServerUrl(body.url)
+ } else {
+ throw Error("Error fetching server url, check server logs")
+ }
+ })
+ })
+ return serverUrl
+}
diff --git a/src/utils/flat-fetch.ts b/src/utils/flat-fetch.ts
new file mode 100644
index 0000000..32c9cf1
--- /dev/null
+++ b/src/utils/flat-fetch.ts
@@ -0,0 +1,40 @@
+type FlatFetchResponse = {
+ ok: boolean
+ status: number
+ statusText: string
+ headers: Record
+ json?: any
+ text?: string
+}
+
+type BodyType = "json" | "text"
+
+export type FlatFetchInit = RequestInit & { responseBodyType?: BodyType }
+
+export async function flatFetch(url: string, init?: FlatFetchInit): Promise {
+ const response = await fetch(url, init)
+
+ const responseBodyType = init?.responseBodyType || "text"
+
+ const headers: Record = {}
+
+ response.headers.forEach((value, key) => (headers[key] = value))
+
+ const flatFetchResponse: FlatFetchResponse = {
+ ok: response.ok,
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ }
+
+ switch (responseBodyType) {
+ case "json":
+ flatFetchResponse.json = await response.json()
+ break
+ case "text":
+ flatFetchResponse.text = await response.text()
+ break
+ }
+
+ return flatFetchResponse
+}
diff --git a/src/utils/server-utils.ts b/src/utils/server-utils.ts
new file mode 100644
index 0000000..6d91daa
--- /dev/null
+++ b/src/utils/server-utils.ts
@@ -0,0 +1,27 @@
+import { RoomServiceClient } from "livekit-server-sdk"
+
+export function getRoomClient(): RoomServiceClient {
+ checkKeys()
+ return new RoomServiceClient(getLiveKitURL())
+}
+
+export function getLiveKitURL(region?: string | string[]): string {
+ let targetKey = "LIVEKIT_URL"
+ if (region && !Array.isArray(region)) {
+ targetKey = `LIVEKIT_URL_${region}`.toUpperCase()
+ }
+ const url = process.env[targetKey]
+ if (!url) {
+ throw new Error(`${targetKey} is not defined`)
+ }
+ return url
+}
+
+function checkKeys() {
+ if (typeof process.env.LIVEKIT_API_KEY === "undefined") {
+ throw new Error("LIVEKIT_API_KEY is not defined")
+ }
+ if (typeof process.env.LIVEKIT_API_SECRET === "undefined") {
+ throw new Error("LIVEKIT_API_SECRET is not defined")
+ }
+}
diff --git a/src/utils/types.ts b/src/utils/types.ts
new file mode 100644
index 0000000..9a1a6c2
--- /dev/null
+++ b/src/utils/types.ts
@@ -0,0 +1,16 @@
+import { LocalAudioTrack, LocalVideoTrack } from "livekit-client"
+
+export interface SessionProps {
+ roomName: string
+ identity: string
+ audioTrack?: LocalAudioTrack
+ videoTrack?: LocalVideoTrack
+ region?: string
+ turnServer?: RTCIceServer
+ forceRelay?: boolean
+}
+
+export interface TokenResult {
+ identity: string
+ accessToken: string
+}