diff --git a/package-lock.json b/package-lock.json index 50e6913..e10e388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,15 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-slot": "^1.1.0", + "@types/socket.io-client": "^1.4.36", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.468.0", + "mediasoup-client": "^3.7.18", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^7.0.2", + "socket.io-client": "^4.8.1", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7" }, @@ -1332,6 +1336,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1377,6 +1387,21 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "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/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1391,6 +1416,12 @@ "dev": true, "license": "MIT" }, + "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/node": { "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", @@ -1401,6 +1432,13 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/npm-events-package": { + "name": "@types/events", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -1429,6 +1467,12 @@ "@types/react": "*" } }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", @@ -1823,6 +1867,18 @@ "postcss": "^8.1.0" } }, + "node_modules/awaitqueue": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-3.0.2.tgz", + "integrity": "sha512-AVAtRwmf0DNSesMdyanFKKejTrOnjdKtz5LIDQFu2OTUgXvB/CRTYMrkPAF/2GCF9XBtYVxSwxDORlD41S+RyQ==", + "license": "ISC", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2052,6 +2108,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2089,7 +2154,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2110,6 +2174,26 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2141,6 +2225,28 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2380,6 +2486,28 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/fake-mediastreamtrack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.2.0.tgz", + "integrity": "sha512-AxHtlEmka1sqNoe3Ej1H1hJc9gjjO/6vCbCPm4D4QeEXvzhjYumA+iZ7wOi2WrmkAhGElHhBgWoNgJhFccectA==", + "license": "ISC", + "dependencies": { + "event-target-shim": "^6.0.2", + "uuid": "^9.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2640,6 +2768,23 @@ "dev": true, "license": "MIT" }, + "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", @@ -2765,6 +2910,26 @@ "node": ">=0.12.0" } }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2954,6 +3119,44 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/mediasoup-client": { + "version": "3.7.18", + "resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.7.18.tgz", + "integrity": "sha512-cz+c2qM6KGJWKGYxySSSmkNL475mt19ZUi2tn7n+1q/kY9uZtOIS4+jebdI6wn4LMJMMpDworVM/GoBqOpzKAQ==", + "license": "ISC", + "dependencies": { + "@types/debug": "^4.1.12", + "@types/npm-events-package": "npm:@types/events@^3.0.3", + "awaitqueue": "^3.0.2", + "debug": "^4.3.7", + "fake-mediastreamtrack": "^1.2.0", + "h264-profile-level-id": "^2.0.0", + "npm-events-package": "npm:events@^3.3.0", + "queue-microtask": "^1.2.3", + "sdp-transform": "^2.15.0", + "supports-color": "^9.4.0", + "ua-parser-js": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/mediasoup-client/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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3002,7 +3205,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -3067,6 +3269,16 @@ "node": ">=0.10.0" } }, + "node_modules/npm-events-package": { + "name": "events", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3467,6 +3679,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.2.tgz", + "integrity": "sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.2.tgz", + "integrity": "sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==", + "license": "MIT", + "dependencies": { + "react-router": "7.0.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3595,6 +3847,15 @@ "loose-envify": "^1.1.0" } }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3605,6 +3866,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3638,6 +3905,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3911,6 +4206,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3965,6 +4266,57 @@ } } }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/ua-parser-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.0.tgz", + "integrity": "sha512-SASgD4RlB7+SCMmlVNqrhPw0f/2pGawWBzJ2+LwGTD0GgNnrKGzPJDiraGHJDwW9Zm5DH2lTmUpqDpbZjJY4+Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -4019,6 +4371,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", @@ -4192,6 +4557,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index a8e0339..960e0f7 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,15 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.1.0", + "@types/socket.io-client": "^1.4.36", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.468.0", + "mediasoup-client": "^3.7.18", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^7.0.2", + "socket.io-client": "^4.8.1", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7" }, diff --git a/src/components/VideoCall/VideoCall.tsx b/src/components/VideoCall/VideoCall.tsx new file mode 100644 index 0000000..02ea3f8 --- /dev/null +++ b/src/components/VideoCall/VideoCall.tsx @@ -0,0 +1,274 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React, { useState, useEffect, useRef } from "react"; +import { types as mediasoupTypes, Device } from "mediasoup-client"; +import { setupVideoSocket } from "@/config/videoSocket"; +import { RtpCapabilities, TransportParams } from "@/types/video"; + +const VideoCall: React.FC = () => { + const [localStream, setLocalStream] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + + const videoSocket = setupVideoSocket(); + const deviceRef = useRef(null); + const producerTransportRef = useRef(null); + const consumerTransportRef = useRef(null); + const producerRef = useRef(null); + const consumerRef = useRef(null); + + useEffect(() => { + setupConnection(); + + // Listen for new producers + videoSocket.socket.on("newProducer", () => { + console.log("New producer available"); + consumeMedia(); + }); + + return () => { + // Cleanup + localStream?.getTracks().forEach((track) => track.stop()); + videoSocket.socket.off("newProducer"); + + // Close transports and producers/consumers + producerTransportRef.current?.close(); + consumerTransportRef.current?.close(); + producerRef.current?.close(); + consumerRef.current?.close(); + }; + }, []); + + const setupConnection = async () => { + try { + await getLocalStream(); + await setupMediasoupDevice(); + await createProducerTransport(); + await produceMedia(); + await createConsumerTransport(); + setIsConnected(true); + } catch (error) { + console.error("Connection setup error:", error); + setIsConnected(false); + } + }; + + const getLocalStream = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + setLocalStream(stream); + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; + } + } catch (error) { + console.error("Error getting local stream:", error); + } + }; + + const setupMediasoupDevice = async () => { + deviceRef.current = new Device(); + return new Promise((resolve, reject) => { + videoSocket.getRouterCapabilities( + (routerRtpCapabilities: RtpCapabilities) => { + try { + if (deviceRef.current) { + deviceRef.current.load({ routerRtpCapabilities }); + resolve(); + } else { + reject(new Error("Device not initialized")); + } + } catch (error) { + console.error("Error loading device:", error); + reject(error); + } + } + ); + }); + }; + + const createProducerTransport = async () => { + return new Promise((resolve, reject) => { + videoSocket.createProducerTransport((params: TransportParams) => { + try { + if (!deviceRef.current) throw new Error("Device not initialized"); + + producerTransportRef.current = + deviceRef.current.createSendTransport(params); + + producerTransportRef.current.on( + "connect", + async ({ dtlsParameters }, callback) => { + try { + await videoSocket.connectProducerTransport( + dtlsParameters, + callback + ); + } catch (error) { + console.error("Producer transport connect error:", error); + } + } + ); + + producerTransportRef.current.on( + "produce", + async ({ kind, rtpParameters }, callback) => { + try { + const result = await videoSocket.produce( + kind, + rtpParameters, + callback + ); + // Notify other clients about new producer + videoSocket.socket.emit("newProducer"); + return result; + } catch (error) { + console.error("Produce error:", error); + } + } + ); + + resolve(); + } catch (error) { + console.error("Create producer transport error:", error); + reject(error); + } + }); + }); + }; + + const produceMedia = async () => { + try { + if (!producerTransportRef.current || !localStream) return; + + const audioTrack = localStream.getAudioTracks()[0]; + const videoTrack = localStream.getVideoTracks()[0]; + + if (audioTrack) { + producerRef.current = await producerTransportRef.current.produce({ + track: audioTrack, + // Removed audioGain as it's not a valid option + }); + } + if (videoTrack) { + producerRef.current = await producerTransportRef.current.produce({ + track: videoTrack, + encodings: [ + { maxBitrate: 1000000 }, + { maxBitrate: 500000 }, + { maxBitrate: 100000 }, + ], + }); + } + } catch (error) { + console.error("Produce media error:", error); + } + }; + + const createConsumerTransport = async () => { + return new Promise((resolve, reject) => { + videoSocket.createConsumerTransport((params: TransportParams) => { + try { + if (!deviceRef.current) throw new Error("Device not initialized"); + + consumerTransportRef.current = + deviceRef.current.createRecvTransport(params); + + consumerTransportRef.current.on( + "connect", + async ({ dtlsParameters }, callback) => { + try { + await videoSocket.connectConsumerTransport( + dtlsParameters, + callback + ); + } catch (error) { + console.error("Consumer transport connect error:", error); + } + } + ); + + resolve(); + } catch (error) { + console.error("Create consumer transport error:", error); + reject(error); + } + }); + }); + }; + + const consumeMedia = async () => { + try { + if (!deviceRef.current || !consumerTransportRef.current) { + throw new Error("Transport or device not initialized"); + } + + const consumerParams = await new Promise((resolve, reject) => { + if (!deviceRef.current) { + throw new Error("Device not initialized"); + } + videoSocket.consume(deviceRef.current.rtpCapabilities, (params) => { + if (params.error) { + reject(new Error(params.error)); + } else { + resolve(params); + } + }); + }); + + // Consume the media track + consumerRef.current = await consumerTransportRef.current.consume({ + id: consumerParams.id, + producerId: consumerParams.producerId, + kind: consumerParams.kind, + rtpParameters: consumerParams.rtpParameters, + appData: consumerParams.appData, + }); + + // Update remote video source + if (remoteVideoRef.current) { + const newRemoteStream = new MediaStream([consumerRef.current.track]); + remoteVideoRef.current.srcObject = newRemoteStream; + } + + // Resume consumer if paused + if (consumerRef.current.paused) { + await consumerRef.current.resume(); + } + } catch (error) { + console.error("Consume media error:", error); + } + }; + + return ( +
+
+

Local Video

+
+
+

Remote Video

+ {isConnected ? ( +
+
+ ); +}; + +export default VideoCall; diff --git a/src/config/videoSocket.ts b/src/config/videoSocket.ts new file mode 100644 index 0000000..90f623a --- /dev/null +++ b/src/config/videoSocket.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// src/config/videoSocket.ts +import io, {Socket } from 'socket.io-client'; +import { types as mediasoupTypes, } from 'mediasoup-client'; +import { RtpCapabilities, TransportParams } from '@/types/video'; + +const SOCKET_URL = 'http://localhost:8080'; + +export const setupVideoSocket = () => { + const socket: typeof Socket = io(SOCKET_URL, { path: '/video' }); + + return { + socket, + getRouterCapabilities: (callback: (caps: RtpCapabilities) => void) => { + socket.emit('getRouterRtpCapabilities', null, callback); + }, + createProducerTransport: (callback: (params: TransportParams) => void) => { + socket.emit('createProducerTransport', null, callback); + }, + createConsumerTransport: (callback: (params: TransportParams) => void) => { + socket.emit('createConsumerTransport', null, callback); + }, + connectProducerTransport: (dtlsParameters: mediasoupTypes.DtlsParameters, callback: () => void) => { + socket.emit('connectProducerTransport', { dtlsParameters }, callback); + }, + connectConsumerTransport: (dtlsParameters: mediasoupTypes.DtlsParameters, callback: () => void) => { + socket.emit('connectConsumerTransport', { dtlsParameters }, callback); + }, + // produce: (kind: mediasoupTypes.MediaKind, rtpParameters: mediasoupTypes.RtpParameters, callback: (id: { id: string }) => void) => { + // socket.emit('produce', { kind, rtpParameters }, callback); + // }, + // consume: (rtpCapabilities: mediasoupTypes.RtpCapabilities, callback: (consumerParams: any) => void) => { + // socket.emit('consume', { rtpCapabilities }, callback); + // }, + resume: (callback: () => void) => { + socket.emit('resume', null, callback); + }, + produce: ( + kind: mediasoupTypes.MediaKind, + rtpParameters: mediasoupTypes.RtpParameters, + callback: (id: { id: string }) => void + ) => { + return new Promise((resolve, reject) => { + socket.emit('produce', { kind, rtpParameters }, (response:any) => { + if (response.error) { + reject(new Error(response.error)); + } else { + resolve(response); + callback(response); + } + }); + }); + }, + consume: ( + rtpCapabilities: mediasoupTypes.RtpCapabilities, + callback: (consumerParams: any) => void + ) => { + return new Promise((resolve, reject) => { + socket.emit('consume', { rtpCapabilities }, (response:any) => { + if (response.error) { + reject(new Error(response.error)); + } else { + resolve(response); + callback(response); + } + }); + }); + }, + }; +}; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index bef5202..24f947b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,14 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import VideoCallPage from "./pages/VideoCallPage.tsx"; -createRoot(document.getElementById('root')!).render( - - - , -) +createRoot(document.getElementById("root")!).render( + + + } /> + } /> + + +); diff --git a/src/pages/VideoCallPage.tsx b/src/pages/VideoCallPage.tsx new file mode 100644 index 0000000..2007d4f --- /dev/null +++ b/src/pages/VideoCallPage.tsx @@ -0,0 +1,386 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useEffect } from "react"; +import io from "socket.io-client"; +import * as mediasoupClient from "mediasoup-client"; + +const VideoCallPage: React.FC = () => { + const localVideoRef = useRef(null); + const [rtpCapabilities, setRtpCapabilities] = + useState(null); + + const deviceRef = useRef(null); + const producerTransportRef = useRef( + null + ); + const consumerTransportRef = useRef( + null + ); + const producerRef = useRef(null); + const consumerRef = useRef(null); + const [isProducer, setIsProducer] = useState(false); + const [params, setParams] = useState({ + encodings: [ + { rid: "r0", maxBitrate: 100000, scalabilityMode: "S1T3" }, + { rid: "r1", maxBitrate: 300000, scalabilityMode: "S1T3" }, + { rid: "r2", maxBitrate: 900000, scalabilityMode: "S1T3" }, + ], + codecOptions: { + videoGoogleStartBitrate: 1000, + }, + }); + const remoteVideoRef = useRef(null); + + const socket = io("https://localhost:8080"); + console.log("reached here"); + + useEffect(() => { + socket.on("connect", () => { + console.log("Client connected, socket ID: " + socket.id); + }); + + socket.on("connection-success", ({ socketId, existsProducer }: any) => { + console.log("Received socket ID: " + socketId); + console.log("Does a producer exist " + existsProducer); + }); + + socket.on("connect_error", (err: any) => { + console.error("Connection error details:", err); + console.error("Error stack:", err.stack); + }); + + socket.on("error", (err: any) => { + console.error("Socket error:", err); + }); + + socket.on("hello", (msg: any) => { + console.log(msg); + }); + socket.on("new-user", (msg: any) => { + console.log(msg); + }); + + return () => { + socket.disconnect(); + }; + }, []); + + console.log("reached here1"); + + const streamSuccess = (stream: MediaStream) => { + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; + } + const track = stream.getVideoTracks()[0]; + + setParams((prev) => ({ + ...prev, + track, + })); + + goConnect(true); + }; + + const getLocalStream = () => { + navigator.mediaDevices + .getUserMedia({ + audio: false, + video: { + width: { min: 640, max: 1920 }, + height: { min: 400, max: 1080 }, + }, + }) + .then(streamSuccess) + .catch((error) => console.log(error.message)); + }; + + const goConsume = () => { + goConnect(false); + }; + + const goConnect = (producerOrConsumer: any) => { + setIsProducer(producerOrConsumer); + console.log("device value is ", deviceRef.current); + + if (deviceRef.current === undefined) { + getRtpCapabilities(); + } else { + goCreateTransport(); + } + }; + + const goCreateTransport = () => { + if (isProducer) { + createSendTransport(); + } else { + createRecvTransport(); + } + }; + const createDevice = async () => { + try { + const device = new mediasoupClient.Device(); + if (!rtpCapabilities) { + throw new Error("RTP Capabilities are null or undefined."); + } + await device.load({ routerRtpCapabilities: rtpCapabilities }); + deviceRef.current = device; + console.log("Device initialized successfully"); + goCreateTransport(); + } catch (error) { + console.error(error); + if ((error as any).name === "UnsupportedError") { + console.warn("Browser not supported"); + } + } + }; + + const getRtpCapabilities = async () => { + await socket.emit("createRoom", (data: any) => { + console.log(`Router RTP Capabilities... ${data.rtpCapabilities}`); + setRtpCapabilities(data.rtpCapabilities); + createDevice(); + }); + }; + + const createSendTransport = async () => { + await socket.emit( + "createWebRtcTransport", + { sender: true }, + ({ params }: any) => { + if (params.error) { + console.log(params.error); + return; + } + + console.log(params); + + if (deviceRef.current) { + producerTransportRef.current = + deviceRef.current.createSendTransport(params); + } else { + console.warn("Device is not initialized."); + return; + } + + console.log("reached here 1"); + producerTransportRef.current?.on( + "connect", + async ({ dtlsParameters }: any, callback: any, errback: any) => { + try { + await socket.emit("transport-connect", { + //transportId: producerTransportRef.current?.id, + dtlsParameters: dtlsParameters, + }); + + callback(); + } catch (error) { + errback(error); + } + } + ); + console.log("reached here 2"); + + producerTransportRef.current?.on( + "produce", + async (parameters: any, callback: any, errback: any) => { + console.log("Produce parameters received:", parameters); + try { + socket.emit( + "transport-produce", + { + kind: parameters.kind, + rtpParameters: parameters.rtpParameters, + appData: parameters.appData, + }, + (response: any) => { + if (response.error) { + throw new Error(response.error); + } + callback({ id: response.id }); + } + ); + } catch (error) { + console.error("Error in transport-produce:", error); + errback(error); + } + } + ); + console.log("reached here 3"); + connectSendTransport(); + } + ); + }; + + const connectSendTransport = async () => { + try { + if (producerTransportRef.current) { + try { + producerRef.current = await producerTransportRef.current?.produce( + params + ); + } catch (error: any) { + console.log("error here:", error); + } + } else { + console.warn("Device is not initialized.(send transport error)"); + return; + } + + producerRef.current?.on("trackended", () => { + console.log("track ended"); + }); + producerRef.current?.on("transportclose", () => { + console.log("transport ended"); + }); + } catch (error: any) { + console.dir("error from connectSendTransport: ", error); + } + }; + + const createRecvTransport = async () => { + await socket.emit( + "createWebRtcTransport", + { sender: false }, + ({ params }: any) => { + if (params.error) { + console.log(params.error); + return; + } + console.log("Received transport params:", params); + + if (deviceRef.current) { + consumerTransportRef.current = + deviceRef.current?.createRecvTransport(params); + } else { + console.warn( + "Device is not initialized.(createWebRtcTransport error)" + ); + return; + } + console.log("i did reach here"); + //some error here=>this part is not being triggered,nor does it give any error + try { + consumerTransportRef.current?.on( + "connect", + async ({ dtlsParameters }: any, callback: any, errback: any) => { + console.log( + "Connect event triggered with dtlsParameters:", + dtlsParameters + ); + try { + // + await socket.emit("transport-recv-connect", { + //transportId: consumerTransportRef.current?.id, + dtlsParameters: dtlsParameters, + }); + + callback(); + } catch (error) { + errback(error); + } + } + ); + } catch (error: any) { + console.log("error in emitting transport-recv-connect :", error); + } + + console.log("but did i reach here?"); + connectRecvTransport(); + } + ); + }; + + const connectRecvTransport = async () => { + await socket.emit( + "consume", + { + rtpCapabilities: deviceRef.current?.rtpCapabilities, + }, + async ({ params }: any) => { + if (params.error) { + console.error("Cannot Consume"); + return; + } + console.log(params); + + if (consumerTransportRef.current) { + consumerRef.current = await consumerTransportRef.current?.consume({ + id: params.id, + producerId: params.producerId, + kind: params.kind, + rtpParameters: params.rtpParameters, + }); + + const { track } = consumerRef.current; + if (remoteVideoRef.current) { + remoteVideoRef.current.srcObject = new MediaStream([track]); + } + socket.emit("consumer-resume"); + } else { + console.warn("Device is not initialized.(consume error)"); + return; + } + } + ); + }; + + return ( +
+ +
+
+ {" "} +
+
+ {" "} +
+
+ + + + + + + + + + + + + +
+ ); +}; + +export default VideoCallPage; diff --git a/src/pages/VirtualSpace.tsx b/src/pages/VirtualSpace.tsx index c9ab5c8..c74a927 100644 --- a/src/pages/VirtualSpace.tsx +++ b/src/pages/VirtualSpace.tsx @@ -5,6 +5,8 @@ import OtherUser from "@/components/UserAvatar/OtherUser"; import { handleKeyPress } from "@/lib/helperFns/handleKeyPress"; import UserDetails from "@/components/UserDetails/UserDetails"; import GroupChat from "@/components/Chat/GroupChat/GroupChat"; +import { Button } from "@/components/ui/button"; +import VideoCall from "@/components/VideoCall/VideoCall"; const VirtualSpace = () => { const [position, setPosition] = useState({ x: 0, y: 0 }); @@ -16,9 +18,10 @@ const VirtualSpace = () => { const [latestMessage, setLatestMessage] = useState(""); const [userMsg, setUserMsg] = useState(""); const [proximityMessage, setProximityMessage] = useState(null); + const [nearUser, setNearUser] = useState(null); useEffect(() => { - const socket = new WebSocket(import.meta.env.VITE_WS_SERVER_URL); + const socket = new WebSocket(import.meta.env.VITE_WS_SERVER_URL + "/chat"); const handleOpen = () => { console.log("Connected to Server"); @@ -74,6 +77,7 @@ const VirtualSpace = () => { setProximityMessage( `User/s ${nearbyUsers.join(", ")} is near you` ); + setNearUser(nearbyUsers); } } else { setProximityMessage(null); @@ -193,7 +197,7 @@ const VirtualSpace = () => { />
-
Latest message: {latestMessage}
+
Latest message by user: {latestMessage}
{
{proximityMessage && ( -
- {proximityMessage} +
+
+ {proximityMessage} +
+ + +
)}
diff --git a/src/types/video.ts b/src/types/video.ts new file mode 100644 index 0000000..331018f --- /dev/null +++ b/src/types/video.ts @@ -0,0 +1,14 @@ +// src/types/video.ts +import { types as mediasoupTypes } from 'mediasoup-client'; + +export interface RtpCapabilities { + codecs: mediasoupTypes.RtpCodecCapability[]; + headerExtensions: mediasoupTypes.RtpHeaderExtension[]; +} + +export interface TransportParams { + id: string; + iceParameters: mediasoupTypes.IceParameters; + iceCandidates: mediasoupTypes.IceCandidate[]; + dtlsParameters: mediasoupTypes.DtlsParameters; +} \ No newline at end of file