From 70ab00795ad9b0a00dd9e63bcc0cec5ec5292ed0 Mon Sep 17 00:00:00 2001 From: lan Date: Mon, 1 Jun 2026 13:45:45 +0800 Subject: [PATCH] feat(call): migrate from WebRTC to LiveKit Replace the custom WebRTC implementation with LiveKit for improved stability and feature support. - Remove `react-native-webrtc` and custom `WebRTCManager` - Implement `LiveKitService` for room and track management - Update `callStore` to handle LiveKit events and connection states - Refactor `CallScreen` (mobile and web) to use `@livekit/react-native` and `VideoView` - Update WebSocket protocol to use `call_ready` instead of manual SDP/ICE exchanges - Add necessary camera and microphone permissions to `app.json` - Update Metro and Babel configurations for LiveKit compatibility --- app.config.js | 3 +- app.json | 3 + metro.config.js | 4 +- package-lock.json | 1726 +++++++++++++++++++++- package.json | 4 +- plugins/withHuaweiPush.js | 57 +- src/components/call/CallScreen.tsx | 171 ++- src/components/call/CallScreen.web.tsx | 315 +++- src/services/core/index.ts | 3 - src/services/core/wsService.ts | 79 +- src/services/livekit/LiveKitService.ts | 220 +++ src/services/livekit/index.ts | 2 + src/services/webrtc/WebRTCManager.ts | 672 --------- src/services/webrtc/WebRTCManager.web.ts | 89 -- src/services/webrtc/index.ts | 2 - src/stores/call/callStore.ts | 476 +++--- web-shims/react-native-webrtc/index.js | 94 -- 17 files changed, 2500 insertions(+), 1420 deletions(-) create mode 100644 src/services/livekit/LiveKitService.ts create mode 100644 src/services/livekit/index.ts delete mode 100644 src/services/webrtc/WebRTCManager.ts delete mode 100644 src/services/webrtc/WebRTCManager.web.ts delete mode 100644 src/services/webrtc/index.ts delete mode 100644 web-shims/react-native-webrtc/index.js diff --git a/app.config.js b/app.config.js index e985640..7a14edb 100644 --- a/app.config.js +++ b/app.config.js @@ -95,7 +95,8 @@ module.exports = { // 配置 Web 端别名,避免加载原生模块 babel: { dangerouslyAddModulePathsToTranspile: [ - 'react-native-webrtc', + 'livekit-client', + '@livekit', ], }, }, diff --git a/app.json b/app.json index 065be4f..505b709 100644 --- a/app.json +++ b/app.json @@ -16,6 +16,7 @@ "supportsTablet": true, "infoPlist": { "NSMicrophoneUsageDescription": "允许威友访问您的麦克风以进行语音通话", + "NSCameraUsageDescription": "允许威友访问您的相机以进行视频通话", "UIBackgroundModes": [ "fetch", "remote-notification", @@ -41,6 +42,7 @@ "permissions": [ "VIBRATE", "RECORD_AUDIO", + "CAMERA", "RECEIVE_BOOT_COMPLETED", "WAKE_LOCK", "READ_EXTERNAL_STORAGE", @@ -138,6 +140,7 @@ } ], "expo-font", + "@livekit/react-native", [ "expo-notifications", { diff --git a/metro.config.js b/metro.config.js index 469bc57..f7b0c3f 100644 --- a/metro.config.js +++ b/metro.config.js @@ -7,8 +7,7 @@ const config = getDefaultConfig(__dirname); // Add wasm asset support config.resolver.assetExts.push('wasm'); -// Fix for react-native-webrtc event-target-shim import warning -// The package doesn't properly export "./index" in its exports field +// Fix for livekit-client event-target-shim import warning config.resolver.unstable_enablePackageExports = false; // Support TypeScript path aliases (@/ -> ./src/) @@ -36,7 +35,6 @@ const webShimPaths = [ // These packages import requireNativeComponent directly from 'react-native' // (which resolves to react-native-web on web) and don't have web implementations. const webPackageShims = { - 'react-native-webrtc': path.resolve(__dirname, 'web-shims/react-native-webrtc/index.js'), }; const originalResolveRequest = config.resolver.resolveRequest; diff --git a/package-lock.json b/package-lock.json index 61a3463..7acc072 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.2", "dependencies": { "@expo/vector-icons": "^15.1.1", + "@livekit/react-native": "^2.11.0", + "@livekit/react-native-webrtc": "^144.1.0", "@react-native-async-storage/async-storage": "^2.2.0", "@react-navigation/bottom-tabs": "^7.15.2", "@react-navigation/material-top-tabs": "^7.4.16", @@ -42,6 +44,7 @@ "jcore-react-native": "^2.3.5", "jpush-react-native": "^3.2.6", "katex": "^0.16.42", + "livekit-client": "^2.19.0", "markdown-it": "^14.1.1", "pako": "^2.1.0", "prismjs": "^1.30.0", @@ -57,7 +60,6 @@ "react-native-share": "^12.2.6", "react-native-sse": "^1.2.1", "react-native-web": "^0.21.0", - "react-native-webrtc": "^124.0.7", "react-native-webview": "13.16.0", "react-native-worklets": "0.7.2", "zod": "^4.3.6", @@ -1462,6 +1464,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@callstack/react-theme-provider": { "version": "3.0.9", "resolved": "https://registry.npmmirror.com/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", @@ -2135,6 +2143,31 @@ "excpretty": "build/cli.js" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -2423,6 +2456,148 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@livekit/components-core": { + "version": "0.12.13", + "resolved": "https://registry.npmmirror.com/@livekit/components-core/-/components-core-0.12.13.tgz", + "integrity": "sha512-DQmi84afHoHjZ62wm8y+XPNIDHTwFHAltjd3lmyXj8UZHOY7wcza4vFt1xnghJOD5wLRY58L1dkAgAw59MgWvw==", + "license": "Apache-2.0", + "dependencies": { + "@floating-ui/dom": "1.7.4", + "loglevel": "1.9.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "livekit-client": "^2.17.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@livekit/components-core/node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.45.8", + "resolved": "https://registry.npmmirror.com/@livekit/protocol/-/protocol-1.45.8.tgz", + "integrity": "sha512-Q+l57E7w/xxOBFVWzdX5rkAZO7ffyF+rlDzNUYq2SU114+5aTyCq+PK4unaEVDNd4952Af7wteKr3sOgasGuaA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@livekit/react-native": { + "version": "2.11.0", + "resolved": "https://registry.npmmirror.com/@livekit/react-native/-/react-native-2.11.0.tgz", + "integrity": "sha512-RTGi+t7PpfnhXbf6p5x3K9ukApF3fqB+p5TQP32/wX//6wdgw0wdwyWjjWM2XIs4kLRTNqXC2yLqNaLBtCS6xQ==", + "license": "Apache-2.0", + "workspaces": [ + ".", + "example" + ], + "dependencies": { + "@livekit/components-react": "^2.9.17", + "@livekit/mutex": "^1.1.1", + "array.prototype.at": "^1.1.1", + "base64-js": "1.5.1", + "events": "^3.3.0", + "loglevel": "^1.8.0", + "promise.allsettled": "^1.0.5", + "react-native-url-polyfill": "^3.0.0", + "typed-emitter": "^2.1.0", + "web-streams-polyfill": "^4.3.0", + "well-known-symbols": "^4.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@livekit/react-native-webrtc": "^144.1.0", + "livekit-client": "^2.19.0", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@livekit/react-native-webrtc": { + "version": "144.1.0", + "resolved": "https://registry.npmmirror.com/@livekit/react-native-webrtc/-/react-native-webrtc-144.1.0.tgz", + "integrity": "sha512-KP5Q2+eIwHe8eZfKv7wwb2cayZ4Uoqi7qkv4tbgKphWRQUZewMNNnV3jBI0vZy9DL0v3Nfpy+fhfmvGxwmHAkA==", + "license": "MIT", + "dependencies": { + "base64-js": "1.5.1", + "debug": "4.3.4" + }, + "peerDependencies": { + "react-native": ">=0.60.0" + } + }, + "node_modules/@livekit/react-native-webrtc/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@livekit/react-native-webrtc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/@livekit/react-native/node_modules/@livekit/components-react": { + "version": "2.9.20", + "resolved": "https://registry.npmmirror.com/@livekit/components-react/-/components-react-2.9.20.tgz", + "integrity": "sha512-hjkYOsJj9Jbghb7wM5cI8HoVisKeL6Zcy1VnRWTLm0sqVbto8GJp/17T4Udx85mCPY6Jgh8I1Cv0yVzgz7CQtg==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/components-core": "0.12.13", + "clsx": "2.1.1", + "events": "^3.3.0", + "jose": "^6.0.12", + "usehooks-ts": "3.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0", + "livekit-client": "^2.17.2", + "react": ">=18", + "react-dom": ">=18", + "tslib": "^2.6.2" + }, + "peerDependenciesMeta": { + "@livekit/krisp-noise-filter": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3793,6 +3968,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/dom-mediacapture-record": { + "version": "1.0.22", + "resolved": "https://registry.npmmirror.com/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", + "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/emscripten": { "version": "1.41.5", "resolved": "https://registry.npmmirror.com/@types/emscripten/-/emscripten-1.41.5.tgz", @@ -4108,6 +4290,83 @@ "node": ">=10" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.at": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/array.prototype.at/-/array.prototype.at-1.1.3.tgz", + "integrity": "sha512-TX4J1Uig4skvpOakrvP8q/uyhUj+ZDNmQoZpHf3MsKTrXcMHDPrgJlpv2nRJMfANrsmhg1JoOGrg3yOp209SWg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.map": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/array.prototype.map/-/array.prototype.map-1.0.8.tgz", + "integrity": "sha512-YocPM7bYYu2hXGxWpb5vwZ8cMeudNHYtYBcUDY4Z1GWa53qcnQMWSl25jeBHNzitjl9HW2AWW4ro/S/nftUaOQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-array-method-boxes-properly": "^1.0.0", + "es-object-atoms": "^1.0.0", + "is-string": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", @@ -4124,6 +4383,15 @@ "node": ">=4" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/async-limiter/-/async-limiter-1.0.1.tgz", @@ -4137,6 +4405,21 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/await-lock": { "version": "2.2.2", "resolved": "https://registry.npmmirror.com/await-lock/-/await-lock-2.2.2.tgz", @@ -4602,7 +4885,6 @@ "version": "5.7.1", "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -4638,6 +4920,24 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4655,7 +4955,6 @@ "version": "1.0.4", "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4854,6 +5153,15 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", @@ -5107,6 +5415,57 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz", @@ -5181,6 +5540,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -5190,6 +5566,23 @@ "node": ">=8" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5352,6 +5745,80 @@ "url": "https://opencollective.com/express" } }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5370,6 +5837,26 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5397,6 +5884,35 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", @@ -5455,6 +5971,15 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", @@ -6693,6 +7218,21 @@ "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", "license": "BSD-2-Clause" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", @@ -6762,6 +7302,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6848,6 +7426,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/getenv": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/getenv/-/getenv-2.0.0.tgz", @@ -6887,6 +7482,22 @@ "node": ">= 6" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", @@ -6905,6 +7516,18 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -6914,6 +7537,33 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", @@ -7086,7 +7736,6 @@ "version": "1.2.1", "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, "funding": [ { "type": "github", @@ -7189,6 +7838,20 @@ "css-in-js-utils": "^3.1.0" } }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz", @@ -7198,6 +7861,39 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7205,6 +7901,68 @@ "devOptional": true, "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7220,6 +7978,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz", @@ -7245,6 +8036,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -7255,6 +8061,25 @@ "node": ">=4" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", @@ -7278,6 +8103,30 @@ "node": ">=8" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", @@ -7287,6 +8136,22 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -7296,6 +8161,51 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", @@ -7309,6 +8219,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -7322,6 +8280,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-1.1.0.tgz", @@ -7332,6 +8333,12 @@ "node": ">=4" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", @@ -7363,6 +8370,28 @@ "node": ">=8" } }, + "node_modules/iterate-iterator": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz", + "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "license": "MIT", + "dependencies": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jcore-react-native": { "version": "2.3.5", "resolved": "https://registry.npmmirror.com/jcore-react-native/-/jcore-react-native-2.3.5.tgz", @@ -7577,6 +8606,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jpush-react-native": { "version": "3.2.6", "resolved": "https://registry.npmmirror.com/jpush-react-native/-/jpush-react-native-3.2.6.tgz", @@ -8005,6 +9043,26 @@ "uc.micro": "^2.0.0" } }, + "node_modules/livekit-client": { + "version": "2.19.1", + "resolved": "https://registry.npmmirror.com/livekit-client/-/livekit-client-2.19.1.tgz", + "integrity": "sha512-knwEuozt3d7optuUMvw+0PGrV/68GWXrTdAefezOM51Dk8QLb28qOm73UMSSQET58gUqelxqU1afchd2YyPKnQ==", + "license": "Apache-2.0", + "dependencies": { + "@livekit/mutex": "1.1.1", + "@livekit/protocol": "1.45.8", + "events": "^3.3.0", + "jose": "^6.1.0", + "loglevel": "^1.9.2", + "sdp-transform": "^2.15.0", + "tslib": "2.8.1", + "typed-emitter": "^2.1.0", + "webrtc-adapter": "9.0.5" + }, + "peerDependencies": { + "@types/dom-mediacapture-record": "^1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -8215,6 +9273,19 @@ "node": ">=6" } }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8945,7 +10016,6 @@ "version": "1.13.4", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8954,6 +10024,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", @@ -9050,6 +10149,23 @@ "node": ">=8" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", @@ -9274,6 +10390,15 @@ "node": ">=4.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.49.tgz", @@ -9376,6 +10501,26 @@ "asap": "~2.0.6" } }, + "node_modules/promise.allsettled": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", + "license": "MIT", + "dependencies": { + "array.prototype.map": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "iterate-value": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", @@ -9395,6 +10540,15 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", @@ -9797,6 +10951,18 @@ "react-native-pager-view": ">= 6.0.0" } }, + "node_modules/react-native-url-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz", + "integrity": "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==", + "license": "MIT", + "dependencies": { + "whatwg-url-without-unicode": "8.0.0-3" + }, + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.21.2", "resolved": "https://registry.npmmirror.com/react-native-web/-/react-native-web-0.21.2.tgz", @@ -9829,55 +10995,6 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, - "node_modules/react-native-webrtc": { - "version": "124.0.7", - "resolved": "https://registry.npmmirror.com/react-native-webrtc/-/react-native-webrtc-124.0.7.tgz", - "integrity": "sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==", - "license": "MIT", - "dependencies": { - "base64-js": "1.5.1", - "debug": "4.3.4", - "event-target-shim": "6.0.2" - }, - "peerDependencies": { - "react-native": ">=0.60.0" - } - }, - "node_modules/react-native-webrtc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/react-native-webrtc/node_modules/event-target-shim": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/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/react-native-webrtc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, "node_modules/react-native-webview": { "version": "13.16.0", "resolved": "https://registry.npmmirror.com/react-native-webview/-/react-native-webview-13.16.0.tgz", @@ -10245,6 +11362,28 @@ "node": ">= 6" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", @@ -10269,6 +11408,26 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpu-core": { "version": "6.4.0", "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.4.0.tgz", @@ -10469,6 +11628,34 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10489,6 +11676,39 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10511,6 +11731,21 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/sdp": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/sdp/-/sdp-3.2.2.tgz", + "integrity": "sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==", + "license": "MIT" + }, + "node_modules/sdp-transform": { + "version": "2.15.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/semver/-/semver-6.3.1.tgz", @@ -10635,6 +11870,52 @@ "devOptional": true, "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", @@ -10699,7 +11980,6 @@ "version": "1.1.0", "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10719,7 +11999,6 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10736,7 +12015,6 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10755,7 +12033,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10972,6 +12249,19 @@ "node": ">= 0.6" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/stream-buffers/-/stream-buffers-2.2.0.tgz", @@ -11042,6 +12332,62 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -11351,6 +12697,89 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", @@ -11397,6 +12826,24 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", @@ -11559,6 +13006,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/usehooks-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11645,12 +13107,52 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.3.0.tgz", + "integrity": "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw==", + "license": "MIT", + "workspaces": [ + "test/benchmark-test", + "test/rollup-test", + "test/webpack-test" + ], + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/webrtc-adapter": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz", + "integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.2.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, + "node_modules/well-known-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/well-known-symbols/-/well-known-symbols-4.1.0.tgz", + "integrity": "sha512-lKhCpGfPkaJnPKyep1Uj44pNmyrdupYHtxci2ThUCC/Y0px44d9BWt2dbewDif4PwOqSI6KkCdwuL22CDUj4rw==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.7", + "has-symbols": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -11673,6 +13175,29 @@ "integrity": "sha512-u2FNVjFVFZhdjb502KzXy1gKn1mEisQRJssmSJT8CPhZdZa0AP6VCbWlXERKyGu0l09t0k50FiDiralpGhBxgA==", "license": "MIT" }, + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmmirror.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "license": "MIT", + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -11688,6 +13213,70 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", @@ -11695,6 +13284,27 @@ "devOptional": true, "license": "ISC" }, + "node_modules/which-typed-array": { + "version": "1.1.21", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.21.tgz", + "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 7b81c05..f372700 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,9 @@ "react-native-share": "^12.2.6", "react-native-sse": "^1.2.1", "react-native-web": "^0.21.0", - "react-native-webrtc": "^124.0.7", + "@livekit/react-native": "^2.11.0", + "@livekit/react-native-webrtc": "^144.1.0", + "livekit-client": "^2.19.0", "react-native-webview": "13.16.0", "react-native-worklets": "0.7.2", "zod": "^4.3.6", diff --git a/plugins/withHuaweiPush.js b/plugins/withHuaweiPush.js index 01dc24c..f97a441 100644 --- a/plugins/withHuaweiPush.js +++ b/plugins/withHuaweiPush.js @@ -14,39 +14,80 @@ const withHuaweiPush = (config, options = {}) => { config = withSettingsGradle(config, (config) => { const contents = config.modResults.contents; + // Add Huawei Maven repo to pluginManagement.repositories for plugin resolution if (!contents.includes('developer.huawei.com/repo')) { if (contents.includes('pluginManagement')) { config.modResults.contents = contents.replace( /(pluginManagement\s*\{[\s\S]*?repositories\s*\{)/, `$1\n maven { url 'https://developer.huawei.com/repo/' }` ); - } - if (!config.modResults.contents.includes('developer.huawei.com/repo')) { + } else { + // No pluginManagement block at all — create one with the Huawei repo config.modResults.contents = - `pluginManagement {\n repositories {\n maven { url 'https://developer.huawei.com/repo/' }\n }\n}\n` + + `pluginManagement {\n repositories {\n maven { url 'https://developer.huawei.com/repo/' }\n }\n}\n\n` + config.modResults.contents; } } - if (!config.modResults.contents.includes("id 'com.huawei.agconnect'")) { - if (config.modResults.contents.includes('pluginManagement')) { + // Map the plugin ID to its Maven artifact via resolutionStrategy. + // AGConnect is not published as a standard Gradle plugin marker, so Gradle 9+ + // cannot resolve it by plugin ID alone. This mapping tells Gradle where to find it. + if (!config.modResults.contents.includes('com.huawei.agconnect:agcp')) { + const pmMatch = config.modResults.contents.match(/pluginManagement\s*\{/); + if (pmMatch) { config.modResults.contents = config.modResults.contents.replace( /(pluginManagement\s*\{)/, - `$1\n plugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false\n }` + `$1\n resolutionStrategy {\n eachPlugin {\n if (requested.id.id == 'com.huawei.agconnect') {\n useModule('com.huawei.agconnect:agcp:1.9.1.301')\n }\n }\n }` ); } } - if (!config.modResults.contents.includes('developer.huawei.com/repo')) { + // Ensure Huawei Maven repo also appears in dependencyResolutionManagement (Gradle 9+) + if (!config.modResults.contents.match(/dependencyResolutionManagement[\s\S]*developer\.huawei\.com\/repo/)) { const depMgmtPattern = /dependencyResolutionManagement\s*\{[\s\S]*?repositories\s*\{/; if (depMgmtPattern.test(config.modResults.contents)) { config.modResults.contents = config.modResults.contents.replace( depMgmtPattern, - `$&\n maven { url 'https://developer.huawei.com/repo/' }` + `$&\n maven { url 'https://developer.huawei.com/repo/' }` ); } } + // Declare the plugin in a top-level plugins {} block (outside pluginManagement). + // In Gradle 9+, plugins declared with `apply false` in settings.gradle are + // available to all subprojects via `apply plugin:` or the plugins DSL. + if (!config.modResults.contents.includes("id 'com.huawei.agconnect'")) { + const pluginsBlockPattern = /^plugins\s*\{/m; + if (pluginsBlockPattern.test(config.modResults.contents)) { + // Add to existing top-level plugins block + config.modResults.contents = config.modResults.contents.replace( + /^plugins\s*\{/m, + `plugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false` + ); + } else { + // Insert a top-level plugins block after pluginManagement closing brace, + // or at the very beginning if no pluginManagement block exists. + const pmClosePattern = /\n\}\n/; + if (pmClosePattern.test(config.modResults.contents)) { + const firstClosing = config.modResults.contents.indexOf('}', config.modResults.contents.indexOf('pluginManagement')) + 1; + if (firstClosing > 0) { + const insertPos = firstClosing; + config.modResults.contents = + config.modResults.contents.slice(0, insertPos) + + `\n\nplugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false\n}` + + config.modResults.contents.slice(insertPos); + } + } else { + // Fallback: prepend after pluginManagement block + config.modResults.contents = + config.modResults.contents.replace( + /(pluginManagement\s*\{[\s\S]*?\})/, + `$1\n\nplugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false\n}` + ); + } + } + } + return config; }); diff --git a/src/components/call/CallScreen.tsx b/src/components/call/CallScreen.tsx index 5b43f32..e54905f 100644 --- a/src/components/call/CallScreen.tsx +++ b/src/components/call/CallScreen.tsx @@ -1,15 +1,19 @@ import React, { useEffect, useState } from 'react'; import { - View, - Text, - StyleSheet, - TouchableOpacity, - Image, - StatusBar, + View, + Text, + StyleSheet, + TouchableOpacity, + Image, + StatusBar, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { Track, LocalVideoTrack, RemoteVideoTrack } from 'livekit-client'; +import { VideoView } from '@livekit/react-native'; + +type VideoTrack = LocalVideoTrack | RemoteVideoTrack; import { callStore } from '../../stores/call'; -import { RTCView } from 'react-native-webrtc'; +import { liveKitService } from '@/services/livekit'; const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); @@ -17,12 +21,9 @@ const formatDuration = (seconds: number): string => { return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; - const CallScreen: React.FC = () => { const currentCall = callStore((s) => s.currentCall); const callDuration = callStore((s) => s.callDuration); - const peerStream = callStore((s) => s.peerStream); - const localStream = callStore((s) => s.localStream); const endCall = callStore((s) => s.endCall); const toggleMute = callStore((s) => s.toggleMute); const toggleSpeaker = callStore((s) => s.toggleSpeaker); @@ -30,33 +31,66 @@ const CallScreen: React.FC = () => { const isMinimized = callStore((s) => s.isMinimized); const toggleMinimize = callStore((s) => s.toggleMinimize); - // Track whether peer has video - const [hasPeerVideo, setHasPeerVideo] = useState(false); - // Track whether local has video - const [hasLocalVideo, setHasLocalVideo] = useState(false); + const [remoteVideoTrack, setRemoteVideoTrack] = useState(null); + const [localVideoTrack, setLocalVideoTrack] = useState(null); - // Check peer stream for video tracks + // Subscribe to LiveKit track events useEffect(() => { - if (peerStream) { - const videoTracks = peerStream.getVideoTracks(); - const hasVideo = videoTracks.length > 0 && videoTracks.some((t) => t.enabled); - setHasPeerVideo(hasVideo); - } else { - setHasPeerVideo(false); - } - }, [peerStream]); - // Check local stream for video tracks - useEffect(() => { - if (localStream) { - const videoTracks = localStream.getVideoTracks(); - const hasVideo = videoTracks.length > 0 && videoTracks.some((t) => t.enabled); - setHasLocalVideo(hasVideo); - } else { - setHasLocalVideo(false); - } - }, [localStream]); + const unsubs: (() => void)[] = []; + + unsubs.push( + liveKitService.on('trackSubscribed', ({ track }) => { + if (track.kind === Track.Kind.Video) { + setRemoteVideoTrack(track as VideoTrack); + } + }) + ); + + unsubs.push( + liveKitService.on('trackUnsubscribed', ({ track }) => { + if (track.kind === Track.Kind.Video) { + setRemoteVideoTrack(null); + } + }) + ); + + unsubs.push( + liveKitService.on('trackMuted', ({ publication }) => { + if (publication.source === Track.Source.Camera) { + setRemoteVideoTrack(null); + } + }) + ); + + unsubs.push( + liveKitService.on('trackUnmuted', ({ publication, participant }) => { + if (publication.source === Track.Source.Camera && publication.track) { + setRemoteVideoTrack(publication.track as VideoTrack); + } + }) + ); + + // Poll local video track from localParticipant + const localPollInterval = setInterval(() => { + const lp = liveKitService.localParticipant; + if (lp) { + const videoPub = lp.getTrackPublication(Track.Source.Camera); + const track = (videoPub?.track ?? null) as VideoTrack | null; + setLocalVideoTrack((prev) => (prev === track ? prev : track)); + } else { + setLocalVideoTrack((prev) => (prev === null ? prev : null)); + } + }, 500); + + return () => { + unsubs.forEach((unsub) => unsub()); + clearInterval(localPollInterval); + }; + }, []); + // Don't render full screen when minimized or no call if (!currentCall || isMinimized) return null; + const getStatusText = (): string => { switch (currentCall.status) { case 'calling': @@ -77,63 +111,52 @@ const CallScreen: React.FC = () => { return ''; } }; + const handleEndCall = () => { endCall('hangup'); }; - const handleToggleMute = () => { - toggleMute(); - }; - const handleToggleSpeaker = () => { - toggleSpeaker(); - }; - const handleToggleVideo = () => { - toggleVideo(); - }; - const handleMinimize = () => { - toggleMinimize(); - }; - // Determine if we should show video UI - const showRemoteVideo = hasPeerVideo; - // Use currentCall.isVideoEnabled directly for local video - // This is more reliable than checking localStream.getVideoTracks() - // because the stream object reference may not trigger useEffect properly - const showLocalVideo = currentCall?.isVideoEnabled && localStream; + + const showRemoteVideo = !!remoteVideoTrack; + const showLocalVideo = currentCall?.isVideoEnabled && !!localVideoTrack; const isVideoCallActive = showRemoteVideo || showLocalVideo; + return ( - {/* Background */} - {/* Remote video - full screen when peer has video */} - {showRemoteVideo && peerStream && ( - )} + {/* Local video - picture in picture */} - {showLocalVideo && localStream && ( + {showLocalVideo && localVideoTrack && ( - )} + {/* Top area: minimize button */} + {/* Center: Peer info - only show when no video */} {!isVideoCallActive && ( @@ -154,6 +177,7 @@ const CallScreen: React.FC = () => { {getStatusText()} )} + {/* Peer name overlay when video is active */} {isVideoCallActive && ( @@ -163,13 +187,14 @@ const CallScreen: React.FC = () => { {getStatusText()} )} + {/* Bottom controls */} {/* Mute */} @@ -185,25 +210,25 @@ const CallScreen: React.FC = () => { {/* Video toggle */} - + - - {hasLocalVideo ? '关闭摄像头' : '打开摄像头'} + + {currentCall.isVideoEnabled ? '关闭摄像头' : '打开摄像头'} {/* Speaker */} @@ -218,7 +243,7 @@ const CallScreen: React.FC = () => { - {/* End call - prominent red button */} + {/* End call */} { ); }; -const CONTROL_CIRCLE_SIZE = 56; -const END_CALL_SIZE = 64; - const styles = StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject, @@ -408,5 +430,4 @@ const styles = StyleSheet.create({ }, }); - export default CallScreen; diff --git a/src/components/call/CallScreen.web.tsx b/src/components/call/CallScreen.web.tsx index c3ecc48..e058a00 100644 --- a/src/components/call/CallScreen.web.tsx +++ b/src/components/call/CallScreen.web.tsx @@ -1,13 +1,19 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, + Image, StatusBar, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { Track, LocalVideoTrack, RemoteVideoTrack } from 'livekit-client'; +import { VideoView } from '@livekit/react-native'; import { callStore } from '../../stores/call'; +import { liveKitService } from '@/services/livekit'; + +type VideoTrack = LocalVideoTrack | RemoteVideoTrack; const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); @@ -19,70 +25,224 @@ const CallScreen: React.FC = () => { const currentCall = callStore((s) => s.currentCall); const callDuration = callStore((s) => s.callDuration); const endCall = callStore((s) => s.endCall); + const toggleMute = callStore((s) => s.toggleMute); + const toggleSpeaker = callStore((s) => s.toggleSpeaker); + const toggleVideo = callStore((s) => s.toggleVideo); const isMinimized = callStore((s) => s.isMinimized); + const toggleMinimize = callStore((s) => s.toggleMinimize); + + const [remoteVideoTrack, setRemoteVideoTrack] = useState(null); + const [localVideoTrack, setLocalVideoTrack] = useState(null); + + // Subscribe to LiveKit track events + useEffect(() => { + const unsubs: (() => void)[] = []; + + unsubs.push( + liveKitService.on('trackSubscribed', ({ track }) => { + if (track.kind === Track.Kind.Video) { + setRemoteVideoTrack(track as VideoTrack); + } + }) + ); + + unsubs.push( + liveKitService.on('trackUnsubscribed', ({ track }) => { + if (track.kind === Track.Kind.Video) { + setRemoteVideoTrack(null); + } + }) + ); + + unsubs.push( + liveKitService.on('trackMuted', ({ publication }) => { + if (publication.source === Track.Source.Camera) { + setRemoteVideoTrack(null); + } + }) + ); + + unsubs.push( + liveKitService.on('trackUnmuted', ({ publication }) => { + if (publication.source === Track.Source.Camera && publication.track) { + setRemoteVideoTrack(publication.track as VideoTrack); + } + }) + ); + + const localPollInterval = setInterval(() => { + const lp = liveKitService.localParticipant; + if (lp) { + const videoPub = lp.getTrackPublication(Track.Source.Camera); + const track = (videoPub?.track ?? null) as VideoTrack | null; + setLocalVideoTrack((prev) => (prev === track ? prev : track)); + } else { + setLocalVideoTrack((prev) => (prev === null ? prev : null)); + } + }, 500); + + return () => { + unsubs.forEach((unsub) => unsub()); + clearInterval(localPollInterval); + }; + }, []); - // Don't render full screen when minimized or no call if (!currentCall || isMinimized) return null; const getStatusText = (): string => { switch (currentCall.status) { + case 'calling': + return '正在等待对方接听...'; case 'ringing': - return '通话功能暂不支持网页端'; + return '来电响铃中...'; case 'connecting': - return '通话功能暂不支持网页端'; + return '连接中...'; case 'connected': - return '通话功能暂不支持网页端'; + return formatDuration(callDuration); + case 'reconnecting': + return '网络重连中...'; case 'ended': return '通话已结束'; + case 'failed': + return '连接失败'; default: return ''; } }; - const handleEndCall = () => { - endCall('hangup'); - }; + const showRemoteVideo = !!remoteVideoTrack; + const showLocalVideo = currentCall?.isVideoEnabled && !!localVideoTrack; + const isVideoCallActive = showRemoteVideo || showLocalVideo; return ( - - {/* Background - single consistent color */} - {/* Center: Peer info */} - - - - - + {showRemoteVideo && remoteVideoTrack && ( + + )} + + {showLocalVideo && localVideoTrack && ( + + - - {currentCall.peerName || '未知用户'} - - {getStatusText()} + )} + + + + + - {/* Bottom controls */} + {!isVideoCallActive && ( + + + {currentCall.peerAvatar ? ( + + ) : ( + + + {currentCall.peerName?.charAt(0).toUpperCase() || '?'} + + + )} + + + {currentCall.peerName || '未知用户'} + + {getStatusText()} + + )} + + {isVideoCallActive && ( + + + {currentCall.peerName || '未知用户'} + + {getStatusText()} + + )} + - {/* End call - prominent red button */} + + + + + + + {currentCall.isMuted ? '取消静音' : '静音'} + + + + + + + + {currentCall.isVideoEnabled ? '关闭摄像头' : '打开摄像头'} + + + + + + + + {currentCall.isSpeakerOn ? '免提' : '听筒'} + + + endCall('hangup')} activeOpacity={0.8} > - 关闭 + 挂断 ); }; -const END_CALL_SIZE = 64; - const styles = StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject, @@ -93,6 +253,25 @@ const styles = StyleSheet.create({ ...StyleSheet.absoluteFillObject, backgroundColor: '#1A1A2E', }, + topBar: { + position: 'absolute', + top: 50, + left: 0, + right: 0, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + height: 44, + zIndex: 100, + }, + topButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + }, centerArea: { position: 'absolute', top: '28%', @@ -132,20 +311,98 @@ const styles = StyleSheet.create({ color: 'rgba(255, 255, 255, 0.5)', letterSpacing: 0.3, }, + fullScreenVideo: { + ...StyleSheet.absoluteFillObject, + backgroundColor: '#1A1A2E', + }, + localVideoContainer: { + position: 'absolute', + top: 100, + right: 20, + width: 120, + height: 160, + borderRadius: 16, + overflow: 'hidden', + backgroundColor: '#2A2A4E', + borderWidth: 2, + borderColor: 'rgba(255, 255, 255, 0.2)', + zIndex: 50, + }, + localVideo: { + width: '100%', + height: '100%', + }, + videoOverlayInfo: { + position: 'absolute', + top: 100, + left: 0, + right: 0, + alignItems: 'center', + zIndex: 50, + }, + videoPeerName: { + fontSize: 18, + fontWeight: '600', + color: '#FFFFFF', + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 2, + }, + videoStatus: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.8)', + marginTop: 4, + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 2, + }, controls: { position: 'absolute', bottom: 60, left: 0, right: 0, alignItems: 'center', + zIndex: 100, + }, + controlRow: { + flexDirection: 'row', + justifyContent: 'center', + gap: 24, + marginBottom: 24, + }, + controlButton: { + alignItems: 'center', + width: 70, + }, + controlCircle: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: 'rgba(255, 255, 255, 0.12)', + justifyContent: 'center', + alignItems: 'center', + }, + controlCircleActive: { + backgroundColor: '#FFFFFF', + }, + controlButtonActive: {}, + controlLabel: { + fontSize: 11, + color: 'rgba(255, 255, 255, 0.55)', + marginTop: 8, + textAlign: 'center', + }, + controlLabelActive: { + color: '#FFFFFF', + fontWeight: '500', }, endCallButton: { alignItems: 'center', }, endCallCircle: { - width: END_CALL_SIZE, - height: END_CALL_SIZE, - borderRadius: END_CALL_SIZE / 2, + width: 64, + height: 64, + borderRadius: 32, backgroundColor: '#E54D42', justifyContent: 'center', alignItems: 'center', diff --git a/src/services/core/index.ts b/src/services/core/index.ts index 57616d4..4713af0 100644 --- a/src/services/core/index.ts +++ b/src/services/core/index.ts @@ -24,14 +24,11 @@ export type { WSCallRejectedMessage, WSCallBusyMessage, WSCallEndedMessage, - WSCallSDPMessage, - WSCallICEMessage, WSCallPeerMutedMessage, WSCallInvitedMessage, WSCallAnsweredElsewhereMessage, WSErrorMessage, WSServerMessage, - ICEServerConfig, } from './wsService'; export { handleError, createErrorHandler, safeAsync, createAppError } from './errorHandler'; diff --git a/src/services/core/wsService.ts b/src/services/core/wsService.ts index b2e243e..006062c 100644 --- a/src/services/core/wsService.ts +++ b/src/services/core/wsService.ts @@ -39,8 +39,7 @@ export type WSMessageType = | 'call_accepted' | 'call_rejected' | 'call_busy' - | 'call_sdp' - | 'call_ice' + | 'call_ready' | 'call_ended' | 'call_peer_muted' | 'call_invited' @@ -56,20 +55,12 @@ export interface WSCallIncomingMessage { media_type?: string; created_at: number; lifetime?: number; - ice_servers?: ICEServerConfig[]; -} - -export interface ICEServerConfig { - urls: string[]; - username?: string; - credential?: string; } export interface WSCallAcceptedMessage { type: 'call_accepted'; call_id: string; started_at: number; - ice_servers?: ICEServerConfig[]; } export interface WSCallRejectedMessage { @@ -92,25 +83,6 @@ export interface WSCallEndedMessage { ended_at?: number; } -export interface WSCallSDPMessage { - type: 'call_sdp'; - call_id: string; - from_id: string; - payload: { - sdp_type: string; - sdp: string; - }; -} - -export interface WSCallICEMessage { - type: 'call_ice'; - call_id: string; - from_id: string; - payload: { - candidate: string; - }; -} - export interface WSCallPeerMutedMessage { type: 'call_peer_muted'; call_id: string; @@ -282,8 +254,6 @@ export type WSMessage = | WSCallRejectedMessage | WSCallBusyMessage | WSCallEndedMessage - | WSCallSDPMessage - | WSCallICEMessage | WSCallPeerMutedMessage | WSCallInvitedMessage | WSCallAnsweredElsewhereMessage @@ -496,12 +466,6 @@ class WebSocketService { case 'call_busy': this.handleCallBusy(payload); break; - case 'call_sdp': - this.handleCallSDP(payload); - break; - case 'call_ice': - this.handleCallICE(payload); - break; case 'call_ended': this.handleCallEnded(payload); break; @@ -664,7 +628,6 @@ class WebSocketService { call_type: payload.media_type || payload.call_type, created_at: payload.created_at, lifetime: payload.lifetime, - ice_servers: payload.ice_servers, }; this.emit('call_incoming', m); } @@ -674,7 +637,6 @@ class WebSocketService { type: 'call_accepted', call_id: payload.call_id, started_at: payload.started_at, - ice_servers: payload.ice_servers, }; this.emit('call_accepted', m); } @@ -696,26 +658,6 @@ class WebSocketService { this.emit('call_busy', m); } - private handleCallSDP(payload: any): void { - const m: WSCallSDPMessage = { - type: 'call_sdp', - call_id: payload.call_id, - from_id: payload.from_id, - payload: payload.payload, - }; - this.emit('call_sdp', m); - } - - private handleCallICE(payload: any): void { - const m: WSCallICEMessage = { - type: 'call_ice', - call_id: payload.call_id, - from_id: payload.from_id, - payload: payload.payload, - }; - this.emit('call_ice', m); - } - private handleCallEnded(payload: any): void { const m: WSCallEndedMessage = { type: 'call_ended', @@ -793,21 +735,6 @@ class WebSocketService { this.sendFireAndForget('call_busy', { call_id: callId }); } - sendCallSDP(callId: string, sdpType: string, sdp: string): void { - this.sendFireAndForget('call_sdp', { - call_id: callId, - sdp_type: sdpType, - sdp, - }); - } - - sendCallICE(callId: string, candidate: string): void { - this.sendFireAndForget('call_ice', { - call_id: callId, - candidate, - }); - } - sendCallEnd(callId: string, reason?: string): void { this.sendFireAndForget('call_end', { call_id: callId, reason }); } @@ -816,6 +743,10 @@ class WebSocketService { this.sendFireAndForget('call_mute', { call_id: callId, muted }); } + sendCallReady(callId: string): void { + this.sendFireAndForget('call_ready', { call_id: callId }); + } + private handleMessageSent(payload: any): void { // 找到对应的发送请求并resolve const pendingMsg = this.pendingMessages.find(p => diff --git a/src/services/livekit/LiveKitService.ts b/src/services/livekit/LiveKitService.ts new file mode 100644 index 0000000..32d670f --- /dev/null +++ b/src/services/livekit/LiveKitService.ts @@ -0,0 +1,220 @@ +import { Room, RoomEvent, Track, ConnectionState, LocalParticipant, RemoteParticipant, RemoteTrackPublication, TrackPublication, Participant as LKParticipant } from 'livekit-client'; + +export interface LiveKitServiceConfig { + url: string; + token: string; +} + +export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'failed'; + +export interface LiveKitEventMap { + connected: undefined; + disconnected: undefined; + reconnecting: undefined; + reconnected: undefined; + trackSubscribed: { participant: RemoteParticipant; publication: RemoteTrackPublication; track: Track }; + trackUnsubscribed: { participant: RemoteParticipant; publication: RemoteTrackPublication; track: Track }; + trackMuted: { participant: LKParticipant; publication: TrackPublication }; + trackUnmuted: { participant: LKParticipant; publication: TrackPublication }; + connectionStatusChanged: ConnectionStatus; + error: Error; +} + +type EventHandler = (data: LiveKitEventMap[K]) => void; + +class LiveKitServiceImpl { + private room: Room | null = null; + private handlers: Map>> = new Map(); + private _connectionStatus: ConnectionStatus = 'disconnected'; + + get connectionStatus(): ConnectionStatus { + return this._connectionStatus; + } + + get connected(): boolean { + return this._connectionStatus === 'connected'; + } + + get localParticipant(): LocalParticipant | null { + return this.room?.localParticipant ?? null; + } + + get remoteParticipants(): Map { + return this.room?.remoteParticipants ?? new Map(); + } + + async connect(url: string, token: string): Promise { + if (this.room) { + await this.disconnect(); + } + + this.room = new Room({ + adaptiveStream: true, + dynacast: true, + }); + + this.setupRoomListeners(); + + this._connectionStatus = 'connecting'; + this.emit('connectionStatusChanged', 'connecting'); + + try { + await this.room.connect(url, token); + this._connectionStatus = 'connected'; + this.emit('connectionStatusChanged', 'connected'); + this.emit('connected', undefined); + } catch (err) { + this._connectionStatus = 'failed'; + this.emit('connectionStatusChanged', 'failed'); + this.emit('error', err instanceof Error ? err : new Error(String(err))); + throw err; + } + } + + async disconnect(): Promise { + if (!this.room) return; + + this.room.off(RoomEvent.TrackSubscribed, this.onTrackSubscribed); + this.room.off(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed); + this.room.off(RoomEvent.TrackMuted, this.onTrackMuted); + this.room.off(RoomEvent.TrackUnmuted, this.onTrackUnmuted); + this.room.off(RoomEvent.Disconnected, this.onDisconnected); + this.room.off(RoomEvent.Reconnecting, this.onReconnecting); + this.room.off(RoomEvent.Reconnected, this.onReconnected); + this.room.off(RoomEvent.ConnectionStateChanged, this.onConnectionStateChanged); + + await this.room.disconnect(); + this.room = null; + this._connectionStatus = 'disconnected'; + this.emit('connectionStatusChanged', 'disconnected'); + this.emit('disconnected', undefined); + } + + async setMuted(muted: boolean): Promise { + if (!this.room?.localParticipant) return; + await this.room.localParticipant.setMicrophoneEnabled(!muted); + } + + async setVideoEnabled(enabled: boolean): Promise { + if (!this.room?.localParticipant) return; + await this.room.localParticipant.setCameraEnabled(enabled); + } + + isMuted(): boolean { + if (!this.room?.localParticipant) return true; + const audioTrack = this.room.localParticipant.getTrackPublication(Track.Source.Microphone); + return audioTrack?.isMuted ?? true; + } + + isVideoEnabled(): boolean { + if (!this.room?.localParticipant) return false; + const videoTrack = this.room.localParticipant.getTrackPublication(Track.Source.Camera); + return videoTrack?.isSubscribed ?? false; + } + + getRemoteAudioTrack(): Track | null { + for (const [, participant] of this.remoteParticipants) { + const audioPub = participant.getTrackPublication(Track.Source.Microphone); + if (audioPub?.track) return audioPub.track; + } + return null; + } + + getRemoteVideoTrack(): Track | null { + for (const [, participant] of this.remoteParticipants) { + const videoPub = participant.getTrackPublication(Track.Source.Camera); + if (videoPub?.track) return videoPub.track; + } + return null; + } + + on(event: K, handler: EventHandler): () => void { + if (!this.handlers.has(event)) { + this.handlers.set(event, new Set()); + } + this.handlers.get(event)!.add(handler); + return () => { + this.handlers.get(event)?.delete(handler); + }; + } + + dispose(): void { + this.disconnect(); + this.handlers.clear(); + } + + private setupRoomListeners(): void { + if (!this.room) return; + + this.room.on(RoomEvent.TrackSubscribed, this.onTrackSubscribed); + this.room.on(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed); + this.room.on(RoomEvent.TrackMuted, this.onTrackMuted); + this.room.on(RoomEvent.TrackUnmuted, this.onTrackUnmuted); + this.room.on(RoomEvent.Disconnected, this.onDisconnected); + this.room.on(RoomEvent.Reconnecting, this.onReconnecting); + this.room.on(RoomEvent.Reconnected, this.onReconnected); + this.room.on(RoomEvent.ConnectionStateChanged, this.onConnectionStateChanged); + } + + private onTrackSubscribed = (track: Track, publication: RemoteTrackPublication, participant: RemoteParticipant): void => { + this.emit('trackSubscribed', { participant, publication, track }); + }; + + private onTrackUnsubscribed = (track: Track, publication: RemoteTrackPublication, participant: RemoteParticipant): void => { + this.emit('trackUnsubscribed', { participant, publication, track }); + }; + + private onTrackMuted = (publication: TrackPublication, participant: LKParticipant): void => { + this.emit('trackMuted', { participant, publication }); + }; + + private onTrackUnmuted = (publication: TrackPublication, participant: LKParticipant): void => { + this.emit('trackUnmuted', { participant, publication }); + }; + + private onDisconnected = (): void => { + this._connectionStatus = 'disconnected'; + this.emit('connectionStatusChanged', 'disconnected'); + this.emit('disconnected', undefined); + }; + + private onReconnecting = (): void => { + this._connectionStatus = 'reconnecting'; + this.emit('connectionStatusChanged', 'reconnecting'); + this.emit('reconnecting', undefined); + }; + + private onReconnected = (): void => { + this._connectionStatus = 'connected'; + this.emit('connectionStatusChanged', 'connected'); + this.emit('reconnected', undefined); + }; + + private onConnectionStateChanged = (state: ConnectionState): void => { + const status: ConnectionStatus = state === ConnectionState.Connected + ? 'connected' + : state === ConnectionState.Connecting + ? 'connecting' + : state === ConnectionState.Reconnecting + ? 'reconnecting' + : 'disconnected'; + this._connectionStatus = status; + this.emit('connectionStatusChanged', status); + }; + + private emit(event: K, data: LiveKitEventMap[K]): void { + const handlers = this.handlers.get(event); + if (handlers) { + for (const handler of handlers) { + try { + handler(data); + } catch (err) { + console.error(`LiveKit event handler error for ${event}:`, err); + } + } + } + } +} + +export const liveKitService = new LiveKitServiceImpl(); +export default liveKitService; \ No newline at end of file diff --git a/src/services/livekit/index.ts b/src/services/livekit/index.ts new file mode 100644 index 0000000..fb8e828 --- /dev/null +++ b/src/services/livekit/index.ts @@ -0,0 +1,2 @@ +export { liveKitService, default } from './LiveKitService'; +export type { LiveKitServiceConfig, LiveKitEventMap, ConnectionStatus } from './LiveKitService'; \ No newline at end of file diff --git a/src/services/webrtc/WebRTCManager.ts b/src/services/webrtc/WebRTCManager.ts deleted file mode 100644 index 36eb0e8..0000000 --- a/src/services/webrtc/WebRTCManager.ts +++ /dev/null @@ -1,672 +0,0 @@ -import { - RTCPeerConnection, - RTCSessionDescription, - RTCIceCandidate, - mediaDevices, - MediaStream, -} from 'react-native-webrtc'; - -export interface ICEServer { - urls: string[]; - username?: string; - credential?: string; -} - -export type ConnectionState = 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'; - -export type CallType = 'voice' | 'video'; - -export type WebRTCManagerEvent = - | { type: 'icecandidate'; candidate: RTCIceCandidate | null } - | { type: 'connectionstatechange'; state: ConnectionState } - | { type: 'remotestream'; stream: MediaStream } - | { type: 'negotiationneeded'; offer: RTCSessionDescriptionInit } - | { type: 'error'; error: Error }; - -type EventHandler = (event: WebRTCManagerEvent) => void; - -class WebRTCManager { - private peerConnection: RTCPeerConnection | null = null; - private localStream: MediaStream | null = null; - private remoteStream: MediaStream | null = null; - private pendingCandidates: RTCIceCandidateInit[] = []; - private iceServers: ICEServer[] = []; - private eventHandlers: Set = new Set(); - private disposed = false; - private isInitiator = false; - - // ICE restart 相关状态 - private reconnectAttempts = 0; - private readonly MAX_RECONNECT_ATTEMPTS = 3; - private reconnectTimer: ReturnType | null = null; - private disconnectTimer: ReturnType | null = null; - - async initialize(iceServers: ICEServer[] = []): Promise { - if (this.peerConnection) { - this.dispose(); - } - this.disposed = false; - this.iceServers = iceServers.length > 0 - ? iceServers - : [{ urls: ['stun:stun.l.google.com:19302'] }]; - } - - private buildPeerConnectionConfig(): RTCConfiguration { - return { - iceServers: this.iceServers.map((server) => ({ - urls: server.urls, - ...(server.username ? { username: server.username } : {}), - ...(server.credential ? { credential: server.credential } : {}), - })), - iceCandidatePoolSize: 10, - }; - } - - private createPeerConnection(): RTCPeerConnection { - const config = this.buildPeerConnectionConfig(); - const pc = new RTCPeerConnection(config); - - // @ts-ignore - pc.onicecandidate = (event) => { - if (event.candidate) { - this.emit({ type: 'icecandidate', candidate: event.candidate }); - } else { - this.emit({ type: 'icecandidate', candidate: null }); - } - }; - - // @ts-ignore - pc.oniceconnectionstatechange = () => { - const state = pc.iceConnectionState as ConnectionState; - this.emit({ type: 'connectionstatechange', state }); - this.handleIceConnectionStateChange(state); - }; - - // @ts-ignore - pc.onconnectionstatechange = () => { - const state = pc.connectionState as ConnectionState; - this.emit({ type: 'connectionstatechange', state }); - this.handleConnectionStateChange(state); - }; - - // @ts-ignore - pc.ontrack = (event) => { - console.log('[WebRTC] ontrack event, kind:', event.track?.kind, 'streams:', event.streams?.length ?? 0); - if (!event.track) { - console.log('[WebRTC] ontrack: no track in event'); - return; - } - - // Always manually construct/append to remoteStream to ensure all tracks are collected - // react-native-webrtc may fire ontrack per-track and event.streams[0] might not contain all tracks - if (!this.remoteStream) { - this.remoteStream = new MediaStream(); - } - - // Check if track already exists to avoid duplicates - const existingTracks = this.remoteStream.getTracks(); - const trackExists = existingTracks.some(t => t.id === event.track.id); - if (!trackExists) { - this.remoteStream.addTrack(event.track); - console.log('[WebRTC] Added remote track:', event.track.kind, 'total tracks:', this.remoteStream.getTracks().length); - } else { - console.log('[WebRTC] Remote track already exists:', event.track.kind); - } - - this.emit({ type: 'remotestream', stream: this.remoteStream }); - }; - - // @ts-ignore - pc.onsignalingstatechange = () => { - console.log('[WebRTC] Signaling state changed:', pc.signalingState); - }; - - // @ts-ignore - pc.onnegotiationneeded = async () => { - console.log('[WebRTC] Negotiation needed, signalingState:', pc.signalingState); - if (!this.peerConnection || this.peerConnection !== pc) { - console.log('[WebRTC] PeerConnection changed or disposed, skipping'); - return; - } - if (pc.signalingState !== 'stable') { - console.log('[WebRTC] Skipping negotiation, not in stable state:', pc.signalingState); - return; - } - try { - const offer = await this.createOffer(); - if (this.peerConnection && !this.disposed) { - this.emit({ type: 'negotiationneeded', offer }); - } - } catch (err) { - console.error('[WebRTC] Negotiation needed failed:', err); - } - }; - - return pc; - } - - async createLocalStream(voiceOnly = true): Promise { - const constraints = voiceOnly - ? { audio: true, video: false } - : { audio: true, video: { facingMode: 'user', frameRate: 30 } }; - - try { - // @ts-ignore - this.localStream = await mediaDevices.getUserMedia(constraints); - return this.localStream; - } catch (error) { - console.error('[WebRTC] Failed to get local media stream:', error); - throw error; - } - } - - /** - * Start a call using official addTrack approach - * Follows react-native-webrtc CallGuide.md - */ - async startCall(isInitiator: boolean, callType: CallType = 'voice'): Promise { - if (this.disposed) throw new Error('WebRTCManager has been disposed'); - if (!this.localStream) throw new Error('Local stream not initialized'); - - this.isInitiator = isInitiator; - this.peerConnection = this.createPeerConnection(); - - // Official approach: add all tracks using addTrack - // addTrack automatically creates senders with proper directions - this.localStream.getTracks().forEach((track) => { - console.log('[WebRTC] Adding track:', track.kind, track.enabled); - this.peerConnection!.addTrack(track, this.localStream!); - }); - - if (isInitiator) { - // For initiator, create offer directly - const offer = await this.createOffer(); - return offer; - } - return null; - } - - async createOffer(): Promise { - if (!this.peerConnection) throw new Error('PeerConnection not initialized'); - - console.log('[WebRTC] Creating offer...'); - - const offerOptions = { - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }; - - const offer = await this.peerConnection.createOffer(offerOptions); - - if (!this.peerConnection || this.disposed) { - throw new Error('PeerConnection was disposed during offer creation'); - } - - await this.peerConnection.setLocalDescription(offer); - return offer; - } - - async createAnswer(): Promise { - if (!this.peerConnection) throw new Error('PeerConnection not initialized'); - - console.log('[WebRTC] Creating answer...'); - - const answerOptions = { - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }; - - // @ts-ignore - const answer = await this.peerConnection.createAnswer(answerOptions); - - if (!this.peerConnection || this.disposed) { - throw new Error('PeerConnection was disposed during answer creation'); - } - - await this.peerConnection.setLocalDescription(answer); - - // Process pending candidates - await this.processPendingCandidates(); - - return answer; - } - - async createAnswerFromOffer(offer: RTCSessionDescriptionInit): Promise { - await this.setRemoteDescription(offer); - return this.createAnswer(); - } - - /** - * Rollback to stable state (for glare handling) - * Used when both peers try to negotiate simultaneously - */ - async rollback(): Promise { - if (!this.peerConnection) { - throw new Error('PeerConnection not initialized'); - } - - const pc = this.peerConnection; - const signalingState = pc.signalingState; - - console.log('[WebRTC] Attempting rollback, current state:', signalingState); - - // Only rollback if we're not in stable state - if (signalingState === 'stable') { - console.log('[WebRTC] Already in stable state, no rollback needed'); - return; - } - - try { - // For react-native-webrtc, we may need to recreate the peer connection - // as rollback is not fully supported - if (signalingState === 'have-local-offer') { - // Rollback local offer by setting local description to null/undefined - // @ts-ignore - react-native-webrtc specific - if (pc.setLocalDescription) { - // @ts-ignore - await pc.setLocalDescription({ type: 'rollback' }); - } - } else if (signalingState === 'have-remote-offer') { - // Rollback remote offer - // @ts-ignore - if (pc.setRemoteDescription) { - // @ts-ignore - await pc.setRemoteDescription({ type: 'rollback' }); - } - } - - console.log('[WebRTC] Rollback successful'); - } catch (err) { - console.error('[WebRTC] Rollback failed:', err); - throw err; - } - } - - async setRemoteDescription(description: RTCSessionDescriptionInit): Promise { - if (!this.peerConnection) { - console.error('[WebRTC] setRemoteDescription: PeerConnection is null'); - throw new Error('PeerConnection not initialized'); - } - - if (!description.sdp) { - throw new Error('setRemoteDescription: sdp is required'); - } - - console.log('[WebRTC] Setting remote description, type:', description.type); - - const desc = new RTCSessionDescription({ - type: description.type, - sdp: description.sdp, - }); - await this.peerConnection.setRemoteDescription(desc); - - console.log('[WebRTC] Remote description set successfully'); - - // Process pending candidates after remote description is set - await this.processPendingCandidates(); - } - - async addIceCandidate(candidate: RTCIceCandidateInit): Promise { - if (!this.peerConnection) { - this.pendingCandidates.push(candidate); - return; - } - - if (!this.peerConnection.remoteDescription) { - this.pendingCandidates.push(candidate); - return; - } - - try { - const iceCandidate = new RTCIceCandidate(candidate); - await this.peerConnection.addIceCandidate(iceCandidate); - } catch (error) { - console.error('[WebRTC] Failed to add ICE candidate:', error); - this.pendingCandidates.push(candidate); - } - } - - private async processPendingCandidates(): Promise { - if (this.pendingCandidates.length === 0) return; - if (!this.peerConnection) return; - - const candidates = [...this.pendingCandidates]; - this.pendingCandidates = []; - - for (const candidate of candidates) { - if (!this.peerConnection) return; - - try { - const iceCandidate = new RTCIceCandidate(candidate); - await this.peerConnection.addIceCandidate(iceCandidate); - } catch (error) { - console.error('[WebRTC] Failed to add pending ICE candidate:', error); - } - } - } - - setMuted(muted: boolean): void { - if (!this.localStream) return; - const audioTracks = this.localStream.getAudioTracks(); - audioTracks.forEach((track) => { - track.enabled = !muted; - }); - } - - isMuted(): boolean { - if (!this.localStream) return false; - const audioTracks = this.localStream.getAudioTracks(); - return audioTracks.some((track) => !track.enabled); - } - - /** - * Enable video - official replaceTrack approach - */ - async enableVideo(): Promise { - if (!this.peerConnection) throw new Error('PeerConnection not initialized'); - - console.log('[WebRTC] Enabling video...'); - - try { - // Get video stream - const videoStream = await mediaDevices.getUserMedia({ - video: { facingMode: 'user', frameRate: 30 }, - audio: false, - }); - - const videoTrack = videoStream.getVideoTracks()[0]; - - // Find the sender for video and replace the track - const senders = this.peerConnection.getSenders(); - const videoSender = senders.find(s => s.track?.kind === 'video'); - - if (videoSender) { - await videoSender.replaceTrack(videoTrack); - console.log('[WebRTC] Video track replaced on existing sender'); - } else { - // No existing video sender, add track - this.peerConnection.addTrack(videoTrack, this.localStream!); - console.log('[WebRTC] Video track added via addTrack'); - } - - // Update local stream - const newStream = new MediaStream(); - - if (this.localStream) { - this.localStream.getAudioTracks().forEach((track) => { - newStream.addTrack(track); - }); - // Stop old video tracks - this.localStream.getVideoTracks().forEach((track) => { - track.stop(); - }); - } - - newStream.addTrack(videoTrack); - this.localStream = newStream; - - console.log('[WebRTC] Video enabled successfully'); - - return newStream; - } catch (error) { - console.error('[WebRTC] Failed to enable video:', error); - throw error; - } - } - - /** - * Disable video - official replaceTrack approach - */ - async disableVideo(): Promise { - if (!this.peerConnection) { - console.log('[WebRTC] disableVideo: No peer connection'); - return null; - } - if (!this.localStream) { - console.log('[WebRTC] disableVideo: No local stream'); - return null; - } - - console.log('[WebRTC] Disabling video...'); - - // Find the sender for video and replace with null - const senders = this.peerConnection.getSenders(); - const videoSender = senders.find(s => s.track?.kind === 'video'); - - if (videoSender) { - await videoSender.replaceTrack(null); - console.log('[WebRTC] Video track removed from sender'); - } - - // Stop video tracks - const videoTracks = this.localStream.getVideoTracks(); - videoTracks.forEach((track) => { - track.stop(); - }); - - // Create new stream with only audio - const newStream = new MediaStream(); - this.localStream.getAudioTracks().forEach((track) => { - newStream.addTrack(track); - }); - - this.localStream = newStream; - console.log('[WebRTC] Video disabled successfully'); - - return newStream; - } - - isVideoEnabled(): boolean { - if (!this.localStream) return false; - const videoTracks = this.localStream.getVideoTracks(); - return videoTracks.length > 0 && videoTracks.some((track) => track.enabled); - } - - getRemoteStream(): MediaStream | null { - return this.remoteStream; - } - - getLocalStream(): MediaStream | null { - return this.localStream; - } - - getPeerConnection(): RTCPeerConnection | null { - return this.peerConnection; - } - - getSignalingState(): RTCSignalingState | null { - return this.peerConnection?.signalingState || null; - } - - getIsInitiator(): boolean { - return this.isInitiator; - } - - onEvent(handler: EventHandler): () => void { - this.eventHandlers.add(handler); - return () => { - this.eventHandlers.delete(handler); - }; - } - - private emit(event: WebRTCManagerEvent): void { - this.eventHandlers.forEach((handler) => { - try { - handler(event); - } catch (error) { - console.error('[WebRTC] Event handler error:', error); - } - }); - } - - // ========== ICE Restart 支持 ========== - - /** - * 处理 ICE 连接状态变化 - * 根据 W3C 规范: disconnected 状态可能间歇性触发并自发解决 - * failed 状态表示需要 ICE restart - */ - private handleIceConnectionStateChange(state: ConnectionState): void { - console.log('[WebRTC] ICE connection state:', state); - - switch (state) { - case 'connected': // 连接成功,重置重连状态 - this.resetReconnectState(); - break; - - case 'disconnected': - // 临时断开,等待一段时间看是否自动恢复 - this.scheduleDisconnectCheck(); - break; - - case 'failed': - // ICE 失败,尝试 ICE restart - this.attemptIceRestart(); - break; - - case 'closed': - this.clearReconnectTimers(); - break; - } - } - - /** - * 处理 PeerConnection 连接状态变化 - */ - private handleConnectionStateChange(state: ConnectionState): void { - console.log('[WebRTC] PeerConnection state:', state); - - switch (state) { - case 'connected': - this.resetReconnectState(); - break; - - case 'disconnected': - // 等待短暂时间看是否自动恢复 - this.scheduleDisconnectCheck(); - break; - - case 'failed': - // 连接完全失败 - this.emit({ type: 'error', error: new Error('Connection failed') }); - break; - } - } - - /** - * 安排断开检查 - * 给 disconnected 状态一个恢复窗口(5秒) - */ - private scheduleDisconnectCheck(): void { - if (this.disconnectTimer) { - clearTimeout(this.disconnectTimer); - } - - this.disconnectTimer = setTimeout(() => { - const pc = this.peerConnection; - if (!pc || this.disposed) return; - - // 如果 5 秒后仍然是 disconnected,尝试 ICE restart - if (pc.iceConnectionState === 'disconnected' || pc.iceConnectionState === 'failed') { - console.log('[WebRTC] Connection still disconnected after 5s, attempting ICE restart'); - this.attemptIceRestart(); - } - }, 5000); - } - - /** - * 尝试 ICE restart - * 参考: https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Session_lifetime#ice_restart - */ - private async attemptIceRestart(): Promise { - if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) { - console.error('[WebRTC] Max reconnection attempts reached'); - this.emit({ type: 'error', error: new Error('Max reconnection attempts reached') }); - return; - } - - const pc = this.peerConnection; - if (!pc || this.disposed) { - console.log('[WebRTC] Cannot restart ICE: PeerConnection not available'); - return; - } - - // 检查信令状态 - if (pc.signalingState !== 'stable') { - console.log('[WebRTC] Cannot restart ICE: signaling state not stable:', pc.signalingState); - return; - } - - this.reconnectAttempts++; - console.log(`[WebRTC] Attempting ICE restart (${this.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})`); - - try { - // 尝试使用 restartIce() API (现代浏览器支持) - // @ts-ignore - if (pc.restartIce) { - // @ts-ignore - pc.restartIce(); - console.log('[WebRTC] restartIce() called'); - } - - // 创建新的 offer,触发 ICE restart - const offer = await pc.createOffer({ iceRestart: true }); - await pc.setLocalDescription(offer); - - console.log('[WebRTC] ICE restart offer created'); - - // 发送新的 offer 给对方 - this.emit({ type: 'negotiationneeded', offer }); - } catch (error) { - console.error('[WebRTC] ICE restart failed:', error); - this.emit({ type: 'error', error: error as Error }); - } - } - - /** - * 重置重连状态 - */ - private resetReconnectState(): void { - this.reconnectAttempts = 0; - this.clearReconnectTimers(); - } - - /** - * 清除重连定时器 - */ - private clearReconnectTimers(): void { - if (this.disconnectTimer) { - clearTimeout(this.disconnectTimer); - this.disconnectTimer = null; - } - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } - - dispose(): void { - this.disposed = true; - this.eventHandlers.clear(); - this.pendingCandidates = []; - this.clearReconnectTimers(); - - if (this.localStream) { - this.localStream.getTracks().forEach((track) => track.stop()); - this.localStream = null; - } - - if (this.remoteStream) { - // Do not stop remote tracks; they are managed by the peer connection - // and will be ended automatically when the connection closes - this.remoteStream = null; - } - - if (this.peerConnection) { - this.peerConnection.close(); - this.peerConnection = null; - } - } -} - -export const webrtcManager = new WebRTCManager(); diff --git a/src/services/webrtc/WebRTCManager.web.ts b/src/services/webrtc/WebRTCManager.web.ts deleted file mode 100644 index 7c372fb..0000000 --- a/src/services/webrtc/WebRTCManager.web.ts +++ /dev/null @@ -1,89 +0,0 @@ -// WebRTCManager for Web platform (stub implementation) -import type { - ICEServer, - ConnectionState, - WebRTCManagerEvent, -} from './WebRTCManager'; - -type EventHandler = (event: WebRTCManagerEvent) => void; - -class WebRTCManagerWeb { - private eventHandlers: Set = new Set(); - private disposed = false; - - async initialize(iceServers: ICEServer[] = []): Promise { - console.log('[WebRTC] WebRTC not supported on web platform'); - } - - async createLocalStream(voiceOnly = true): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - async startCall(isInitiator: boolean): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - async createOffer(): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - async createAnswer(): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - async createAnswerFromOffer(offer: RTCSessionDescriptionInit): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - async setRemoteDescription(description: RTCSessionDescriptionInit): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - } - - async addIceCandidate(candidate: RTCIceCandidateInit): Promise { - console.warn('[WebRTC] WebRTC not supported on web platform'); - } - - setMuted(muted: boolean): void { - console.warn('[WebRTC] WebRTC not supported on web platform'); - } - - isMuted(): boolean { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return false; - } - - getRemoteStream(): MediaStream | null { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - getLocalStream(): MediaStream | null { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - getPeerConnection(): RTCPeerConnection | null { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return null; - } - - onEvent(handler: EventHandler): () => void { - console.warn('[WebRTC] WebRTC not supported on web platform'); - return () => { - this.eventHandlers.delete(handler); - }; - } - - dispose(): void { - this.disposed = true; - this.eventHandlers.clear(); - console.log('[WebRTC] WebRTC disposed on web platform'); - } -} - -export const webrtcManager = new WebRTCManagerWeb(); \ No newline at end of file diff --git a/src/services/webrtc/index.ts b/src/services/webrtc/index.ts deleted file mode 100644 index 1c1d65d..0000000 --- a/src/services/webrtc/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { webrtcManager } from './WebRTCManager'; -export type { ICEServer, ConnectionState, WebRTCManagerEvent } from './WebRTCManager'; diff --git a/src/stores/call/callStore.ts b/src/stores/call/callStore.ts index f09834e..bf75805 100644 --- a/src/stores/call/callStore.ts +++ b/src/stores/call/callStore.ts @@ -1,26 +1,24 @@ import { create } from 'zustand'; -import { MediaStream } from 'react-native-webrtc'; import { wsService, WSCallIncomingMessage, - WSCallSDPMessage, - WSCallICEMessage, WSErrorMessage, + api, } from '@/services/core'; -import { webrtcManager, ICEServer } from '@/services/webrtc'; +import { liveKitService } from '@/services/livekit'; import { getCurrentUserId } from '../auth/sessionStore'; import { useUserStore } from '../userStore'; import { userManager } from '../user/UserManager'; -export type CallStatus = - | 'idle' // 空闲状态 - | 'calling' // 正在呼出(已发送邀请,等待对方响应) - | 'ringing' // 来电响铃中 - | 'connecting' // 正在建立连接(WebRTC 协商中) - | 'connected' // 已接通 - | 'reconnecting' // 网络断开,正在重连 - | 'ended' // 已结束 - | 'failed'; // 连接失败 +export type CallStatus = + | 'idle' + | 'calling' + | 'ringing' + | 'connecting' + | 'connected' + | 'reconnecting' + | 'ended' + | 'failed'; export type CallType = 'voice' | 'video'; @@ -39,6 +37,7 @@ export interface CallSession { isVideoEnabled: boolean; isPeerVideoEnabled: boolean; isInitiator: boolean; + isPeerReady: boolean; } export interface IncomingCallInfo { @@ -48,7 +47,6 @@ export interface IncomingCallInfo { callerName?: string; callerAvatar?: string | null; callType: string; - iceServers: ICEServer[]; receivedAt: number; lifetime?: number; } @@ -57,8 +55,6 @@ interface CallState { currentCall: CallSession | null; incomingCall: IncomingCallInfo | null; callDuration: number; - peerStream: MediaStream | null; - localStream: MediaStream | null; isMinimized: boolean; initCall: () => () => void; @@ -84,15 +80,13 @@ let durationTimer: ReturnType | null = null; let callTimeoutTimer: ReturnType | null = null; let initCallUnsub: (() => void) | null = null; let unsubInvited: (() => void) | null = null; -let pendingOffer: { callId: string; sdp: string } | null = null; -let rtcUnsubscribe: (() => void) | null = null; // WebRTC event subscription +let liveKitUnsubs: (() => void)[] = []; // Constants const CALL_LIFETIME_MS = 55000; const CALL_TIMEOUT_MS = 115000; const IGNORE_CALL_ID_TTL = 30000; -// Track processed call IDs to prevent duplicates const processedCallIds = new Map(); function cleanupProcessedCallIds() { @@ -105,222 +99,160 @@ function cleanupProcessedCallIds() { } /** - * Unified WebRTC event handler - * This is called from both initiator and receiver paths + * Fetch LiveKit token from the backend and connect to the room. */ -function setupWebRTCEvents(callId: string, myUserId: string): void { - // Clean up any existing subscription - if (rtcUnsubscribe) { - rtcUnsubscribe(); - rtcUnsubscribe = null; +async function joinLiveKitRoom(callId: string, videoEnabled: boolean): Promise { + const res = await api.get<{ token: string; url: string }>('/calls/token', { + room: callId, + }); + + const { token, url } = res.data; + if (!token || !url) { + throw new Error('Invalid LiveKit token response'); } - rtcUnsubscribe = webrtcManager.onEvent((event) => { - switch (event.type) { - case 'icecandidate': - if (event.candidate) { - wsService.sendCallICE(callId, JSON.stringify(event.candidate.toJSON())); - } - break; + setupLiveKitEvents(callId); - case 'remotestream': - handleRemoteStream(event.stream); - break; + await liveKitService.connect(url, token); - case 'negotiationneeded': - handleNegotiationNeeded(callId, event.offer); - break; - - case 'connectionstatechange': - handleConnectionStateChange(event.state); - break; - - case 'error': - console.error('[CallStore] WebRTC error:', event.error); - break; - } - }); + // Enable/disable video based on call type + await liveKitService.setVideoEnabled(videoEnabled); } /** - * Handle remote stream with video track detection + * Set up LiveKit room event handlers. */ -function handleRemoteStream(stream: MediaStream): void { - // Create a new MediaStream instance to force zustand subscribers to update - // because react-native-webrtc may add tracks to the same stream object reference - const newStream = new MediaStream(); - stream.getTracks().forEach((track) => { - newStream.addTrack(track); - }); +function setupLiveKitEvents(callId: string): void { + // Clean up any existing subscriptions + liveKitUnsubs.forEach((unsub) => unsub()); + liveKitUnsubs = []; - callStore.setState({ peerStream: newStream }); + liveKitUnsubs.push( + liveKitService.on('connected', () => { + console.log('[CallStore] LiveKit connected'); + const now = Date.now(); + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, status: 'connected', startedAt: now } + : null, + })); - // Detect video tracks in remote stream - const videoTracks = newStream.getVideoTracks(); - const hasPeerVideo = videoTracks.length > 0 && videoTracks.some((t) => t.enabled); + wsService.sendCallReady(callId); - console.log('[CallStore] Remote stream received, hasVideo:', hasPeerVideo, 'videoTracks:', videoTracks.length, 'audioTracks:', newStream.getAudioTracks().length); + if (durationTimer) clearInterval(durationTimer); + durationTimer = setInterval(() => { + callStore.setState((s) => ({ callDuration: s.callDuration + 1 })); + }, 1000); + }) + ); - callStore.setState((s) => ({ - currentCall: s.currentCall - ? { ...s.currentCall, isPeerVideoEnabled: hasPeerVideo } - : null, - })); -} - -/** - * Handle negotiation needed event - send offer to peer - */ -function handleNegotiationNeeded(callId: string, offer: RTCSessionDescriptionInit): void { - console.log('[CallStore] Negotiation needed, sending offer'); - wsService.sendCallSDP(callId, 'offer', offer.sdp || ''); -} - - /** - * Handle connection state change with enhanced state machine - */ - function handleConnectionStateChange(state: string): void { - console.log('[CallStore] Connection state changed:', state); - - const { currentCall } = callStore.getState(); - if (!currentCall) return; - - switch (state) { - case 'connected': - // 连接成功,开始计时 - const now = Date.now(); + liveKitUnsubs.push( + liveKitService.on('disconnected', () => { + console.log('[CallStore] LiveKit disconnected'); + const { currentCall } = callStore.getState(); + if (currentCall && currentCall.status !== 'ended' && currentCall.status !== 'failed') { callStore.setState((s) => ({ currentCall: s.currentCall - ? { ...s.currentCall, status: 'connected', startedAt: now } + ? { ...s.currentCall, status: 'reconnecting' } : null, })); - - if (durationTimer) clearInterval(durationTimer); - durationTimer = setInterval(() => { - callStore.setState((s) => ({ callDuration: s.callDuration + 1 })); - }, 1000); - break; - - case 'disconnected': - // 临时断开,进入重连状态 - if (currentCall.status === 'connected') { - callStore.setState((s) => ({ - currentCall: s.currentCall - ? { ...s.currentCall, status: 'reconnecting' } - : null, - })); - } - break; - - case 'failed': - // 连接失败 - if (currentCall.status === 'connected' || currentCall.status === 'reconnecting') { - callStore.getState().endCall('connection_failed'); - } else if (currentCall.status === 'connecting') { - // 初始连接失败 - callStore.getState().endCall('connection_failed'); - } - break; - - case 'closed': - // 连接关闭 - if (currentCall.status !== 'ended' && currentCall.status !== 'failed') { - callStore.getState().endCall('connection_closed'); - } - break; - } - } - -/** - * Handle incoming SDP offer with Glare handling - */ -async function handleIncomingOffer(msg: WSCallSDPMessage, myUserId: string): Promise { - let pc = webrtcManager.getPeerConnection(); - if (!pc) { - // Cache offer for later processing - pendingOffer = { callId: msg.call_id, sdp: msg.payload.sdp }; - console.log('[CallStore] Caching pending offer, PeerConnection not ready yet'); - return; - } - - let signalingState = pc.signalingState; - console.log('[CallStore] Received offer, signalingState:', signalingState); - - // Glare handling: if we're not in stable state, we have a conflict - if (signalingState !== 'stable') { - const remoteUserId = msg.from_id; - - // Compare user IDs to determine who wins - // Higher user ID wins the negotiation - if (myUserId > remoteUserId) { - console.log('[CallStore] Glare: I win (my ID > remote ID), ignoring incoming offer'); - return; - } - - console.log('[CallStore] Glare: Remote wins (remote ID > my ID), rolling back'); - - // Rollback to stable state - try { - await webrtcManager.rollback(); - } catch (err) { - console.error('[CallStore] Rollback failed:', err); - // If rollback fails because we're already stable, that's fine - just proceed - if (pc.signalingState !== 'stable') { - return; } - } + }) + ); - // Re-check state after rollback - signalingState = pc.signalingState; - if (signalingState !== 'stable') { - console.warn('[CallStore] Not stable after rollback, state:', signalingState); - return; - } - } + liveKitUnsubs.push( + liveKitService.on('reconnecting', () => { + console.log('[CallStore] LiveKit reconnecting'); + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, status: 'reconnecting' } + : null, + })); + }) + ); - pendingOffer = null; + liveKitUnsubs.push( + liveKitService.on('reconnected', () => { + console.log('[CallStore] LiveKit reconnected'); + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, status: 'connected' } + : null, + })); + }) + ); - // Set remote description and create answer - try { - await webrtcManager.setRemoteDescription({ - type: msg.payload.sdp_type as 'offer' | 'answer', - sdp: msg.payload.sdp, - }); + liveKitUnsubs.push( + liveKitService.on('connectionStatusChanged', (status) => { + console.log('[CallStore] LiveKit connection status:', status); + const { currentCall } = callStore.getState(); + if (!currentCall) return; - const answer = await webrtcManager.createAnswer(); - wsService.sendCallSDP(msg.call_id, 'answer', answer.sdp || ''); - } catch (err) { - console.error('[CallStore] Failed to handle incoming offer:', err); - } + if (status === 'failed') { + callStore.getState().endCall('connection_failed'); + } + }) + ); + + liveKitUnsubs.push( + liveKitService.on('trackSubscribed', ({ track }) => { + console.log('[CallStore] Remote track subscribed:', track.kind); + if (track.kind === 'video') { + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, isPeerVideoEnabled: true } + : null, + })); + } + }) + ); + + liveKitUnsubs.push( + liveKitService.on('trackUnsubscribed', ({ track }) => { + console.log('[CallStore] Remote track unsubscribed:', track.kind); + if (track.kind === 'video') { + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, isPeerVideoEnabled: false } + : null, + })); + } + }) + ); + + liveKitUnsubs.push( + liveKitService.on('trackMuted', ({ publication }) => { + if (publication.source === 'camera') { + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, isPeerVideoEnabled: false } + : null, + })); + } + }) + ); + + liveKitUnsubs.push( + liveKitService.on('trackUnmuted', ({ publication }) => { + if (publication.source === 'camera') { + callStore.setState((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, isPeerVideoEnabled: true } + : null, + })); + } + }) + ); + + liveKitUnsubs.push( + liveKitService.on('error', (err) => { + console.error('[CallStore] LiveKit error:', err); + }) + ); } /** - * Handle incoming SDP answer - */ -async function handleIncomingAnswer(msg: WSCallSDPMessage): Promise { - const pc = webrtcManager.getPeerConnection(); - if (!pc) return; - - const signalingState = pc.signalingState; - console.log('[CallStore] Received answer, signalingState:', signalingState); - - if (signalingState !== 'have-local-offer') { - console.warn('[CallStore] Ignoring answer, signaling state is', signalingState); - return; - } - - try { - await webrtcManager.setRemoteDescription({ - type: msg.payload.sdp_type as 'offer' | 'answer', - sdp: msg.payload.sdp, - }); - } catch (err) { - console.error('[CallStore] Failed to set remote description from answer:', err); - } -} - -/** - * Clean up all resources + * Clean up all resources. */ function cleanupResources(): void { if (callTimeoutTimer) { @@ -331,23 +263,17 @@ function cleanupResources(): void { clearInterval(durationTimer); durationTimer = null; } - if (rtcUnsubscribe) { - rtcUnsubscribe(); - rtcUnsubscribe = null; - } - pendingOffer = null; + liveKitUnsubs.forEach((unsub) => unsub()); + liveKitUnsubs = []; } export const callStore = create((set, get) => ({ currentCall: null, incomingCall: null, callDuration: 0, - peerStream: null, - localStream: null, isMinimized: false, initCall: () => { - // Prevent duplicate handler registration if (initCallUnsub) { initCallUnsub(); initCallUnsub = null; @@ -376,7 +302,6 @@ export const callStore = create((set, get) => ({ return; } - // Check lifetime expiry const callAge = Date.now() - msg.created_at; const lifetime = msg.lifetime || 60000; if (callAge > lifetime - 5000) { @@ -386,7 +311,6 @@ export const callStore = create((set, get) => ({ return; } - // Fetch caller info let caller: { nickname?: string; username?: string; avatar?: string | null } | null = useUserStore.getState().userCache[msg.caller_id]; if (!caller) { @@ -413,7 +337,6 @@ export const callStore = create((set, get) => ({ callerName, callerAvatar: caller?.avatar, callType: msg.call_type, - iceServers: msg.ice_servers || [], receivedAt: Date.now(), lifetime: msg.lifetime, }, @@ -421,7 +344,6 @@ export const callStore = create((set, get) => ({ processedCallIds.set(msg.call_id, Date.now()); - // Set timeout based on lifetime if (callTimeoutTimer) clearTimeout(callTimeoutTimer); const remainingTime = Math.max(lifetime - callAge - 1000, 5000); callTimeoutTimer = setTimeout(() => { @@ -451,25 +373,17 @@ export const callStore = create((set, get) => ({ callTimeoutTimer = null; } - const myUserId = getCurrentUserId() || ''; - try { + set((s) => ({ + currentCall: s.currentCall + ? { ...s.currentCall, status: 'connecting' } + : null, + })); + const isVideoCall = currentCall.callType === 'video'; - await webrtcManager.initialize(msg.ice_servers || []); - const newStream = await webrtcManager.createLocalStream(!isVideoCall); - - set({ localStream: newStream }); - - // Setup unified WebRTC event handler - setupWebRTCEvents(currentCall.id, myUserId); - - // Start call with transceiver-based approach - const offer = await webrtcManager.startCall(true, currentCall.callType); - if (offer) { - wsService.sendCallSDP(currentCall.id, 'offer', offer.sdp || ''); - } + await joinLiveKitRoom(currentCall.id, isVideoCall); } catch (err) { - console.error('[CallStore] call_accepted error:', err); + console.error('[CallStore] call_accepted LiveKit join error:', err); get().endCall('connection_failed'); } }) @@ -557,41 +471,6 @@ export const callStore = create((set, get) => ({ }) ); - unsubs.push( - wsService.on('call_sdp', async (msg: WSCallSDPMessage) => { - const { currentCall } = get(); - if (!currentCall || currentCall.id !== msg.call_id) return; - - const myUserId = getCurrentUserId() || ''; - if (msg.from_id === myUserId) return; - - if (msg.payload.sdp_type === 'offer') { - await handleIncomingOffer(msg, myUserId); - } else if (msg.payload.sdp_type === 'answer') { - await handleIncomingAnswer(msg); - } - }) - ); - - unsubs.push( - wsService.on('call_ice', (msg: WSCallICEMessage) => { - const { currentCall } = get(); - if (!currentCall || currentCall.id !== msg.call_id) return; - - const myUserId = getCurrentUserId() || ''; - if (msg.from_id === myUserId) return; - - try { - const candidate = typeof msg.payload.candidate === 'string' - ? JSON.parse(msg.payload.candidate) - : msg.payload.candidate; - webrtcManager.addIceCandidate(candidate); - } catch (err) { - console.error('[CallStore] call_ice error:', err); - } - }) - ); - unsubs.push( wsService.on('call_peer_muted', (msg) => { console.log('[CallStore] Peer muted:', msg.user_id, msg.muted); @@ -641,13 +520,14 @@ export const callStore = create((set, get) => ({ peerName: calleeName, peerAvatar: callee?.avatar, callType, - status: 'calling', // 改为 'calling' 表示正在呼出 + status: 'calling', duration: 0, isMuted: false, isSpeakerOn: false, isVideoEnabled: callType === 'video', isPeerVideoEnabled: false, isInitiator: true, + isPeerReady: false, }, }); @@ -667,7 +547,6 @@ export const callStore = create((set, get) => ({ if (callTimeoutTimer) clearTimeout(callTimeoutTimer); callTimeoutTimer = setTimeout(() => { const { currentCall: cc } = get(); - // 只有在 'calling' 状态(呼出中)才超时 if (cc && cc.status === 'calling') { console.warn('[CallStore] Call timeout'); get().endCall('timeout'); @@ -688,7 +567,6 @@ export const callStore = create((set, get) => ({ const isVideoCall = incomingCall.callType === 'video'; console.log('[CallStore] acceptCall, isVideoCall:', isVideoCall); - const myUserId = getCurrentUserId() || ''; set({ currentCall: { @@ -703,8 +581,9 @@ export const callStore = create((set, get) => ({ isMuted: false, isSpeakerOn: false, isVideoEnabled: isVideoCall, - isPeerVideoEnabled: isVideoCall, + isPeerVideoEnabled: false, isInitiator: false, + isPeerReady: false, }, incomingCall: null, }); @@ -712,19 +591,7 @@ export const callStore = create((set, get) => ({ wsService.sendCallAnswer(incomingCall.callId); try { - await webrtcManager.initialize(incomingCall.iceServers); - const newStream = await webrtcManager.createLocalStream(!isVideoCall); - - set({ localStream: newStream }); - - // Setup unified WebRTC event handler - setupWebRTCEvents(incomingCall.callId, myUserId); - - // Start call (non-initiator, will wait for offer) - await webrtcManager.startCall(false, incomingCall.callType as CallType); - - // Note: For non-initiator, we don't create an offer here. - // We wait for the initiator's offer via handleIncomingOffer. + await joinLiveKitRoom(incomingCall.callId, isVideoCall); } catch (err) { console.error('[CallStore] Failed to accept call:', err); get().endCall('connection_failed'); @@ -760,11 +627,13 @@ export const callStore = create((set, get) => ({ set({ currentCall: null, callDuration: 0, - peerStream: null, - localStream: null, }); - webrtcManager.dispose(); + try { + await liveKitService.disconnect(); + } catch (err) { + console.error('[CallStore] Error disconnecting LiveKit:', err); + } if (callId && reason !== 'ended') { wsService.sendCallEnd(callId, reason); @@ -776,7 +645,7 @@ export const callStore = create((set, get) => ({ if (!currentCall) return; const newMuted = !currentCall.isMuted; - webrtcManager.setMuted(newMuted); + liveKitService.setMuted(newMuted); if (currentCall.id) { wsService.sendCallMute(currentCall.id, newMuted); } @@ -812,13 +681,7 @@ export const callStore = create((set, get) => ({ if (!currentCall) return; try { - if (enabled) { - const newStream = await webrtcManager.enableVideo(); - set({ localStream: newStream }); - } else { - const newStream = await webrtcManager.disableVideo(); - set({ localStream: newStream }); - } + await liveKitService.setVideoEnabled(enabled); set((s) => ({ currentCall: s.currentCall @@ -830,36 +693,27 @@ export const callStore = create((set, get) => ({ } }, - // 重置所有状态,用于登出时清理 reset: () => { - // 清理所有定时器和资源 cleanupResources(); - // 清理 initCall 的订阅 if (initCallUnsub) { initCallUnsub(); initCallUnsub = null; } - // 清理 invited 订阅 if (unsubInvited) { unsubInvited(); unsubInvited = null; } - // 清理 WebRTC 资源 - webrtcManager.dispose(); + liveKitService.dispose(); - // 清理已处理的通话ID缓存 processedCallIds.clear(); - // 重置状态 set({ currentCall: null, incomingCall: null, callDuration: 0, - peerStream: null, - localStream: null, isMinimized: false, }); }, diff --git a/web-shims/react-native-webrtc/index.js b/web-shims/react-native-webrtc/index.js deleted file mode 100644 index 9b10db3..0000000 --- a/web-shims/react-native-webrtc/index.js +++ /dev/null @@ -1,94 +0,0 @@ -const React = require('react'); - -const RTCView = React.forwardRef((props, ref) => { - const { streamURL, mirror, objectFit, style, ...rest } = props; - const src = streamURL || (props.stream && props.stream.toURL ? props.stream.toURL() : ''); - - if (src) { - return React.createElement('video', { - ...rest, - ref, - src, - autoPlay: true, - playsInline: true, - muted: props.muted || false, - style: { - ...style, - transform: mirror ? 'scaleX(-1)' : undefined, - objectFit: objectFit || 'cover', - width: '100%', - height: '100%', - }, - }); - } - return React.createElement('div', { ...rest, ref, style }); -}); -RTCView.displayName = 'RTCView'; - -const ScreenCapturePickerView = React.forwardRef((props, ref) => { - return React.createElement('div', { ...props, ref, 'data-native-component': 'ScreenCapturePickerView' }); -}); -ScreenCapturePickerView.displayName = 'ScreenCapturePickerView'; - -const mediaDevices = typeof navigator !== 'undefined' && navigator.mediaDevices - ? navigator.mediaDevices - : { - getUserMedia: () => Promise.reject(new Error('mediaDevices not available')), - getDisplayMedia: () => Promise.reject(new Error('mediaDevices not available')), - enumerateDevices: () => Promise.resolve([]), - }; - -const RTCPeerConnection = typeof window !== 'undefined' ? window.RTCPeerConnection : function () {}; -const RTCSessionDescription = typeof window !== 'undefined' ? window.RTCSessionDescription : function () {}; -const RTCIceCandidate = typeof window !== 'undefined' ? window.RTCIceCandidate : function () {}; -const MediaStream = typeof window !== 'undefined' ? window.MediaStream : function () {}; -const MediaStreamTrack = typeof window !== 'undefined' ? window.MediaStreamTrack : function () {}; - -function registerGlobals() {} - -const permissions = { - request: () => Promise.resolve(true), - check: () => Promise.resolve(true), -}; - -const RTCAudioSession = { - setCategory: () => {}, - setActive: () => {}, - setMode: () => {}, -}; - -const RTCRtpReceiver = typeof window !== 'undefined' ? window.RTCRtpReceiver : function () {}; -const RTCRtpSender = typeof window !== 'undefined' ? window.RTCRtpSender : function () {}; -const RTCRtpTransceiver = typeof window !== 'undefined' ? window.RTCRtpTransceiver : function () {}; -const RTCErrorEvent = typeof window !== 'undefined' ? window.RTCErrorEvent : function () {}; -const MediaStreamTrackEvent = typeof window !== 'undefined' ? window.MediaStreamTrackEvent : function () {}; - -const RTCPIPView = React.forwardRef((props, ref) => { - return React.createElement('div', { ...props, ref, 'data-native-component': 'RTCPIPView' }); -}); -RTCPIPView.displayName = 'RTCPIPView'; - -function startIOSPIP() {} -function stopIOSPIP() {} - -module.exports = { - RTCView, - ScreenCapturePickerView, - RTCPeerConnection, - RTCSessionDescription, - RTCIceCandidate, - MediaStream, - MediaStreamTrack, - MediaStreamTrackEvent, - mediaDevices, - permissions, - registerGlobals, - RTCAudioSession, - RTCRtpReceiver, - RTCRtpSender, - RTCRtpTransceiver, - RTCErrorEvent, - RTCPIPView, - startIOSPIP, - stopIOSPIP, -};