diff --git a/.gitignore b/.gitignore index 8b879778..0c3f5617 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ node_modules /coverage/ /tsbuild /types +/.vscode +/packages/webrtc-sdk/docs/* +/packages/webrtc-sdk/dist +/packages/webrtc-sdk/docs diff --git a/package.json b/package.json index c61a92e5..986945f2 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "doc": "doc" }, "scripts": { - "compile": "npm run cleanup:tsbuild && npm run compile:js && npm run compile:ts && copy-files-from-to && npm run cleanup:tsbuild", + "compile": "npm run cleanup:tsbuild && npm run compile:js && npm run compile:ts && npm run compile:webrtc-sdk && copy-files-from-to && npm run cleanup:tsbuild", "compile:js": "rollup -c rollup.config.module.cjs && rollup -c rollup.config.browser.cjs ", "compile:ts": "tsc -p ./tsconfig.json && api-extractor run", + "compile:webrtc-sdk": "npm install --prefix packages/webrtc-sdk && npm run build --prefix packages/webrtc-sdk", "cleanup:tsbuild": "rimraf ./tsbuild", "test": "karma start karma.conf.cjs", "codecov": "codecov" @@ -28,6 +29,38 @@ { "from": "./dist/es/*", "to": "./src/main/webapp/js/" + }, + { + "from": "./packages/webrtc-sdk/dist/**/*", + "to": "./src/main/webapp/v2/js/" + }, + { + "from": "./packages/webrtc-sdk/examples/*", + "to": "./src/main/webapp/v2/" + }, + { + "from": "./src/main/webapp/js/external/**/*", + "to": "./src/main/webapp/v2/js/external/" + }, + { + "from": "./src/main/webapp/js/external/**/*", + "to": "./src/main/webapp/v2/js/external/" + }, + { + "from": "./src/main/webapp/js/fetch.stream.js", + "to": "./src/main/webapp/v2/js/" + }, + { + "from": "./src/main/webapp/js/utility.js", + "to": "./src/main/webapp/v2/js/" + }, + { + "from": "./src/main/webapp/js/external/**/*", + "to": "./src/main/webapp/v2/js/external/" + }, + { + "from": "./src/main/webapp/css/**/*", + "to": "./src/main/webapp/v2/css/" } ], "copyFilesSettings": { diff --git a/packages/webrtc-sdk/.prettierrc.json b/packages/webrtc-sdk/.prettierrc.json new file mode 100644 index 00000000..3ed9654e --- /dev/null +++ b/packages/webrtc-sdk/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/packages/webrtc-sdk/README.md b/packages/webrtc-sdk/README.md new file mode 100644 index 00000000..e01c09ee --- /dev/null +++ b/packages/webrtc-sdk/README.md @@ -0,0 +1,229 @@ +# WebRTC SDK v2 (TypeScript) + +Modern, strictly-typed client SDK for Ant Media Server. + +## Install + +This package is currently private for development. Build locally: + +```bash +npm install +npm run build +``` + +## Quick start + +```ts +import { WebRTCClient, getWebSocketURL } from 'webrtc-sdk'; + +// One-liner session helper (recommended for most apps) +const { client } = await WebRTCClient.createSession({ + websocketURL: getWebSocketURL('wss://example.com:5443/LiveApp/websocket'), + role: 'publisher', + streamId: 'stream1', + localVideo: document.getElementById('local') as HTMLVideoElement, + remoteVideo: document.getElementById('remote') as HTMLVideoElement, + mediaConstraints: { audio: true, video: true }, + autoPlay: true, // attempt autoplay on the remote element after join +}); + +client.on('play_started', ({ streamId }) => console.log('playing', streamId)); +``` + + + +### Common operations + +```ts +// Stop a session +await client.stop('stream1'); + +// Send text over data channel +await client.sendData('stream1', 'hello'); + +// One-off stats snapshot +const stats = await client.getStats('stream1'); +``` + +## Events + +### Events quick reference + +Common events emitted by the SDK (see TypeDoc for full list): + +- `initialized`: signaling is ready +- `publish_started` / `publish_finished` +- `play_started` / `play_finished` +- `newTrackAvailable` { stream, track, streamId } +- `ice_connection_state_changed` { state, streamId } +- `data_channel_opened` / `data_channel_closed` +- `data_received` { streamId, data: string | ArrayBuffer } +- `updated_stats` PeerStats +- `devices_updated` GroupedDevices +- `room_joined` / `room_left` +- `room_information`, `broadcast_object`, `subscriber_count`, `subscriber_list` +- `video_track_assignments` +- `reconnection_attempt_for_publisher` / `reconnection_attempt_for_player` +- `error` { error, message? } + +### Listening to events (v2) + +```ts +client.on('initialized', () => console.log('ready')); +client.on('publish_started', ({ streamId }) => console.log('publishing', streamId)); +client.on('play_started', ({ streamId }) => console.log('playing', streamId)); +client.on('data_received', ({ streamId, data }) => console.log('dc <-', streamId, data)); +client.on('ice_connection_state_changed', ({ state, streamId }) => console.log('ice', state, streamId)); +client.on('reconnected', ({ streamId }) => console.log('reconnected', streamId)); + +// AMS server notifications exposed as typed events and also under notification: +client.on('broadcast_object', (obj) => console.log('broadcast_object', obj)); +client.on('notification:subscriberCount', (payload) => console.log('subscriberCount', payload)); +``` + +## Documentation + +Generated API docs are available in the `docs/` folder. To regenerate: + +```bash +npm run docs +``` + +Open `docs/index.html` in a browser. + +### Architecture and usage guidance + +`WebRTCClient` is the primary API surface. It composes: + +- `WebSocketAdaptor`: handles signaling with Ant Media Server (WS commands, notifications). +- `MediaManager`: handles local media (getUserMedia, device switching, screen share). + +For most applications, call methods on `WebRTCClient` only. It exposes the common +operations you need: `join()`, `publish()`, `play()`, `stop()`, `listDevices()`, +`selectVideoInput()`, `selectAudioInput()`, `startScreenShare()`, `stopScreenShare()`, +`sendData()`, `enableStats()/disableStats()`, room/multitrack helpers, and emits typed events. + +Note: Methods internally wait for signaling readiness; you don't need to call `ready()` yourself. + +Only use `WebSocketAdaptor` or `MediaManager` directly if you have advanced +customization needs (e.g., custom signaling transport or bespoke media capture). +Otherwise, prefer the higher-level `WebRTCClient` methods. + +### Room / Multitrack quick start + +```ts +// Join a room +await client.joinRoom({ roomId: 'my-room', streamId: 'publisher1' }); + +// Selectively play only some subtracks of a main stream +await client.playSelective({ + streamId: 'mainStreamId', + enableTracks: ['camera_user1', 'screen_user2'], + disableTracksByDefault: true, +}); + +// Enable/disable a specific subtrack +client.enableTrack('mainStreamId', 'camera_user3', true); + +// Force quality (ABR) +client.forceStreamQuality('mainStreamId', 720); // or 'auto' +``` + +### Device management and screen share + +```ts +// Switch devices without renegotiation +await client.selectVideoInput('camera-device-id'); +await client.selectAudioInput('mic-device-id'); + +// Camera on/off keeps sender alive (black dummy track) +await client.turnOffLocalCamera(); +await client.turnOnLocalCamera(); + +// Screen share and overlay (PIP camera) +await client.startScreenShare(); +await client.stopScreenShare(); +await client.startScreenWithCameraOverlay(); +await client.stopScreenWithCameraOverlay(); +``` + +### Data channel + +```ts +// Text +await client.sendData('s1', 'hello'); +// Binary +await client.sendData('s1', new Uint8Array([1,2,3]).buffer); +// Optional sanitize received strings +client.setSanitizeDataChannelStrings(true); +client.on('data_received', ({ data }) => console.log('rx', data)); +``` + +## Feature examples + +### Reconnect configuration + +```ts +client.configureReconnect({ backoff: "exp", baseMs: 500, maxMs: 10000, jitter: 0.3 }); +client.on("reconnected", ({ streamId }) => console.log("reconnected", streamId)); +``` + +### Device hot-swap and track controls + +```ts +client.on("device_hotswapped", e => console.log("hotswapped", e)); + +// Pause/resume tracks without renegotiation +client.pauseTrack("audio"); +client.resumeTrack("audio"); +``` + +### Remote audio level (viewer) + +```ts +await client.enableRemoteAudioLevel("s1", level => console.log(level), 200); +client.disableRemoteAudioLevel("s1"); +``` + +### Data-only publish + +```ts +const { client } = await WebRTCClient.createSession({ + websocketURL: getWebSocketURL('wss://example.com:5443/LiveApp/websocket'), + role: 'publisher', + streamId: 'data-only', + onlyDataChannel: true, +}); +await client.sendJSON('data-only', { hello: 'world' }); +``` + +### Stats helpers + +```ts +// One-off snapshot and event +const stats = await client.getStats('s1'); +client.on('updated_stats', (ps) => console.log(ps)); + +// Poll every 2s +client.enableStats('s1', 2000); +``` + +## Troubleshooting + +- Autoplay blocked: browsers may block autoplay with sound. Use `autoPlay: true` and call `videoEl.play()` on a user gesture if needed. +- HTTPS required: `getUserMedia` and `getDisplayMedia` need HTTPS (or localhost). Serve examples over HTTPS. +- TURN/ICE: if connections fail across networks, configure proper TURN servers on Ant Media Server and pass `peerConfig.iceServers` if needed. + +## Examples + +- `examples/publish.html` +- `examples/play.html` +- `examples/room.html` (rooms/multitrack, enable/disable subtracks, force quality, selective play) + +## Development + +- Lint: `npm run lint` (ESLint) +- Format: Prettier (integrated; run `npm run lint:fix` for quick fixes) +- Tests: `npm test` +- Docs: `npm run docs` + diff --git a/packages/webrtc-sdk/documents/Cookbook.md b/packages/webrtc-sdk/documents/Cookbook.md new file mode 100644 index 00000000..cb2e06f0 --- /dev/null +++ b/packages/webrtc-sdk/documents/Cookbook.md @@ -0,0 +1,83 @@ +## WebRTC SDK v2 Cookbook + +### 1) Publish/Play with one-liner +```ts +const { client } = await WebRTCClient.createSession({ websocketURL, role: 'publisher', streamId: 's1', localVideo, remoteVideo, mediaConstraints: { audio: true, video: true }, autoPlay: true }); +``` + +### 1b) Event listeners +```ts +client.on('publish_started', ({ streamId }) => console.log('publishing', streamId)); +client.on('data_received', ({ data }) => console.log('dc <-', data)); + +``` + +### 2) Device switching +```ts +await client.selectVideoInput(cameraId); +await client.selectAudioInput(micId); +``` + +### 3) Screen share and PIP overlay +```ts +await client.startScreenShare(); +// later +await client.stopScreenShare(); + +await client.startScreenWithCameraOverlay(); +await client.stopScreenWithCameraOverlay(); +``` + +### 4) Data channel helpers +```ts +await client.sendData('s1', 'hello'); +await client.sendJSON('s1', { type: 'chat', text: 'hi' }); +``` + +### 5) Reconnect policy and events +```ts +client.configureReconnect({ backoff: 'exp', baseMs: 500, maxMs: 10000, jitter: 0.3 }); +client.on('reconnected', ({ streamId }) => console.log('reconnected', streamId)); +``` + +### 6) Room / multitrack helpers +```ts +await client.joinRoom({ roomId: 'room1', streamId: 'pub1' }); +await client.playSelective({ streamId: 'main', enableTracks: ['camera_u1'], disableTracksByDefault: true }); +client.enableTrack('main', 'camera_u1', true); +client.forceStreamQuality('main', 720); // or 'auto' +``` + +### 7) Bandwidth/quality +```ts +await client.changeBandwidth('s1', 600); +await client.changeBandwidth('s1', 'unlimited'); +await client.setDegradationPreference('s1', 'maintain-framerate'); +``` + +### 8) Stats +```ts +client.on('updated_stats', ps => console.log(ps)); +client.enableStats('s1', 2000); +``` + +### 9) Audio output (sinkId) and meters +```ts +await client.setAudioOutput(deviceId, remoteVideo); +await client.enableRemoteAudioLevel('s1', level => console.log(level)); +``` + +### 10) Track controls +```ts +client.pauseTrack('audio'); +client.resumeTrack('audio'); +``` + +### 11) Data-only publish +```ts +const { client } = await WebRTCClient.createSession({ websocketURL, role: 'publisher', streamId: 'data', onlyDataChannel: true }); +await client.sendJSON('data', { ping: true }); +``` + + + diff --git a/packages/webrtc-sdk/documents/EventMatrix.md b/packages/webrtc-sdk/documents/EventMatrix.md new file mode 100644 index 00000000..586a5069 --- /dev/null +++ b/packages/webrtc-sdk/documents/EventMatrix.md @@ -0,0 +1,37 @@ +## Event Matrix + +### Core SDK events +- initialized: void +- closed: unknown +- server_will_stop: unknown +- publish_started: { streamId } +- publish_finished: { streamId } +- play_started: { streamId } +- play_finished: { streamId } +- ice_connection_state_changed: { state, streamId } +- reconnected: { streamId } +- updated_stats: PeerStats +- data_channel_opened: { streamId } +- data_channel_closed: { streamId } +- data_received: { streamId, data } +- newTrackAvailable: { stream, track, streamId } +- devices_updated: GroupedDevices +- device_hotswapped: { kind: "audioinput"|"videoinput", deviceId? } +- local_track_paused: { kind: "audio"|"video" } +- local_track_resumed: { kind: "audio"|"video" } +- error: { error, message? } + +### Common AMS notifications (forwarded) +- room_information → room_information +- broadcastObject → broadcast_object +- subscriberCount → subscriber_count +- subscriberList → subscriber_list +- videoTrackAssignmentList → video_track_assignments +- streamInformation → stream_information +- trackList → track_list +- subtrackList → subtrack_list +- subtrackCount → subtrack_count + +All notifications are also emitted under `notification:`. + + diff --git a/packages/webrtc-sdk/documents/ManualTestPlan.md b/packages/webrtc-sdk/documents/ManualTestPlan.md new file mode 100644 index 00000000..db6227c4 --- /dev/null +++ b/packages/webrtc-sdk/documents/ManualTestPlan.md @@ -0,0 +1,63 @@ +## Manual Test Plan (WebRTC SDK v2) + +### 1. Publish/Play basics +- Start publisher with camera+mic using `createSession`. Expect initialized → publish_started → ICE connected. +- Start viewer and call `play`/`join`. Expect `play_started`, remote audio/video. +- Stop from publisher and viewer; expect `publish_finished`/`play_finished`. + +### 2. Device switching +- `selectVideoInput()` while publishing. Remote video switches without renegotiation. +- `selectAudioInput()` while publishing. Remote audio continues. +- `turnOffLocalCamera()` then `turnOnLocalCamera()`; remote shows black → restores. +- `muteLocalMic()` then `unmuteLocalMic()`; mutes local audio → restores. + +### 3. Screen share +- `startScreenShare()`; verify screen video and mixed system+mic audio at remote. +- Call `enableSecondStreamInMixedAudio(false)` via console on `media` to disable mic (if using mix); re-enable to restore. +- `stopScreenShare()` restores camera. + +### 4. Screen share with camera overlay +- `startScreenWithCameraOverlay()`; verify PIP camera. Stop; camera restores. + +### 5. Data channel +- After publish, `sendData('s1','hello')` and 128KB binary; viewer receives correct data. +- Toggle `setSanitizeDataChannelStrings(true)`; sending `x` arrives escaped. +- `sendJSON('s1',{ type:'chat', text:'hi' })` arrives as string payload on viewer. + +### 6. Stats +- `getStats('s1')` returns values; `enableStats('s1',2000)` emits updates; totals increase during activity. + +### 7. Reconnect +- Disable and re-enable network. Expect `reconnection_attempt_for_*`, then `reconnected` when session resumes. +- Test `configureReconnect({ backoff:'exp', baseMs:500, maxMs:10000, jitter:0.3 })` and observe delays. + +### 8. Rooms / Multitrack +- `joinRoom({ roomId, streamId })`; server responds with `room_information`. +- `playSelective({ streamId:'main', enableTracks:['camera_u1'], disableTracksByDefault:true })`. +- `enableTrack('main','camera_u1',true)` toggles visibility. +- `forceStreamQuality('main', 720)` then `'auto'` and observe quality changes. +- `requestVideoTrackAssignments`, `updateVideoTrackAssignments`, `setMaxVideoTrackCount`; verify notifications/UI pagination. + +### 9. Bandwidth/quality controls +- `changeBandwidth('s1',600)` then `'unlimited'`; verify bitrate changes in stats. +- `setDegradationPreference('s1','maintain-framerate')`; observe under load. + +### 10. Audio utilities +- `setVolumeLevel(0)` → remote silence; restore >0. +- `enableAudioLevelForLocalStream(cb)`; levels update; disable stops. +- `enableRemoteAudioLevel('s1', cb)` on viewer; levels update; disable stops. + +### 11. Audio output selection +- `setAudioOutput(deviceId, remoteVideo)`; verify audio routes to selected output device (if supported). Expect `error:set_sink_id_unsupported` otherwise. + +### 12. Track controls +- `pauseTrack('audio')` then `resumeTrack('audio')`; viewer hears silence then audio resumes. Listen for `local_track_paused`/`local_track_resumed`. + +### 13. Data-only publish +- `createSession({ onlyDataChannel:true, role:'publisher', streamId:'data' })`. +- Verify no camera/mic prompt, DC opens, and `sendData`/`sendJSON` works. + +### 14. Close +- `close()`; peers close, websocket closes, `closed` event fires. + + diff --git a/packages/webrtc-sdk/documents/V2-vs-V1.md b/packages/webrtc-sdk/documents/V2-vs-V1.md new file mode 100644 index 00000000..5c426439 --- /dev/null +++ b/packages/webrtc-sdk/documents/V2-vs-V1.md @@ -0,0 +1,169 @@ +## WebRTC SDK v2 vs v1 + +### TL;DR +- v2 provides a modern, type-safe, promise-based API with clear, high-level methods and typed events. It reduces boilerplate and "footguns" while keeping parity with core AMS features. +- v1 is powerful but callback-heavy, loosely typed, and exposes a very broad low-level surface that is harder to use correctly. + +### Why v2 is better +- **TypeScript-first**: strict types, better IDE help, fewer runtime mistakes. +- **Promise-based flow**: `await ready()` / `await join()` vs ad-hoc callbacks. +- **Typed events**: ergonomic `on('event', payload => { ... })` rather than stringly-typed callback fanout. +- **Single primary API**: `WebRTCClient` composes media and signaling with safe defaults. +- **Safer media operations**: device switching via `replaceTrack`, black dummy track when camera is turned off (no renegotiation hiccups). +- **Data channel**: chunking with backpressure, optional input sanitization. +- **QoS controls**: bandwidth, degradation preferences, stats helpers. +- **Rooms/Multitrack helpers**: `playSelective`, `enableTrack`, track assignment APIs. +- **New v2 features**: plugin API, data-only publish, audio output selection (sinkId), remote audio level metering, stream metadata update. + +### Quick start (v2) +```ts +import { WebRTCClient } from 'webrtc-sdk'; + +const { client } = await WebRTCClient.createSession({ + websocketURL, + role: 'publisher', // or 'viewer' + streamId: 's1', + localVideo, + remoteVideo, + mediaConstraints: { audio: true, video: true }, + autoPlay: true, +}); +``` + +### Quick usage comparison + +#### Initialize and publish +```js +// v1 (JS) +const adaptor = new WebRTCAdaptor({ + websocketURL, + mediaConstraints: { audio: true, video: true }, + callback: (info, obj) => { /* handle events */ }, + callbackError: (err, msg) => { /* handle errors */ }, +}); +// wait for 'initialized' callback +adaptor.publish('s1', 'OPTIONAL_TOKEN'); +``` + +```ts +// v2 (TS) +import { WebRTCClient } from './src'; +const sdk = new WebRTCClient({ websocketURL, mediaConstraints: { audio: true, video: true }, localVideo }); +await sdk.join({ role: 'publisher', streamId: 's1', token: 'OPTIONAL_TOKEN' }); +``` + +#### Play +```js +// v1 +adaptor.play('s1'); +``` + +```ts +// v2 +const viewer = new WebRTCClient({ websocketURL, isPlayMode: true, remoteVideo }); +await viewer.join({ role: 'viewer', streamId: 's1' }); +``` + +#### Device selection +```js +// v1 +adaptor.switchVideoCameraCapture('s1', deviceId); +adaptor.switchAudioInputSource('s1', micId); +``` + +```ts +// v2 +await sdk.selectVideoInput(deviceId); +await sdk.selectAudioInput(micId); +``` + +#### Screen share (+ overlay) +```js +// v1 +adaptor.switchDesktopCapture('s1'); +``` + +```ts +// v2 +await sdk.startScreenShare(); +await sdk.startScreenWithCameraOverlay(); +``` + +#### Data channel +```js +// v1 +adaptor.sendData('s1', 'hello'); +``` + +```ts +// v2 +await sdk.sendData('s1', 'hello'); +await sdk.sendJSON('s1', { type: 'chat', text: 'hi' }); +``` + +#### Event handling +```js +// v1: single callback fanout +const adaptor = new WebRTCAdaptor({ + websocketURL, + callback: (info, obj) => { + if (info === 'publish_started') console.log('publishing', obj.streamId); + if (info === 'play_started') console.log('playing', obj.streamId); + if (info === 'roomInformation') console.log('room info', obj); + }, + callbackError: (err, message) => console.warn('error', err, message), +}); +``` + +```ts +// v2: typed, granular listeners + dynamic notification channel +client.on('publish_started', ({ streamId }) => console.log('publishing', streamId)); +client.on('play_started', ({ streamId }) => console.log('playing', streamId)); +client.on('error', ({ error, message }) => console.warn('error', error, message)); + +// AMS notifications are exposed as first-class events and under notification: +client.on('room_information', payload => console.log('room_information', payload)); +client.on('notification:subscriberCount', payload => console.log('subscriberCount', payload)); +``` + +### New v2 capabilities (not in v1 by default) +- **Plugin API**: `WebRTCClient.register(sdk => { /* augment */ })` to extend behavior cleanly. +- **Data-only publish**: `new WebRTCClient({ websocketURL, onlyDataChannel: true })` to publish with DC only (no A/V capture). +- **Audio output selection (sinkId)**: `await sdk.setAudioOutput(deviceId, mediaElement?)` for routing playback to chosen output. +- **Remote audio levels (viewer)**: `enableRemoteAudioLevel('s1', cb)` to meter incoming audio. +- **Stream metadata update**: `updateStreamMetaData('s1', obj)` to push metadata to AMS. +- **One-liner sessions**: `WebRTCClient.createSession({ role, streamId, autoPlay })` simplifies startup. +- **Convenience**: `sendJSON` for data-channel JSON messaging. +- **Reconnect backoff config**: `configureReconnect({ backoff, baseMs, maxMs, jitter })` with `reconnected` event. +- **Device hot-swap**: automatic default device re-acquisition on `devicechange`, emits `device_hotswapped`. +- **Track controls**: `pauseTrack('audio'|'video')` and `resumeTrack(...)`, emitting `local_track_paused/resumed`. +- **Tooling**: ESLint + Prettier baked in for consistent, automated code quality. + +### Rooms / multitrack +```js +// v1 selective play +adaptor.play({ streamId: 'main', enableTracks: ['camera_u1'], disableTracksByDefault: true }); +adaptor.enableTrack('main', 'camera_u1', true); +``` + +```ts +// v2 selective play +await viewer.playSelective({ streamId: 'main', enableTracks: ['camera_u1'], disableTracksByDefault: true }); +viewer.enableTrack('main', 'camera_u1', true); +``` + +### Migration tips +- Publishing: `adaptor.publish(...)` → `await sdk.publish(streamId)` or `await sdk.join({ role: 'publisher', ... })`. +- Play: `adaptor.play(...)` → `await sdk.play(streamId)` or `await sdk.join({ role: 'viewer', ... })`. +- Device switching: `switchVideoCameraCapture` → `selectVideoInput`; `switchAudioInputSource` → `selectAudioInput`. +- Track toggling: `toggleVideo/toggleAudio` → `enableTrack(main, trackId, enabled)`. +- ABR: `forceStreamQuality` is available in both. +- Stats: `getStats`/`enableStats` available in both (types differ in v2). +- Data-only: v1 `onlyDataChannel` → v2 `{ onlyDataChannel: true }` in constructor. +- Metadata: v2 `updateStreamMetaData(streamId, obj)`. +- Audio output: new in v2 `setAudioOutput(deviceId, element?)`. + +### Bottom line +v2 streamlines common workflows, improves safety and DX, and adds modern capabilities, while keeping AMS feature parity for publish/play/rooms. Prefer v2 for new work; adopt it incrementally by mapping v1 calls to v2’s higher-level methods. + + diff --git a/packages/webrtc-sdk/eslint.config.js b/packages/webrtc-sdk/eslint.config.js new file mode 100644 index 00000000..86cba8fb --- /dev/null +++ b/packages/webrtc-sdk/eslint.config.js @@ -0,0 +1,64 @@ +// ESLint flat config +// Requires ESLint v9+ +import js from '@eslint/js'; +import ts from 'typescript-eslint'; +import eslintPluginImport from 'eslint-plugin-import'; +import eslintPluginPromise from 'eslint-plugin-promise'; +import eslintPluginPrettier from 'eslint-plugin-prettier'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import eslintPluginUnusedImports from 'eslint-plugin-unused-imports'; + +export default [ + js.configs.recommended, + ...ts.configs.recommended, + eslintConfigPrettier, + { + name: 'webrtc-sdk/ts', + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + project: ['./tsconfig.json'], + }, + }, + plugins: { + import: eslintPluginImport, + promise: eslintPluginPromise, + prettier: eslintPluginPrettier, + 'unused-imports': eslintPluginUnusedImports, + }, + rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + 'import/no-unresolved': 'off', + 'promise/no-nesting': 'off', + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always' + } + ], + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_' + } + ] + }, + + linterOptions: { + reportUnusedDisableDirectives: true, + }, + }, + { + ignores: ['dist', 'node_modules', 'examples', 'test'] + } +]; \ No newline at end of file diff --git a/packages/webrtc-sdk/examples/conference-v2.html b/packages/webrtc-sdk/examples/conference-v2.html new file mode 100644 index 00000000..43f98cce --- /dev/null +++ b/packages/webrtc-sdk/examples/conference-v2.html @@ -0,0 +1,701 @@ + + + + + + Ant Media WebRTC Conference (SDK v2) + + + + + + + + +
+
+
+

WebRTC Multitrack Conference (SDK v2)

+ +
+
+ +
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+ + +
+ + + +
+ +
+
+
+ + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ + + + + diff --git a/packages/webrtc-sdk/examples/data-only-v2.html b/packages/webrtc-sdk/examples/data-only-v2.html new file mode 100644 index 00000000..31fcf41d --- /dev/null +++ b/packages/webrtc-sdk/examples/data-only-v2.html @@ -0,0 +1,290 @@ + + + + WebRTC Samples > Data Channel Only (TS v2) + + + + + + + +
+
+
+

+ WebRTC Samples > Data Channel Only (TS v2) +

+
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+
+ +
+
+
+ +
+
+ Status: Offline +
+
+ + +
+ + +
+
+ +

+
+      
+    
+ + + + + + + diff --git a/packages/webrtc-sdk/examples/play-v2.html b/packages/webrtc-sdk/examples/play-v2.html new file mode 100644 index 00000000..2a0dd1fe --- /dev/null +++ b/packages/webrtc-sdk/examples/play-v2.html @@ -0,0 +1,749 @@ + + + +WebRTC SDK Samples > Play + + + + + + + + + + +
+
+
+

+ WebRTC SDK Samples > Play +

+
+
+ + +
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ + +
+ + +
+ + + + +
+ +
+ + + + + + +
+
+ Audio Output + + +
+
+
+
+ Remote Audio Level + + + level: 0.00 +
+
+ + + + + diff --git a/packages/webrtc-sdk/examples/publish-v2.html b/packages/webrtc-sdk/examples/publish-v2.html new file mode 100644 index 00000000..aaedbc81 --- /dev/null +++ b/packages/webrtc-sdk/examples/publish-v2.html @@ -0,0 +1,652 @@ + + + + WebRTC Samples > Publish (TS v2) + + + + + + + +
+
+
+

WebRTC Samples > Publish (TS v2)

+
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ + +
+ + + +
+ + + +
+ + +
+ + + Video Source + + + Audio Source + + + + + Microphone Gain & Level +
+ + level: 0.00 +
+
+ + +
+ + + Media Controls +
+ + +
+
+ + +
+
+ + +
+ + + Screen Share +
+ + +
+
+ + +
+ + + +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+ Status: Offline +
+
+ + +
+ + +
+ +
+
+
+ +
+
+
+
+
+
+ +

+
+      
+    
+ + + + + + + diff --git a/packages/webrtc-sdk/package-lock.json b/packages/webrtc-sdk/package-lock.json new file mode 100644 index 00000000..da3376a0 --- /dev/null +++ b/packages/webrtc-sdk/package-lock.json @@ -0,0 +1,5565 @@ +{ + "name": "webrtc-sdk", + "version": "2.0.0-beta.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webrtc-sdk", + "version": "2.0.0-beta.1", + "dependencies": { + "prettier": "^3.6.2" + }, + "devDependencies": { + "@eslint/js": "^9.12.0", + "@types/node": "^22.5.4", + "eslint": "^9.33.0", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-promise": "^7.1.0", + "eslint-plugin-unused-imports": "^4.1.4", + "rimraf": "^5.0.5", + "typedoc": "^0.25.13", + "typescript": "^5.4.5", + "typescript-eslint": "^8.10.0", + "vitest": "^2.1.9" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.17.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz", + "integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.39.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.3.tgz", + "integrity": "sha512-+fksAx9eG3Ab6LDnLs3ZqZa8KVJ/jYnX+D4Qe1azX+LFGFAXqynCQLOdLpNYN/l9e7l6hMWwZbrnctqr6eSQSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "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-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "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.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "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.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "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.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "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/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "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-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "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/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-promise": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", + "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "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.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "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/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "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.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "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.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "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.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "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-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "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.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "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.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "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.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "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.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "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.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "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.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "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.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "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-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "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.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "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.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "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.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "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.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "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.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "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/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "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/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "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.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "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/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "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/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "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.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "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.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "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.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "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.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "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.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "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": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "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.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "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.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "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.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedoc": { + "version": "0.25.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", + "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.7" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz", + "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.39.1", + "@typescript-eslint/parser": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "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": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "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.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "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.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "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-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/webrtc-sdk/package.json b/packages/webrtc-sdk/package.json new file mode 100644 index 00000000..2f9e38fb --- /dev/null +++ b/packages/webrtc-sdk/package.json @@ -0,0 +1,37 @@ +{ + "name": "webrtc-sdk", + "version": "2.0.0-beta.1", + "private": true, + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "clean": "rimraf dist", + "typecheck": "tsc -p tsconfig.json --noEmit", + "docs": "typedoc --options typedoc.json", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@eslint/js": "^9.12.0", + "@types/node": "^22.5.4", + "eslint": "^9.33.0", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-promise": "^7.1.0", + "eslint-plugin-unused-imports": "^4.1.4", + "rimraf": "^5.0.5", + "typedoc": "^0.25.13", + "typescript": "^5.4.5", + "typescript-eslint": "^8.10.0", + "vitest": "^2.1.9" + }, + "dependencies": { + "prettier": "^3.6.2" + } +} diff --git a/packages/webrtc-sdk/src/client/base-client.ts b/packages/webrtc-sdk/src/client/base-client.ts new file mode 100644 index 00000000..e1591bf3 --- /dev/null +++ b/packages/webrtc-sdk/src/client/base-client.ts @@ -0,0 +1,1023 @@ +import { Logger } from "../utils/logger.js"; +import { Emitter } from "../core/emitter.js"; +import type { EventMap } from "../core/events.js"; +import { MediaManager } from "../core/media-manager.js"; +import type { BaseClientOptions, GroupedDevices } from "../core/types.js"; +import { WebSocketAdaptor } from "../core/websocket-adaptor.js"; +export interface PeerContext { + pc: RTCPeerConnection; + dc?: RTCDataChannel; + videoSender?: RTCRtpSender; + audioSender?: RTCRtpSender; + mode?: StreamMode; +} + +export type StreamMode = "publish" | "play"; + +export interface ActiveStreamInfo { + mode: StreamMode; + token?: string; + roomId?: string; + streamName?: string; + metaData?: unknown; + role?: string; + subscriberId?: string; + subscriberCode?: string; + userPublishId?: string; + enableTracks?: string[]; + disableTracksByDefault?: boolean; +} + +interface ChunkState { + expected: number; + received: number; + buffers: Uint8Array[]; +} + +interface RemoteAudioMeter { + analyser: AnalyserNode; + timer: ReturnType; + data: Uint8Array; + source: MediaStreamAudioSourceNode; +} + +interface ReconnectConfig { + backoff: "fixed" | "exp"; + baseMs: number; + maxMs: number; + jitter: number; +} + +/** + * BaseClient + * + * Low-level WebRTC signaling and media management foundation used by higher-level clients + * (e.g., {@link ConferenceClient}, {@link StreamingClient}). + * + * Responsibilities: + * - Manages WebSocket signaling to Ant Media Server + * - Creates/maintains per-stream RTCPeerConnections and DataChannels + * - Applies local media tracks and exposes helpers to control devices and screen share + * - Emits typed events described by {@link EventMap} + * - Provides reconnection with backoff and per-stream tracking + * + * Consumers typically use concrete subclasses rather than instantiating this class directly. + */ +export abstract class BaseClient extends Emitter { + static pluginInitMethods: Array<(sdk: BaseClient) => void> = []; + + static register(init: (sdk: BaseClient) => void): void { + BaseClient.pluginInitMethods.push(init); + } + + protected ws?: WebSocketAdaptor; + protected media: MediaManager; + protected get mediaManager(): MediaManager { + return this.media; + } + protected log: Logger; + protected isReady = false; + protected isPlayMode: boolean; + protected onlyDataChannel: boolean; + protected sanitizeDcStrings: boolean; + protected autoReconnect: boolean; + protected reconnectConfig: ReconnectConfig = { + backoff: "exp", + baseMs: 500, + maxMs: 8000, + jitter: 0.2, + }; + protected peers: Map = new Map(); + protected peerConfig: RTCConfiguration = { + iceServers: [{ urls: "stun:stun1.l.google.com:19302" }], + }; + protected remoteDescriptionSet: Map = new Map(); + protected candidateQueue: Map = new Map(); + protected remoteVideo: HTMLVideoElement | null; + protected candidateTypes: Array<"udp" | "tcp"> = ["udp", "tcp"]; + protected rxChunks: Map = new Map(); + protected activeStreams: Map = new Map(); + protected reconnectTimers: Map> = new Map(); + protected lastReconnectAt: Map = new Map(); + protected remoteStreams: Map = new Map(); + protected audioContext: AudioContext | null = null; + protected remoteMeters: Map = new Map(); + protected idMapping: Record> = Object.create(null); + + constructor(opts: BaseClientOptions) { + super(); + this.isPlayMode = !!opts.isPlayMode; + this.onlyDataChannel = !!opts.onlyDataChannel; + this.sanitizeDcStrings = !!opts.sanitizeDataChannelStrings; + this.autoReconnect = opts.autoReconnect ?? true; + this.remoteVideo = opts.remoteVideo ?? null; + this.log = new Logger(opts.debug ? "debug" : "info"); + + if (opts.reconnectConfig) { + this.reconnectConfig = { + backoff: opts.reconnectConfig.backoff ?? this.reconnectConfig.backoff, + baseMs: opts.reconnectConfig.baseMs ?? this.reconnectConfig.baseMs, + maxMs: opts.reconnectConfig.maxMs ?? this.reconnectConfig.maxMs, + jitter: opts.reconnectConfig.jitter ?? this.reconnectConfig.jitter, + }; + } + + this.media = + opts.mediaManager ?? + new MediaManager({ mediaConstraints: opts.mediaConstraints, localVideo: opts.localVideo }); + + this.media.on("devices_updated", g => this.emit("devices_updated", g)); + this.media.on("local_tracks_changed", () => { + void this.applyLocalTracks(); + }); + + if (!this.isPlayMode && !this.onlyDataChannel) { + this.media.initLocalStream().catch(() => { + this.emit("error", { error: "getUserMediaIsNotAllowed" }); + }); + } + + if (opts.websocketURL || opts.httpEndpointUrl) { + this.ws = new WebSocketAdaptor({ + websocketURL: opts.websocketURL, + httpEndpointUrl: opts.httpEndpointUrl, + webrtcadaptor: { + notifyEventListeners: (info: string, obj?: unknown) => + this.handleTransportEvent(info, obj), + }, + debug: opts.debug, + }); + } + } + + /** + * Resolves when underlying signaling is initialized and ready. + */ + async ready(): Promise { + if (this.isReady) return; + await new Promise(resolve => { + this.once("initialized", () => resolve()); + }); + } + + /** + * Stop an active stream (publish or play) and close its peer connection. + */ + stop(streamId: string): void { + const ctx = this.peers.get(streamId); + const active = this.activeStreams.get(streamId); + const mode = (ctx && ctx.mode) || (active && active.mode); + if (ctx) { + try { + ctx.pc.close(); + } catch (e) { + this.log.warn("pc.close failed", e); + } + this.peers.delete(streamId); + } + this.clearActiveStream(streamId); + + if (this.ws) { + this.sendCommand({ command: "stop", streamId }); + } + if (mode === "publish") { + this.emit("publish_finished", { streamId }); + } else if (mode === "play") { + this.emit("play_finished", { streamId }); + } else { + // fallback: emit both to preserve backward behavior when mode is unknown + this.emit("publish_finished", { streamId }); + this.emit("play_finished", { streamId }); + } + } + + /** Configure reconnect backoff at runtime. */ + configureReconnect(cfg: Partial): void { + this.reconnectConfig = { ...this.reconnectConfig, ...cfg } as ReconnectConfig; + } + + /** Enumerate and group available media devices. */ + async listDevices(): Promise { + return this.media.listDevices(); + } + + async setAudioOutput(deviceId: string, element?: HTMLMediaElement | null): Promise { + await this.media.setAudioOutput(deviceId, element); + } + + async selectVideoInput(source: string | { facingMode: "user" | "environment" }): Promise { + await this.media.selectVideoInput(source); + await this.applyLocalTracks(); + } + + async selectAudioInput(deviceId: string): Promise { + await this.media.selectAudioInput(deviceId); + await this.applyLocalTracks(); + } + + pauseTrack(kind: "audio" | "video"): void { + this.media.pauseLocalTrack(kind); + } + + resumeTrack(kind: "audio" | "video"): void { + this.media.resumeLocalTrack(kind); + } + + async startScreenShare(): Promise { + await this.media.startScreenShare(); + await this.applyLocalTracks(); + } + + async stopScreenShare(): Promise { + await this.media.stopScreenShare(); + await this.applyLocalTracks(); + } + + async startScreenWithCameraOverlay(): Promise { + await this.media.startScreenWithCameraOverlay(); + await this.applyLocalTracks(); + } + + async stopScreenWithCameraOverlay(): Promise { + await this.media.stopScreenWithCameraOverlay(); + await this.applyLocalTracks(); + } + + async turnOffLocalCamera(): Promise { + this.media.turnOffLocalCamera(); + for (const ctx of this.peers.values()) { + const primary = ctx.videoSender; + const sender = primary ?? ctx.pc.getSenders().find(s => s.track?.kind === "video"); + if (!sender) continue; + try { + const stream = this.media.getLocalStream(); + const blackTrack = stream?.getVideoTracks()[0] ?? null; + await sender.replaceTrack(blackTrack); + } catch (e) { + this.log.warn("replaceTrack(black) failed", e); + } + } + } + + async turnOnLocalCamera(): Promise { + await this.media.turnOnLocalCamera(); + await this.applyLocalTracks(); + } + + muteLocalMic(): void { + this.media.muteLocalMic(); + } + + unmuteLocalMic(): void { + this.media.unmuteLocalMic(); + } + + setVolumeLevel(level: number): void { + this.media.setVolumeLevel(level); + } + + async enableAudioLevelForLocalStream( + callback: (level: number) => void, + periodMs = 200 + ): Promise { + await this.media.enableAudioLevelForLocalStream(callback, periodMs); + } + + disableAudioLevelForLocalStream(): void { + this.media.disableAudioLevelForLocalStream(); + } + + async enableAudioLevelWhenMuted( + callback: (speaking: boolean) => void, + threshold = 0.1 + ): Promise { + await this.media.enableAudioLevelWhenMuted(callback, threshold); + } + + disableAudioLevelWhenMuted(): void { + this.media.disableAudioLevelWhenMuted(); + } + + async getStats(streamId: string): Promise { + const ctx = this.peers.get(streamId); + if (!ctx) return false; + try { + const stats = await ctx.pc.getStats(); + const ps = new (await import("../core/peer-stats.js")).PeerStats(streamId); + let bytesSent = 0; + let bytesRecv = 0; + let now = 0; + stats.forEach(r => { + if (r.type === "outbound-rtp") { + bytesSent += (r as RTCOutboundRtpStreamStats).bytesSent || 0; + if ((r as RTCOutboundRtpStreamStats).packetsSent) { + if ((r as RTCOutboundRtpStreamStats).kind === "audio") + ps.audioPacketsSent = (r as RTCOutboundRtpStreamStats).packetsSent; + if ((r as RTCOutboundRtpStreamStats).kind === "video") { + ps.videoPacketsSent = (r as RTCOutboundRtpStreamStats).packetsSent; + ps.frameWidth = (r as RTCOutboundRtpStreamStats).frameWidth ?? ps.frameWidth; + ps.frameHeight = (r as RTCOutboundRtpStreamStats).frameHeight ?? ps.frameHeight; + if ((r as RTCOutboundRtpStreamStats).framesEncoded != null) + ps.framesEncoded = (r as RTCOutboundRtpStreamStats).framesEncoded; + } + } + now = (r as RTCOutboundRtpStreamStats).timestamp || now; + } else if (r.type === "inbound-rtp") { + bytesRecv += (r as RTCInboundRtpStreamStats).bytesReceived || 0; + if ((r as RTCInboundRtpStreamStats).packetsReceived) { + if ((r as RTCInboundRtpStreamStats).kind === "audio") + ps.audioPacketsReceived = (r as RTCInboundRtpStreamStats).packetsReceived; + if ((r as RTCInboundRtpStreamStats).kind === "video") + ps.videoPacketsReceived = (r as RTCInboundRtpStreamStats).packetsReceived; + } + now = (r as RTCInboundRtpStreamStats).timestamp || now; + } else if (r.type === "remote-inbound-rtp") { + if ((r as RTCInboundRtpStreamStats).kind === "audio") { + if ((r as RTCInboundRtpStreamStats).packetsLost != null) + ps.audioPacketsLost = (r as RTCInboundRtpStreamStats).packetsLost; + if ((r as any).roundTripTime != null) ps.audioRoundTripTime = (r as any).roundTripTime; + if ((r as RTCInboundRtpStreamStats).jitter != null) + ps.audioJitter = (r as RTCInboundRtpStreamStats).jitter; + } else if ((r as RTCRtpStreamStats).kind === "video") { + if ((r as any).packetsLost != null) ps.videoPacketsLost = (r as any).packetsLost; + if ((r as any).roundTripTime != null) ps.videoRoundTripTime = (r as any).roundTripTime; + if ((r as any).jitter != null) ps.videoJitter = (r as any).jitter; + } + } else if (r.type === "track") { + if ((r as RTCRtpStreamStats).kind === "video") { + if ((r as any).frameWidth != null) ps.frameWidth = (r as any).frameWidth; + if ((r as any).frameHeight != null) ps.frameHeight = (r as any).frameHeight; + if ((r as any).framesDecoded != null) ps.framesDecoded = (r as any).framesDecoded; + if ((r as any).framesDropped != null) ps.framesDropped = (r as any).framesDropped; + if ((r as any).framesReceived != null) ps.framesReceived = (r as any).framesReceived; + } + } else if (r.type === "candidate-pair" && (r as any).state === "succeeded") { + if ((r as RTCIceCandidatePairStats).availableOutgoingBitrate != null) { + ps.availableOutgoingBitrateKbps = + ((r as RTCIceCandidatePairStats).availableOutgoingBitrate as number) / 1000; + } + if ((r as RTCIceCandidatePairStats).currentRoundTripTime != null) { + ps.currentRoundTripTime = (r as RTCIceCandidatePairStats) + .currentRoundTripTime as number; + } + } + }); + ps.totalBytesSent = bytesSent; + ps.totalBytesReceived = bytesRecv; + ps.currentTimestamp = now; + this.emit("updated_stats", ps); + return ps; + } catch { + return false; + } + } + + enableStats(streamId: string, periodMs = 5000): void { + const key = `__stats_${streamId}`; + if ((this as unknown as Record)[key]) return; + (this as unknown as Record)[key] = setInterval(() => { + void this.getStats(streamId); + }, periodMs); + } + + disableStats(streamId: string): void { + const key = `__stats_${streamId}`; + const timer = (this as unknown as Record)[key] as + | ReturnType + | undefined; + if (timer) { + clearInterval(timer); + delete (this as unknown as Record)[key]; + } + } + + async sendData(streamId: string, data: string | ArrayBuffer): Promise { + const ctx = this.peers.get(streamId); + if (!ctx || !ctx.dc) { + this.log.warn("sendData: data channel not available for %s", streamId); + throw new Error("data_channel_not_available"); + } + const dc = ctx.dc; + if (typeof data === "string") { + dc.send(data); + return; + } + const CHUNK_SIZE = 16000; + const binary = data as ArrayBuffer; + const length = binary.byteLength; + const token = Math.floor(Math.random() * 999999) | 0; + const header = new Int32Array(2); + header[0] = token; + header[1] = length; + dc.send(header); + + let sent = 0; + dc.bufferedAmountLowThreshold = 1 << 20; + while (sent < length) { + const size = Math.min(length - sent, CHUNK_SIZE); + const buffer = new Uint8Array(size + 4); + const tokenArray = new Int32Array(1); + tokenArray[0] = token; + buffer.set(new Uint8Array(tokenArray.buffer, 0, 4), 0); + const chunk = new Uint8Array(binary, sent, size); + buffer.set(chunk, 4); + if (dc.bufferedAmount > dc.bufferedAmountLowThreshold) { + await new Promise(resolve => { + const onlow = () => { + (dc as RTCDataChannel).removeEventListener("bufferedamountlow", onlow); + resolve(); + }; + (dc as RTCDataChannel).addEventListener("bufferedamountlow", onlow, { once: true }); + }); + } + dc.send(buffer); + sent += size; + } + } + + async sendJSON(streamId: string, obj: unknown): Promise { + const text = JSON.stringify(obj); + await this.sendData(streamId, text); + } + + close(): void { + for (const streamId of Array.from(this.peers.keys())) { + this.stop(streamId); + } + try { + this.ws?.close(); + } catch (e) { + this.log.warn("ws close failed", e); + } + this.emit("closed", undefined as never); + } + + setSanitizeDataChannelStrings(enabled: boolean): void { + this.sanitizeDcStrings = !!enabled; + } + + async enableRemoteAudioLevel( + streamId: string, + callback: (level: number) => void, + periodMs = 200 + ): Promise { + const stream = + this.remoteStreams.get(streamId) ?? + (this.remoteVideo?.srcObject as MediaStream | null) ?? + null; + if (!stream) return; + if (!this.audioContext) this.audioContext = new AudioContext(); + const ctx = this.audioContext; + const source = ctx.createMediaStreamSource(stream); + const analyser = ctx.createAnalyser(); + analyser.fftSize = 256; + source.connect(analyser); + const data = new Uint8Array(analyser.frequencyBinCount); + if (this.remoteMeters.has(streamId)) this.disableRemoteAudioLevel(streamId); + const timer = setInterval(() => { + analyser.getByteTimeDomainData(data); + let sum = 0; + for (let i = 0; i < data.length; i++) { + const v = (data[i] - 128) / 128; + sum += v * v; + } + const rms = Math.sqrt(sum / data.length); + try { + callback(rms); + } catch (e) { + this.log.warn("remote audio level callback failed", e); + } + }, periodMs); + this.remoteMeters.set(streamId, { analyser, timer, data, source }); + } + + disableRemoteAudioLevel(streamId: string): void { + const meter = this.remoteMeters.get(streamId); + if (!meter) return; + clearInterval(meter.timer); + try { + meter.source.disconnect(); + } catch (e) { + this.log.warn("remote audio source disconnect failed", e); + } + try { + meter.analyser.disconnect(); + } catch (e) { + this.log.warn("remote audio analyser disconnect failed", e); + } + this.remoteMeters.delete(streamId); + } + + /** + * Called to get the signalling state for a stream. + * This information can be used for error handling. + * Check: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState + * @param {string} streamId : unique id for the stream + */ + signallingState(streamId: string): RTCSignalingState | null { + return this.peers.get(streamId)?.pc.signalingState ?? null; + } + + protected abstract restartStream(streamId: string, info: ActiveStreamInfo): void; + + protected trackActiveStream(streamId: string, info: ActiveStreamInfo): void { + this.activeStreams.set(streamId, info); + } + + protected clearActiveStream(streamId: string): void { + this.activeStreams.delete(streamId); + } + + protected getActiveStream(streamId: string): ActiveStreamInfo | undefined { + return this.activeStreams.get(streamId); + } + + protected sendCommand(payload: Record): void { + if (!this.ws) return; + this.ws.send(JSON.stringify(payload)); + } + + protected onInitialized(): void { + // subclasses can extend; default no-op + } + + protected onTransportEvent(_info: string, _obj?: unknown): void { + // subclasses override as needed + } + + protected onStartCommand(streamId: string): void { + void this.startPublishing(streamId); + } + + protected onRemoteOfferAnswered(streamId: string): void { + this.emit("play_started", { streamId }); + } + + protected onRemoteAnswerApplied(_streamId: string): void { + // subclasses may override + } + + protected onNotification(_payload: Record): void { + // subclasses may override + } + + protected createPeer(streamId: string, mode: StreamMode = "publish"): RTCPeerConnection { + const existingCtx = this.peers.get(streamId); + if (existingCtx && existingCtx.pc) { + existingCtx.mode = mode; + this.peers.set(streamId, existingCtx); + return existingCtx.pc; + } + const pc = new RTCPeerConnection(this.peerConfig); + pc.onicecandidate = ev => { + if (ev.candidate && this.ws) { + const cand = ev.candidate.candidate ?? ""; + const protocolSupported = this.candidateTypes.some(p => cand.toLowerCase().includes(p)); + if (!protocolSupported && cand !== "") { + this.log.debug("Skipping candidate due to protocol filter: %s", cand); + return; + } + const msg = { + command: "takeCandidate", + streamId, + label: ev.candidate.sdpMLineIndex ?? 0, + id: ev.candidate.sdpMid, + candidate: ev.candidate.candidate, + }; + this.sendCommand(msg); + } + }; + pc.oniceconnectionstatechange = () => { + this.log.info("ice state %s %s", streamId, pc.iceConnectionState); + this.emit("ice_connection_state_changed", { state: pc.iceConnectionState, streamId }); + if (!this.autoReconnect) return; + if (!this.activeStreams.has(streamId)) return; + const state = pc.iceConnectionState; + if (state === "failed" || state === "closed") { + this.reconnectIfRequired(streamId, 0, false); + } else if (state === "disconnected") { + this.reconnectIfRequired(streamId, 3000, false); + } + }; + + pc.ontrack = (event: RTCTrackEvent) => { + this.log.debug("ontrack %s", streamId); + const stream = event.streams[0]; + if (this.remoteVideo && this.remoteVideo.srcObject !== stream) { + this.remoteVideo.srcObject = stream; + } + if (stream) this.remoteStreams.set(streamId, stream); + + const mid = event.transceiver && event.transceiver.mid; + const mapping = this.idMapping[streamId] || {}; + const mappedId = typeof mid === "string" ? mapping[mid] : undefined; + const trackId = mappedId || event.track.id; + const payload = { stream, track: event.track, streamId, trackId }; + this.emit("newTrackAvailable", payload as never); + this.emit("newStreamAvailable" as keyof EventMap, payload as never); + }; + + const existing = this.peers.get(streamId) ?? { pc }; + existing.pc = pc; + existing.mode = mode; + this.peers.set(streamId, existing as PeerContext); + return pc; + } + + protected setupDataChannel(streamId: string, dc: RTCDataChannel): void { + const ctx = this.peers.get(streamId); + if (ctx) ctx.dc = dc; + try { + (dc as any).binaryType = "arraybuffer"; + } catch (e) { + this.log.warn("setting binaryType failed", e); + } + dc.onerror = error => { + this.log.warn("data channel error", error); + if (dc.readyState !== "closed") { + this.emit("error", { error: "data_channel_error", message: error }); + } + }; + dc.onopen = () => { + this.log.debug("data channel opened %s", streamId); + this.emit("data_channel_opened", { streamId }); + }; + dc.onclose = () => { + this.log.debug("data channel closed %s", streamId); + this.emit("data_channel_closed", { streamId }); + }; + dc.onmessage = event => { + const raw = event.data; + const processBuffer = (u8: Uint8Array) => { + if (u8.byteLength === 8) { + const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength); + const token = view.getInt32(0, true); + const total = view.getInt32(4, true); + this.rxChunks.set(token, { expected: total, received: 0, buffers: [] }); + return; + } + if (u8.byteLength >= 4) { + const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength); + const token = view.getInt32(0, true); + const dataPart = u8.subarray(4); + const st = this.rxChunks.get(token); + if (!st) { + this.emit("data_received", { + streamId, + data: u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer, + }); + return; + } + st.buffers.push(dataPart); + st.received += dataPart.byteLength; + if (st.received >= st.expected) { + const full = new Uint8Array(st.expected); + let offset = 0; + for (const b of st.buffers) { + full.set(b, offset); + offset += b.byteLength; + } + this.rxChunks.delete(token); + this.emit("data_received", { streamId, data: full.buffer }); + } + return; + } + this.emit("data_received", { + streamId, + data: u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer, + }); + }; + + if (typeof raw === "string") { + const text = this.sanitizeDcStrings ? raw.replace(//g, ">") : raw; + this.emit("data_received", { streamId, data: text }); + return; + } + if (typeof Blob !== "undefined" && raw instanceof Blob) { + raw + .arrayBuffer() + .then(ab => processBuffer(new Uint8Array(ab))) + .catch(() => { + this.emit("error", { error: "data_channel_blob_parse_failed", message: raw }); + }); + return; + } + if (raw instanceof ArrayBuffer) { + processBuffer(new Uint8Array(raw)); + return; + } + if (ArrayBuffer.isView(raw)) { + const view = raw as ArrayBufferView; + processBuffer(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)); + return; + } + this.emit("data_received", { streamId, data: raw }); + }; + } + + protected async startPublishing(streamId: string): Promise { + const pc = this.peers.get(streamId)?.pc ?? this.createPeer(streamId); + const stream = this.media.getLocalStream(); + if (!stream && !this.onlyDataChannel) throw new Error("no_local_stream"); + + if (!this.onlyDataChannel && pc.getSenders().length === 0 && stream) { + for (const track of stream.getTracks()) { + const sender = pc.addTrack(track, stream); + if (track.kind === "video") this.peers.get(streamId)!.videoSender = sender; + if (track.kind === "audio") this.peers.get(streamId)!.audioSender = sender; + } + } else { + const ctx = this.peers.get(streamId); + if (ctx) { + const senders = pc.getSenders(); + ctx.videoSender = ctx.videoSender || senders.find(s => s.track?.kind === "video"); + ctx.audioSender = ctx.audioSender || senders.find(s => s.track?.kind === "audio"); + } + } + + try { + const dc = pc.createDataChannel + ? pc.createDataChannel(streamId, { ordered: true }) + : undefined; + if (dc) this.setupDataChannel(streamId, dc); + } catch (e) { + this.log.warn("createDataChannel not supported", e); + } + const offer = await pc.createOffer({ offerToReceiveAudio: false, offerToReceiveVideo: false }); + await pc.setLocalDescription(offer); + this.sendTakeConfiguration(streamId, offer.type, offer.sdp ?? ""); + } + + protected sendTakeConfiguration(streamId: string, type: RTCSdpType, sdp: string): void { + const msg = { command: "takeConfiguration", streamId, type, sdp }; + this.sendCommand(msg); + if (type === "offer") { + this.emit("publish_started", { streamId }); + } + } + + protected async applyLocalTracks(): Promise { + const stream = this.media.getLocalStream(); + if (!stream) return; + for (const ctx of this.peers.values()) { + const senders = ctx.pc.getSenders(); + const videoTracks = stream.getVideoTracks(); + for (const track of videoTracks) { + let sender = + (track.kind === "video" ? ctx.videoSender : ctx.audioSender) || + senders.find(s => s.track && s.track.kind === track.kind); + if (sender && sender.replaceTrack) { + try { + await sender.replaceTrack(track); + if (track.kind === "video") ctx.videoSender = sender; + if (track.kind === "audio") ctx.audioSender = sender; + } catch (e) { + this.log.warn("replaceTrack failed", e); + } + } else { + try { + sender = ctx.pc.addTrack(track, stream); + if (track.kind === "video") ctx.videoSender = sender; + if (track.kind === "audio") ctx.audioSender = sender; + } catch (e) { + this.log.warn("addTrack failed", e); + } + } + } + const audioTracks = stream.getAudioTracks(); + for (const track of audioTracks) { + let sender = ctx.audioSender || senders.find(s => s.track && s.track.kind === "audio"); + if (sender && sender.replaceTrack) { + try { + await sender.replaceTrack(track); + ctx.audioSender = sender; + } catch (e) { + this.log.warn("replaceTrack failed", e); + } + } else { + try { + sender = ctx.pc.addTrack(track, stream); + ctx.audioSender = sender; + } catch (e) { + this.log.warn("addTrack failed", e); + } + } + } + } + } + + protected takeConfiguration(obj: Record): void { + const payload = obj as { + streamId: string; + sdp: string; + type: RTCSdpType; + idMapping?: Record; + streamTrackIds?: Record; + }; + const { streamId, sdp, type } = payload; + + this.log.debug("takeConfiguration %s %s", streamId, type); + + const mapping = payload.idMapping || payload.streamTrackIds; + + if (mapping) { + this.idMapping[streamId] = mapping; + } + + if (type === "answer") { + const ctx = this.peers.get(streamId); + if (ctx) { + ctx.pc.setRemoteDescription(new RTCSessionDescription({ type, sdp })).then(() => { + this.remoteDescriptionSet.set(streamId, true); + const queued = this.candidateQueue.get(streamId) ?? []; + queued.forEach(c => ctx.pc.addIceCandidate(new RTCIceCandidate(c))); + this.candidateQueue.set(streamId, []); + this.onRemoteAnswerApplied(streamId); + }); + } + } else if (type === "offer") { + const pc = this.createPeer(streamId, "play"); + // Set up data channel for play mode like the original WebRTCAdaptor + pc.ondatachannel = ev => { + this.setupDataChannel(streamId, ev.channel); + }; + pc.setRemoteDescription(new RTCSessionDescription({ type, sdp })) + .then(async () => { + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + this.sendTakeConfiguration(streamId, answer.type, answer.sdp ?? ""); + this.remoteDescriptionSet.set(streamId, true); + + const queued = this.candidateQueue.get(streamId) ?? []; + queued.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c))); + this.candidateQueue.set(streamId, []); + this.onRemoteOfferAnswered(streamId); + }) + .catch(e => this.log.warn("setRemoteDescription failed", e)); + } + } + + protected takeCandidate(obj: Record): void { + const { streamId, label, id, candidate } = obj as { + streamId: string; + label: number | null; + id?: string; + candidate: string; + }; + this.log.debug("takeCandidate %s", streamId); + + const ice: RTCIceCandidateInit = { sdpMLineIndex: label ?? undefined, sdpMid: id, candidate }; + const ctx = this.peers.get(streamId); + + if (ctx) { + if (this.remoteDescriptionSet.get(streamId)) { + ctx.pc + .addIceCandidate(new RTCIceCandidate(ice)) + .catch(e => this.log.warn("addIceCandidate failed", e)); + } else { + const q = this.candidateQueue.get(streamId) ?? []; + q.push(ice); + this.candidateQueue.set(streamId, q); + } + } + } + + private handleTransportEvent(info: string, obj?: unknown): void { + if (info === "initialized") { + this.isReady = true; + this.log.info("adaptor initialized"); + + for (const init of BaseClient.pluginInitMethods) { + try { + init(this); + } catch (e) { + this.log.warn("plugin init failed", e); + } + } + + this.onInitialized(); + return; + } else if (info === "start") { + const { streamId } = obj as { streamId: string }; + this.log.debug("start received for %s", streamId); + this.onStartCommand(streamId); + } else if (info === "takeConfiguration") { + this.takeConfiguration(obj as Record); + } else if (info === "takeCandidate") { + this.takeCandidate(obj as Record); + } else if (info === "iceServerConfig") { + const cfg = obj as { + stunServerUri?: string; + turnServerUsername?: string; + turnServerCredential?: string; + }; + if (cfg.stunServerUri) { + if (cfg.stunServerUri.startsWith("turn:")) { + this.peerConfig.iceServers = [ + { urls: "stun:stun1.l.google.com:19302" }, + { + urls: cfg.stunServerUri, + username: cfg.turnServerUsername ?? "", + credential: cfg.turnServerCredential ?? "", + }, + ]; + } else if (cfg.stunServerUri.startsWith("stun:")) { + this.peerConfig.iceServers = [{ urls: cfg.stunServerUri }]; + } + this.log.info("updated ice servers"); + } + } else if (info === "stop") { + const { streamId } = obj as { streamId: string }; + this.log.info("stop received for %s", streamId); + this.stop(streamId); + } else if (info === "notification") { + const payload = obj as Record; + const def = (payload.definition as string) || ""; + const streamId = (payload.streamId as string) || ""; + + if (def === "publish_started") this.emit("publish_started", { streamId }); + if (def === "publish_finished") this.emit("publish_finished", { streamId }); + if (def === "play_started") this.emit("play_started", { streamId }); + if (def === "play_finished") this.emit("play_finished", { streamId }); + if (def === "subscriberCount") this.emit("subscriber_count" as keyof EventMap, obj as never); + if (def === "subscriberList") this.emit("subscriber_list" as keyof EventMap, obj as never); + if (def === "roomInformation") this.emit("room_information" as keyof EventMap, obj as never); + if (def === "broadcastObject") this.emit("broadcast_object" as keyof EventMap, obj as never); + if (def === "videoTrackAssignmentList") + this.emit("video_track_assignments" as keyof EventMap, obj as never); + if (def === "streamInformation") + this.emit("stream_information" as keyof EventMap, obj as never); + if (def === "trackList") this.emit("track_list" as keyof EventMap, obj as never); + if (def === "subtrackList") this.emit("subtrack_list" as keyof EventMap, obj as never); + if (def === "subtrackCount") this.emit("subtrack_count" as keyof EventMap, obj as never); + if (def === "joinedTheRoom") this.emit("room_joined" as keyof EventMap, obj as never); + if (def === "leavedTheRoom") this.emit("room_left" as keyof EventMap, obj as never); + if (def) this.emit(`notification:${def}` as keyof EventMap, obj as never); + + this.onNotification(payload); + } else if (info === "closed") { + this.emit("closed", obj as never); + return; + } else if (info === "server_will_stop") { + this.emit("server_will_stop", obj as never); + return; + } + + this.onTransportEvent(info, obj); + this.emit(info as keyof EventMap, obj as never); + } + + private reconnectIfRequired(streamId: string, delayMs = 3000, forceReconnect = false): void { + if (!this.autoReconnect) return; + if (!this.activeStreams.has(streamId)) return; + if (delayMs <= 0) delayMs = this.reconnectConfig.baseMs; + if (this.reconnectTimers.has(streamId)) return; + const now = Date.now(); + const last = this.lastReconnectAt.get(streamId) ?? 0; + if (!forceReconnect && now - last < 1000) { + delayMs = Math.max(delayMs, 1000); + } + const mode = this.activeStreams.get(streamId)?.mode; + if (mode === "publish") { + this.emit("reconnection_attempt_for_publisher" as keyof EventMap, { streamId } as never); + } else if (mode === "play") { + this.emit("reconnection_attempt_for_player" as keyof EventMap, { streamId } as never); + } + const nextDelay = this.computeNextDelay(delayMs); + const timer = setTimeout(() => { + this.reconnectTimers.delete(streamId); + this.tryAgain(streamId, forceReconnect); + }, nextDelay); + this.reconnectTimers.set(streamId, timer); + } + + private computeNextDelay(lastDelay: number): number { + const { backoff, baseMs, maxMs, jitter } = this.reconnectConfig; + let next = + backoff === "exp" + ? Math.min(maxMs, Math.max(baseMs, lastDelay * 2)) + : Math.min(maxMs, baseMs); + if (jitter > 0) { + const rand = 1 + (Math.random() * 2 - 1) * jitter; + next = Math.max(0, Math.floor(next * rand)); + } + return next; + } + + private tryAgain(streamId: string, forceReconnect: boolean): void { + const active = this.activeStreams.get(streamId); + if (!active) return; + this.lastReconnectAt.set(streamId, Date.now()); + if (forceReconnect) { + this.log.info("Force reconnect requested for %s", streamId); + } + try { + this.stop(streamId); + } catch (e) { + this.log.warn("stop during reconnect failed", e); + } + setTimeout(() => { + this.restartStream(streamId, active); + }, 500); + } +} diff --git a/packages/webrtc-sdk/src/client/conference-client.ts b/packages/webrtc-sdk/src/client/conference-client.ts new file mode 100644 index 00000000..d1d1dd31 --- /dev/null +++ b/packages/webrtc-sdk/src/client/conference-client.ts @@ -0,0 +1,480 @@ +import type { EventMap } from "../core/events.js"; +import type { + ConferenceClientOptions, + ConferencePlayOptions, + ConferencePublishOptions, + JoinOptions, + JoinResult, + PlaySelectiveOptions, + RoomJoinOptions, + UpdateVideoTrackAssignmentsOptions, +} from "../core/types.js"; + +import { BaseClient, type ActiveStreamInfo } from "./base-client.js"; + +/** + * ConferenceClient + * + * High-level SDK for Ant Media multitrack conferences. It publishes a participant stream as a + * subtrack of a room (main track) and plays the room with all (or selected) subtracks over a + * single RTCPeerConnection. It also exposes helpers for track assignment/pagination and room + * information queries. + * + * Typical usage: + * 1. Optionally publish your local stream as a subtrack of a room using {@link publish}. + * 2. Play the room using {@link play} to receive other participants' subtracks. + * 3. Listen to `newTrackAvailable` events to attach incoming tracks to media elements. + */ +export class ConferenceClient extends BaseClient { + private currentRoom?: string; + private currentPublishId?: string; + + static register(initMethod: (sdk: ConferenceClient) => void): void { + BaseClient.register(initMethod as (sdk: BaseClient) => void); + } + + /** + * Create a ConferenceClient. + * @param opts Client options including signaling endpoints and media constraints. + */ + constructor(opts: ConferenceClientOptions) { + super(opts); + } + + protected override onInitialized(): void { + this.sendCommand({ command: "getIceServerConfig" }); + } + + /** + * Publish a participant stream as a subtrack of the given room. + * @param opts Options including `streamId` (your publish id) and `roomId` (main track id). + * `metaData` may include user state (e.g., camera/mic status) as JSON. + */ + async publish(opts: ConferencePublishOptions): Promise { + await this.ready(); + this.trackActiveStream(opts.streamId, { + mode: "publish", + token: opts.token, + roomId: opts.roomId, + streamName: opts.streamName, + metaData: opts.metaData, + role: opts.role, + subscriberId: opts.subscriberId, + subscriberCode: opts.subscriberCode, + }); + this.currentPublishId = opts.streamId; + this.currentRoom = opts.roomId; + const stream = this.media.getLocalStream(); + const hasVideo = this.onlyDataChannel ? false : !!stream && stream.getVideoTracks().length > 0; + const hasAudio = this.onlyDataChannel ? false : !!stream && stream.getAudioTracks().length > 0; + this.sendCommand({ + command: "publish", + streamId: opts.streamId, + token: opts.token ?? "", + mainTrack: opts.roomId, + streamName: opts.streamName ?? "", + metaData: opts.metaData ?? "", + role: opts.role ?? "", + subscriberId: opts.subscriberId ?? "", + subscriberCode: opts.subscriberCode ?? "", + video: hasVideo, + audio: hasAudio, + }); + } + + /** + * Play a room (main track). Server responds with an SDP offer containing the enabled subtracks. + * The client answers and emits `newTrackAvailable` for each incoming media track. + * @param opts Options including `streamId` (room id to use on the PC) and `roomId` (room). + */ + async play(opts: ConferencePlayOptions): Promise { + await this.ready(); + this.trackActiveStream(opts.streamId, { + mode: "play", + token: opts.token, + roomId: opts.roomId, + enableTracks: opts.enableTracks, + userPublishId: opts.userPublishId, + role: opts.role, + subscriberId: opts.subscriberId, + subscriberCode: opts.subscriberCode, + disableTracksByDefault: opts.disableTracksByDefault, + }); + if (opts.roomId) this.currentRoom = opts.roomId; + const pc = this.createPeer(opts.streamId, "play"); + pc.ondatachannel = ev => this.setupDataChannel(opts.streamId, ev.channel); + this.sendCommand({ + command: "play", + streamId: opts.streamId, + token: opts.token ?? "", + room: opts.roomId ?? "", + trackList: opts.enableTracks ?? [], + subscriberId: opts.subscriberId ?? "", + subscriberCode: opts.subscriberCode ?? "", + viewerInfo: opts.metaData ?? "", + role: opts.role ?? "", + subscriberName: opts.subscriberName ?? "", + userPublishId: opts.userPublishId ?? "", + disableTracksByDefault: opts.disableTracksByDefault ?? false, + }); + } + + /** + * Start a selective play session with optional `enableTracks` and `disableTracksByDefault`. + * This is useful for rendering a subset of participants. + */ + async playSelective(opts: PlaySelectiveOptions): Promise { + await this.ready(); + this.trackActiveStream(opts.streamId, { + mode: "play", + token: opts.token, + roomId: opts.roomId ?? opts.streamId, + }); + this.createPeer(opts.streamId, "play"); + this.sendCommand({ + command: "play", + streamId: opts.streamId, + token: opts.token ?? "", + room: opts.roomId ?? "", + trackList: opts.enableTracks ?? [], + subscriberId: opts.subscriberId ?? "", + subscriberCode: opts.subscriberCode ?? "", + viewerInfo: opts.metaData ?? "", + role: opts.role ?? "", + userPublishId: "", + disableTracksByDefault: opts.disableTracksByDefault ?? false, + }); + } + + /** + * Join helper that resolves when ICE is `connected/completed` or the first media track arrives. + * Publishes or plays depending on `options.role`. + */ + async join(options: JoinOptions): Promise { + await this.ready(); + const timeout = options.timeoutMs ?? 20000; + return await new Promise((resolve, reject) => { + const to = setTimeout(() => reject(new Error("join_timeout")), timeout); + const cleanup = () => { + clearTimeout(to); + this.off("ice_connection_state_changed", onIce); + this.off("play_started", onPlayStarted); + this.off("publish_started", onPublishStarted); + this.off("error", onError); + }; + const onIce = (payload: EventMap["ice_connection_state_changed"]) => { + if ( + payload.streamId === options.streamId && + (payload.state === "connected" || payload.state === "completed") + ) { + cleanup(); + resolve({ + streamId: options.streamId, + state: payload.state as "connected" | "completed", + }); + } + }; + const onPlayStarted = (payload: EventMap["play_started"]) => { + cleanup(); + resolve({ streamId: payload.streamId, state: "track_added" }); + }; + const onPublishStarted = (payload: EventMap["publish_started"]) => { + cleanup(); + resolve({ streamId: payload.streamId, state: "track_added" }); + }; + const onError = () => { + cleanup(); + reject(new Error("join_failed")); + }; + this.on("ice_connection_state_changed", onIce); + this.on("play_started", onPlayStarted); + this.on("publish_started", onPublishStarted); + this.on("error", onError); + if (options.role === "publisher") { + void this.publish({ + streamId: options.streamId, + roomId: options.roomId ?? "", + token: options.token, + }).catch(onError); + } else { + void this.play({ + streamId: options.streamId, + roomId: options.roomId, + token: options.token, + }).catch(onError); + } + }); + } + + /** + * Legacy room-join command. For multitrack conferencing, prefer {@link publish} + {@link play}. + */ + async joinRoom(opts: RoomJoinOptions): Promise { + await this.ready(); + const payload = { + command: "joinRoom", + room: opts.roomId, + mainTrack: opts.roomId, + streamId: opts.streamId ?? "", + mode: opts.mode ?? "multitrack", + streamName: opts.streamName ?? "", + role: opts.role ?? "", + metadata: opts.metaData ?? "", + } as Record; + this.sendCommand(payload); + } + + /** + * Leave the room (server side). This will also close all peer connections for this client. + */ + async leaveRoom(roomId: string, streamId?: string): Promise { + await this.ready(); + this.sendCommand({ + command: "leaveFromRoom", + room: roomId, + mainTrack: roomId, + streamId: streamId ?? "", + }); + } + + /** + * Enable/disable a specific subtrack under a room (server forwards media when enabled). + */ + async enableTrack(mainTrackId: string, trackId: string, enabled: boolean): Promise { + await this.ready(); + this.sendCommand({ command: "enableTrack", streamId: mainTrackId, trackId, enabled }); + } + + /** + * Request list of track ids under a main track. + */ + async getTracks(streamId: string, token = ""): Promise { + await this.ready(); + this.sendCommand({ command: "getTrackList", streamId, token }); + } + + /** + * Request paginated subtracks for a given main track, optionally filtered by `role`. + */ + async getSubtracks(streamId: string, role = "", offset = 0, size = 50): Promise { + await this.ready(); + this.sendCommand({ command: "getSubtracks", streamId, role, offset, size }); + } + + /** + * Request subtrack count for a main track, filterable by role and status. + */ + async getSubtrackCount(streamId: string, role = "", status = ""): Promise { + await this.ready(); + this.sendCommand({ command: "getSubtracksCount", streamId, role, status }); + } + + /** + * Request current video track assignment list (useful for pagination/slot assignments). + */ + async requestVideoTrackAssignments(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getVideoTrackAssignmentsCommand", streamId }); + } + + /** + * Update pagination window for video track assignments. + */ + async updateVideoTrackAssignments(opts: UpdateVideoTrackAssignmentsOptions): Promise { + await this.ready(); + this.sendCommand({ + command: "updateVideoTrackAssignmentsCommand", + streamId: opts.streamId, + offset: opts.offset, + size: opts.size, + }); + } + + /** + * Set the maximum number of video tracks the server should forward for the room. + */ + async setMaxVideoTrackCount(streamId: string, maxTrackCount: number): Promise { + await this.ready(); + this.sendCommand({ command: "setMaxVideoTrackCountCommand", streamId, maxTrackCount }); + } + + /** + * Force a specific ABR height for the current viewer session (or `auto`). + */ + async forceStreamQuality(streamId: string, height: number | "auto"): Promise { + await this.ready(); + this.sendCommand({ + command: "forceStreamQuality", + streamId, + streamHeight: height === "auto" ? "auto" : height, + }); + } + + /** + * Toggle a video track server-side. + */ + toggleVideo(streamId: string, trackId: string, enabled: boolean): void { + this.sendCommand({ command: "toggleVideo", streamId, trackId, enabled }); + } + + /** + * Toggle an audio track server-side. + */ + toggleAudio(streamId: string, trackId: string, enabled: boolean): void { + this.sendCommand({ command: "toggleAudio", streamId, trackId, enabled }); + } + + /** + * Request legacy room information (ids and names of streams in the room). + */ + async getRoomInfo(roomId: string, streamId = ""): Promise { + await this.ready(); + this.sendCommand({ command: "getRoomInfo", room: roomId, streamId }); + } + + /** + * Request stream information for a specific stream. + */ + async getStreamInfo(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getStreamInfo", streamId }); + } + + /** + * Request broadcast object for a main track or subtrack (includes metadata and relations). + */ + async getBroadcastObject(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getBroadcastObject", streamId }); + } + + /** + * Request subscriber count for a stream. + */ + async getSubscriberCount(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getSubscriberCount", streamId }); + } + + /** + * Request paginated subscriber list for a stream. + */ + async getSubscriberList(streamId: string, offset = 0, size = 50): Promise { + await this.ready(); + this.sendCommand({ command: "getSubscribers", streamId, offset, size }); + } + + /** + * Send a peer message to another participant in a peer-to-peer session. + */ + peerMessage(streamId: string, definition: string, data: unknown): void { + this.sendCommand({ command: "peerMessageCommand", streamId, definition, data }); + } + + registerPushNotificationToken( + subscriberId: string, + authToken: string, + pushToken: string, + tokenType: "fcm" | "apn" + ): void { + this.sendCommand({ + command: "registerPushNotificationToken", + subscriberId, + token: authToken, + pnsRegistrationToken: pushToken, + pnsType: tokenType, + }); + } + + sendPushNotification( + subscriberId: string, + authToken: string, + pushNotificationContent: Record, + subscriberIdsToNotify: string[] + ): void { + if (typeof pushNotificationContent !== "object") { + throw new Error("pushNotificationContent must be an object"); + } + if (!Array.isArray(subscriberIdsToNotify)) { + throw new Error("subscriberIdsToNotify must be an array"); + } + this.sendCommand({ + command: "sendPushNotification", + subscriberId, + token: authToken, + pushNotificationContent, + subscriberIdsToNotify, + }); + } + + sendPushNotificationToTopic( + subscriberId: string, + authToken: string, + pushNotificationContent: Record, + topic: string + ): void { + if (typeof pushNotificationContent !== "object") { + throw new Error("pushNotificationContent must be an object"); + } + this.sendCommand({ + command: "sendPushNotification", + subscriberId, + token: authToken, + pushNotificationContent, + topic, + }); + } + + /** + * Update the metadata (free-form JSON/text) for a specific stream. + */ + updateStreamMetaData(streamId: string, metaData: unknown): void { + this.sendCommand({ command: "updateStreamMetaData", streamId, metaData }); + } + + protected override onTransportEvent(info: string, obj?: unknown): void { + if (info === "notification") { + const payload = obj as Record | undefined; + const def = (payload?.definition as string) || ""; + if (def === "roomInformation") this.emit("room_information", obj as never); + if (def === "joinedTheRoom") this.emit("room_joined", obj as never); + if (def === "leavedTheRoom") this.emit("room_left", obj as never); + if (def === "videoTrackAssignmentList") this.emit("video_track_assignments", obj as never); + if (def === "subscriberList") this.emit("subscriber_list", obj as never); + if (def === "subscriberCount") this.emit("subscriber_count", obj as never); + if (def === "trackList") this.emit("track_list", obj as never); + if (def === "subtrackList") this.emit("subtrack_list", obj as never); + if (def === "subtrackCount") this.emit("subtrack_count", obj as never); + } + super.onTransportEvent(info, obj); + } + + protected override restartStream(streamId: string, info: ActiveStreamInfo): void { + if (info.mode === "publish") { + this.log.info("Re-publish attempt for %s", streamId); + void this.publish({ + streamId, + roomId: info.roomId ?? this.currentRoom ?? "", + token: info.token, + streamName: info.streamName, + metaData: info.metaData, + role: info.role, + subscriberId: info.subscriberId, + subscriberCode: info.subscriberCode, + }).catch(e => this.log.warn("republish failed", e)); + } else { + this.log.info("Re-play attempt for %s", streamId); + void this.play({ + streamId, + roomId: info.roomId ?? this.currentRoom, + token: info.token, + enableTracks: info.enableTracks, + subscriberId: info.subscriberId, + subscriberCode: info.subscriberCode, + role: info.role, + disableTracksByDefault: info.disableTracksByDefault, + userPublishId: info.userPublishId, + }).catch(e => this.log.warn("replay failed", e)); + } + } +} diff --git a/packages/webrtc-sdk/src/client/streaming-client.ts b/packages/webrtc-sdk/src/client/streaming-client.ts new file mode 100644 index 00000000..7a98b31e --- /dev/null +++ b/packages/webrtc-sdk/src/client/streaming-client.ts @@ -0,0 +1,386 @@ +import { BaseClient, type ActiveStreamInfo } from "./base-client.js"; + +import type { EventMap } from "../core/events.js"; +import type { + JoinOptions, + JoinResult, + PlaySelectiveOptions, + StreamingClientOptions, +} from "../core/types.js"; + +/** + * StreamingClient + * + * High-level SDK for simple publish/play scenarios (single-track). It wraps the base signaling + * and media operations, providing convenience helpers like {@link createSession} and bandwidth + * controls without the multitrack room semantics of {@link ConferenceClient}. + */ +export class StreamingClient extends BaseClient { + /** + * Register a plugin initializer to be invoked once the client is initialized. + */ + static register(initMethod: (sdk: StreamingClient) => void): void { + BaseClient.register(initMethod as (sdk: BaseClient) => void); + } + + protected override onInitialized(): void { + this.sendCommand({ command: "getIceServerConfig" }); + } + + /** + * Create a client, wait for readiness, and immediately join as publisher/player. + * Optionally attempts to autoplay the provided `remoteVideo` element. + */ + static async createSession( + opts: StreamingClientOptions & + Pick & { + autoPlay?: boolean; + } + ): Promise<{ client: StreamingClient; result: JoinResult }> { + const client = new StreamingClient(opts); + await client.ready(); + const result = await client.join({ + role: opts.role, + streamId: opts.streamId, + token: opts.token, + timeoutMs: opts.timeoutMs, + }); + if (opts.autoPlay && opts.remoteVideo) { + try { + await opts.remoteVideo.play(); + } catch { + // ignore autoplay errors (gesture required) + } + } + return { client, result }; + } + + constructor(opts: StreamingClientOptions) { + super(opts); + } + + /** + * Publish a simple single-track stream. + */ + async publish(streamId: string, token?: string): Promise { + await this.ready(); + this.log.info("publish %s", streamId); + this.trackActiveStream(streamId, { mode: "publish", token }); + + const stream = this.media.getLocalStream(); + const hasVideo = this.onlyDataChannel ? false : !!stream && stream.getVideoTracks().length > 0; + const hasAudio = this.onlyDataChannel ? false : !!stream && stream.getAudioTracks().length > 0; + + this.sendCommand({ + command: "publish", + streamId, + token: token ?? "", + video: hasVideo, + audio: hasAudio, + }); + } + + /** + * Play a simple single-track stream. + */ + async play(streamId: string, token?: string): Promise { + await this.ready(); + this.log.info("play %s", streamId); + this.trackActiveStream(streamId, { mode: "play", token }); + + const pc = this.createPeer(streamId, "play"); + pc.ondatachannel = ev => this.setupDataChannel(streamId, ev.channel); + + this.sendCommand({ + command: "play", + streamId, + token: token ?? "", + room: "", + trackList: [], + subscriberId: "", + subscriberCode: "", + viewerInfo: "", + role: "", + userPublishId: "", + }); + } + + /** + * Play selectively with optional track filtering (roomId-enabled servers only). + */ + async playSelective(opts: PlaySelectiveOptions): Promise { + await this.ready(); + this.log.info("playSelective %s", opts.streamId); + this.trackActiveStream(opts.streamId, { mode: "play", token: opts.token }); + + const pc = this.createPeer(opts.streamId); + pc.ondatachannel = ev => this.setupDataChannel(opts.streamId, ev.channel); + + this.sendCommand({ + command: "play", + streamId: opts.streamId, + token: opts.token ?? "", + room: opts.roomId ?? "", + trackList: opts.enableTracks ?? [], + subscriberId: opts.subscriberId ?? "", + subscriberCode: opts.subscriberCode ?? "", + viewerInfo: opts.metaData ?? "", + role: opts.role ?? "", + userPublishId: "", + disableTracksByDefault: opts.disableTracksByDefault ?? false, + }); + } + + /** + * Stop the active stream and close its peer connection. + */ + override stop(streamId: string): void { + super.stop(streamId); + } + + /** + * Join helper that resolves on ICE connectivity or first track received. + */ + async join(options: JoinOptions): Promise { + await this.ready(); + const timeout = options.timeoutMs ?? 15000; + + return await new Promise((resolve, reject) => { + const to = setTimeout(() => reject(new Error("join_timeout")), timeout); + + const cleanup = () => { + clearTimeout(to); + this.off("ice_connection_state_changed", onIce); + this.off("play_started", onPlayStarted); + this.off("publish_started", onPublishStarted); + this.off("error", onErr); + }; + + const onIce = (payload: EventMap["ice_connection_state_changed"]) => { + if ( + payload.streamId === options.streamId && + (payload.state === "connected" || payload.state === "completed") + ) { + cleanup(); + resolve({ + streamId: options.streamId, + state: payload.state as "connected" | "completed", + }); + } + }; + const onPlayStarted = (payload: EventMap["play_started"]) => { + cleanup(); + resolve({ streamId: payload.streamId, state: "track_added" }); + }; + const onPublishStarted = (payload: EventMap["publish_started"]) => { + cleanup(); + resolve({ streamId: payload.streamId, state: "track_added" }); + }; + const onErr = (_payload: EventMap["error"]) => { + cleanup(); + reject(new Error("join_failed")); + }; + + this.on("ice_connection_state_changed", onIce); + this.on("play_started", onPlayStarted); + this.on("publish_started", onPublishStarted); + this.on("error", onErr); + + if (options.role === "publisher") { + void this.publish(options.streamId, options.token).catch(onErr); + } else { + void this.play(options.streamId, options.token).catch(onErr); + } + }); + } + + /** + * Force a specific ABR height (or `auto`) for the viewer. + */ + async forceStreamQuality(streamId: string, height: number | "auto"): Promise { + await this.ready(); + this.sendCommand({ + command: "forceStreamQuality", + streamId, + streamHeight: height === "auto" ? "auto" : height, + }); + } + + /** + * Change sender max bitrate (kbps). Use `"unlimited"` to remove cap. + */ + async changeBandwidth(streamId: string, bandwidth: number | "unlimited"): Promise { + const ctx = this.peers.get(streamId); + if (!ctx) return; + const sender = ctx.videoSender || ctx.pc.getSenders().find(s => s.track?.kind === "video"); + if (!sender) return; + const params = sender.getParameters(); + params.encodings = params.encodings || [{}]; + if (bandwidth === "unlimited") { + delete (params.encodings[0] as Record).maxBitrate; + } else { + (params.encodings[0] as Record).maxBitrate = bandwidth * 1000; + } + try { + await sender.setParameters(params); + } catch (e) { + this.log.warn("setParameters(maxBitrate) failed", e); + } + } + + /** + * Set WebRTC `degradationPreference` for the video sender. + */ + async setDegradationPreference( + streamId: string, + preference: "maintain-framerate" | "maintain-resolution" | "balanced" + ): Promise { + const ctx = this.peers.get(streamId); + if (!ctx) return; + const sender = ctx.videoSender || ctx.pc.getSenders().find(s => s.track?.kind === "video"); + if (!sender) return; + const params = sender.getParameters(); + try { + (params as unknown as { degradationPreference?: string }).degradationPreference = preference; + await sender.setParameters(params); + this.log.info("Degradation Preference set to %s", preference); + } catch (e) { + this.log.warn("setParameters(degradationPreference) failed", e); + } + } + + /** + * Toggle video server-side. + */ + toggleVideo(streamId: string, trackId: string, enabled: boolean): void { + this.sendCommand({ command: "toggleVideo", streamId, trackId, enabled }); + } + + /** + * Toggle audio server-side. + */ + toggleAudio(streamId: string, trackId: string, enabled: boolean): void { + this.sendCommand({ command: "toggleAudio", streamId, trackId, enabled }); + } + + /** + * Request stream info for a specific stream. + */ + async getStreamInfo(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getStreamInfo", streamId }); + } + + /** + * Request broadcast object for a stream. + */ + async getBroadcastObject(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getBroadcastObject", streamId }); + } + + /** + * Request subscriber count for a stream. + */ + async getSubscriberCount(streamId: string): Promise { + await this.ready(); + this.sendCommand({ command: "getSubscriberCount", streamId }); + } + + /** + * Request paginated subscriber list for a stream. + */ + async getSubscriberList(streamId: string, offset = 0, size = 50): Promise { + await this.ready(); + this.sendCommand({ command: "getSubscribers", streamId, offset, size }); + } + + /** + * Send a peer message in peer-to-peer mode. + */ + peerMessage(streamId: string, definition: string, data: unknown): void { + this.sendCommand({ command: "peerMessageCommand", streamId, definition, data }); + } + + /** + * Register a push notification token to Ant Media Server. + */ + registerPushNotificationToken( + subscriberId: string, + authToken: string, + pushToken: string, + tokenType: "fcm" | "apn" + ): void { + this.sendCommand({ + command: "registerPushNotificationToken", + subscriberId, + token: authToken, + pnsRegistrationToken: pushToken, + pnsType: tokenType, + }); + } + + /** + * Send push notification to users. + */ + sendPushNotification( + subscriberId: string, + authToken: string, + pushNotificationContent: Record, + subscriberIdsToNotify: string[] + ): void { + if (typeof pushNotificationContent !== "object") { + throw new Error("pushNotificationContent must be an object"); + } + if (!Array.isArray(subscriberIdsToNotify)) { + throw new Error("subscriberIdsToNotify must be an array"); + } + this.sendCommand({ + command: "sendPushNotification", + subscriberId, + token: authToken, + pushNotificationContent, + subscriberIdsToNotify, + }); + } + + /** + * Send push notification to a topic. + */ + sendPushNotificationToTopic( + subscriberId: string, + authToken: string, + pushNotificationContent: Record, + topic: string + ): void { + if (typeof pushNotificationContent !== "object") { + throw new Error("pushNotificationContent must be an object"); + } + this.sendCommand({ + command: "sendPushNotification", + subscriberId, + token: authToken, + pushNotificationContent, + topic, + }); + } + + /** + * Update metadata for a stream. + */ + updateStreamMetaData(streamId: string, metaData: unknown): void { + this.sendCommand({ command: "updateStreamMetaData", streamId, metaData }); + } + + /** @internal */ + protected override restartStream(streamId: string, info: ActiveStreamInfo): void { + if (info.mode === "publish") { + this.log.info("Re-publish attempt for %s", streamId); + void this.publish(streamId, info.token).catch(e => this.log.warn("republish failed", e)); + } else { + this.log.info("Re-play attempt for %s", streamId); + void this.play(streamId, info.token).catch(e => this.log.warn("replay failed", e)); + } + } +} + diff --git a/packages/webrtc-sdk/src/core/emitter.ts b/packages/webrtc-sdk/src/core/emitter.ts new file mode 100644 index 00000000..ded20b97 --- /dev/null +++ b/packages/webrtc-sdk/src/core/emitter.ts @@ -0,0 +1,44 @@ +export class Emitter> { + private handlers: Map void>> = + new Map(); + + on(event: K, handler: (payload: EventMap[K]) => void): void { + if (!this.handlers.has(event)) this.handlers.set(event, new Set()); + // TypeScript cannot narrow Set element type per key; wrap to preserve type safety + const wrapped = ((p: unknown) => handler(p as EventMap[K])) as ( + payload: EventMap[keyof EventMap] + ) => void; + (wrapped as unknown as { original?: typeof handler }).original = handler; + this.handlers.get(event)!.add(wrapped); + } + + off(event: K, handler: (payload: EventMap[K]) => void): void { + const set = this.handlers.get(event); + if (!set) return; + for (const fn of Array.from(set)) { + if ((fn as unknown as { original?: unknown }).original === handler) { + set.delete(fn); + } + } + } + + once(event: K, handler: (payload: EventMap[K]) => void): void { + const wrap = (payload: EventMap[K]) => { + this.off(event, wrap as unknown as (p: EventMap[K]) => void); + handler(payload); + }; + this.on(event, wrap); + } + + emit(event: K, payload: EventMap[K]): void { + const set = this.handlers.get(event); + if (!set) return; + for (const fn of Array.from(set)) { + try { + (fn as (p: EventMap[K]) => void)(payload); + } catch (e) { + console.warn(e); + } + } + } +} diff --git a/packages/webrtc-sdk/src/core/errors.ts b/packages/webrtc-sdk/src/core/errors.ts new file mode 100644 index 00000000..9165f1f8 --- /dev/null +++ b/packages/webrtc-sdk/src/core/errors.ts @@ -0,0 +1,30 @@ +/** + * Well-known error codes emitted by the SDK. + */ +export type ErrorCode = + | "WebSocketNotConnected" + | "WebSocketNotSupported" + | "UnsecureContext" + | "getUserMediaIsNotAllowed" + | "ScreenSharePermissionDenied" + | "notSetRemoteDescription" + | "protocol_not_supported" + | "data_channel_error" + | "data_channel_blob_parse_failed" + | "join_timeout" + | "join_failed"; + +/** + * Standardized error type produced by the SDK. Use {@link code} for programmatic handling. + */ +export class SDKError extends Error { + readonly code: ErrorCode; + readonly info?: unknown; + + constructor(code: ErrorCode, message?: string, info?: unknown) { + super(message ?? code); + this.name = "SDKError"; + this.code = code; + this.info = info; + } +} diff --git a/packages/webrtc-sdk/src/core/events.ts b/packages/webrtc-sdk/src/core/events.ts new file mode 100644 index 00000000..956dab5c --- /dev/null +++ b/packages/webrtc-sdk/src/core/events.ts @@ -0,0 +1,53 @@ +import type { GroupedDevices } from "./types"; +import type { PeerStats } from "./peer-stats"; + +/** + * Typed events emitted by {@link WebRTCClient} and helpers. + */ +export interface EventMap { + [key: string]: unknown; + initialized: void; + closed: unknown; + server_will_stop: unknown; + /** Emitted when new local tracks are attached or replaced; used internally to refresh senders */ + publish_started: { streamId: string }; + publish_finished: { streamId: string }; + play_started: { streamId: string }; + play_finished: { streamId: string }; + ice_connection_state_changed: { state: string; streamId: string }; + reconnected?: { streamId: string }; + updated_stats: PeerStats; + data_received: { streamId: string; data: string | ArrayBuffer }; + data_channel_opened: { streamId: string }; + data_channel_closed: { streamId: string }; + newTrackAvailable: { stream: MediaStream; track: MediaStreamTrack; streamId: string }; + devices_updated: GroupedDevices; + local_tracks_changed: void; + device_hotswapped?: { kind: "audioinput" | "videoinput"; deviceId?: string }; + local_track_paused?: { kind: "audio" | "video" }; + local_track_resumed?: { kind: "audio" | "video" }; + error: { error: string; message?: unknown }; + // dynamic notification channel e.g. notification:subscriberCount -> payload from server + [k: `notification:${string}`]: unknown; + // commonly used server notifications as first-class events + subscriber_count?: { streamId?: string; count?: number } | unknown; + subscriber_list?: unknown; + room_information?: unknown; + broadcast_object?: unknown; + room_joined?: unknown; + room_left?: unknown; + video_track_assignments?: unknown; + // additional common notifications + stream_information?: unknown; + track_list?: unknown; + subtrack_list?: unknown; + subtrack_count?: unknown; + reconnection_attempt_for_publisher?: string | { streamId: string }; + reconnection_attempt_for_player?: string | { streamId: string }; +} + +export interface TypedEmitter> { + on(event: K, handler: (payload: M[K]) => void): void; + off(event: K, handler: (payload: M[K]) => void): void; + once(event: K, handler: (payload: M[K]) => void): void; +} diff --git a/packages/webrtc-sdk/src/core/media-manager.ts b/packages/webrtc-sdk/src/core/media-manager.ts new file mode 100644 index 00000000..4714bb52 --- /dev/null +++ b/packages/webrtc-sdk/src/core/media-manager.ts @@ -0,0 +1,640 @@ +import type { GroupedDevices } from "./types.js"; +import { Emitter } from "./emitter.js"; +import type { EventMap } from "./events.js"; + +export interface MediaManagerOptions { + mediaConstraints?: MediaStreamConstraints; + localVideo?: HTMLVideoElement | null; + debug?: boolean; +} + +/** + * Manages local media acquisition and device switching. + */ +export class MediaManager extends Emitter { + private localStream: MediaStream | null = null; + private localVideo: HTMLVideoElement | null; + private constraints: MediaStreamConstraints; + private screenVideoTrack: MediaStreamTrack | null = null; + private screenShareAudioTrack: MediaStreamTrack | null = null; + private cameraOverlayTrack: MediaStreamTrack | null = null; + private overlayTimer: ReturnType | null = null; + private selectedVideoInputId: string | null = null; + private selectedAudioInputId: string | null = null; + private selectedAudioOutputId: string | null = null; + private defaultDeviceListenerInstalled = false; + private cameraDisabled = false; + // v1 parity: keep a dummy canvas based black frame stream when camera is off + private dummyCanvas: HTMLCanvasElement = document.createElement("canvas"); + private blackVideoTrack: MediaStreamTrack | null = null; + private blackFrameTimer: ReturnType | null = null; + private replacementStream: MediaStream | null = null; + + /** + * @param opts Media constraints and optional local preview element. + */ + constructor(opts: MediaManagerOptions) { + super(); + this.localVideo = opts.localVideo ?? null; + this.constraints = opts.mediaConstraints ?? { video: true, audio: true }; + } + + /** Return the currently active local stream, if any. */ + getLocalStream(): MediaStream | null { + return this.localStream; + } + + /** (Re)initialize local stream using current constraints and emit `devices_updated`. */ + async initLocalStream(): Promise { + // Stop and release any previously active tracks to avoid keeping devices (camera/mic) on + if (this.localStream) { + try { + for (const track of this.localStream.getTracks()) { + try { + track.stop(); + } catch { + // ignore + } + } + } catch { + // ignore + } + } + const requestConstraints: MediaStreamConstraints = this.cameraDisabled + ? { ...this.constraints, video: false } + : this.constraints; + const stream = await navigator.mediaDevices.getUserMedia(requestConstraints); + this.localStream = stream; + if (this.localVideo) this.localVideo.srcObject = stream; + await this.refreshDevices(); + + if (!this.defaultDeviceListenerInstalled) { + this.defaultDeviceListenerInstalled = true; + try { + navigator.mediaDevices.addEventListener("devicechange", () => { + // If default devices changed, re-open tracks with current constraints and replace + void this.handleDeviceHotSwap(); + }); + } catch { + // ignore if unsupported + } + } + } + + async refreshDevices(): Promise { + const devices = await navigator.mediaDevices.enumerateDevices(); + const grouped: GroupedDevices = { + videoInputs: devices + .filter(d => d.kind === "videoinput") + .map(d => ({ deviceId: d.deviceId, label: d.label })), + audioInputs: devices + .filter(d => d.kind === "audioinput") + .map(d => ({ deviceId: d.deviceId, label: d.label })), + audioOutputs: devices + .filter(d => d.kind === "audiooutput") + .map(d => ({ deviceId: d.deviceId, label: d.label })), + selectedVideoInputId: this.selectedVideoInputId || undefined, + selectedAudioInputId: this.selectedAudioInputId || undefined, + }; + // audio output selection (sinkId) is media element specific; we expose last selected if available + grouped.selectedAudioOutputId = this.selectedAudioOutputId || undefined; + this.emit("devices_updated", grouped); + return grouped; + } + + /** + * Re-acquire default input devices when `devicechange` fires and replace local tracks in-place. + * Emits `device_hotswapped` with the swapped kind. Does nothing if user explicitly selected devices. + */ + private async handleDeviceHotSwap(): Promise { + try { + const prevVideoId = this.selectedVideoInputId; + const prevAudioId = this.selectedAudioInputId; + // Re-enumerate devices + const devs = await navigator.mediaDevices.enumerateDevices(); + const defaultCam = devs.find(d => d.kind === "videoinput"); + const defaultMic = devs.find(d => d.kind === "audioinput"); + // If user has explicitly selected ids, keep them. Otherwise use defaults. + if (!prevVideoId && defaultCam) { + const cam = await navigator.mediaDevices.getUserMedia({ + video: { deviceId: { exact: defaultCam.deviceId } }, + audio: false, + }); + const v = cam.getVideoTracks()[0]; + if (v) this.replaceLocalVideoTrack(v); + this.emit("device_hotswapped", { + kind: "videoinput", + deviceId: defaultCam.deviceId, + } as never); + } + if (!prevAudioId && defaultMic) { + const mic = await navigator.mediaDevices.getUserMedia({ + audio: { deviceId: { exact: defaultMic.deviceId } }, + video: false, + }); + const a = mic.getAudioTracks()[0]; + if (a) this.replaceLocalAudioTrack(a); + this.emit("device_hotswapped", { + kind: "audioinput", + deviceId: defaultMic.deviceId, + } as never); + } + await this.refreshDevices(); + } catch (e) { + this.emit("error", { error: "device_hotswap_failed", message: e } as never); + } + } + + /** Enumerate and group available input/output devices. */ + async listDevices(): Promise { + return this.refreshDevices(); + } + + /** Pause local track of given kind (audio/video) without renegotiation. */ + pauseLocalTrack(kind: "audio" | "video"): void { + if (!this.localStream) return; + const tracks = + kind === "video" ? this.localStream.getVideoTracks() : this.localStream.getAudioTracks(); + for (const t of tracks) t.enabled = false; + this.emit("local_track_paused", { kind } as never); + } + + /** Resume local track of given kind (audio/video). */ + resumeLocalTrack(kind: "audio" | "video"): void { + if (!this.localStream) return; + const tracks = + kind === "video" ? this.localStream.getVideoTracks() : this.localStream.getAudioTracks(); + for (const t of tracks) t.enabled = true; + this.emit("local_track_resumed", { kind } as never); + } + + /** Update video constraints to use a specific deviceId or facingMode and refresh stream. */ + async selectVideoInput(source: string | { facingMode: "user" | "environment" }): Promise { + const video: MediaTrackConstraints = + typeof source === "string" + ? { deviceId: { exact: source } } + : { facingMode: source.facingMode }; + this.constraints = { ...this.constraints, video }; + this.selectedVideoInputId = typeof source === "string" ? source : null; + // Reacquire only video for performance and replace in-place (v1 parity) + const cam = await navigator.mediaDevices.getUserMedia({ video, audio: false }); + const vtrack = cam.getVideoTracks()[0]; + if (vtrack) this.replaceLocalVideoTrack(vtrack); + await this.refreshDevices(); + } + + /** Set audio output device (sinkId) for a given media element; stores selection for future emits. */ + async setAudioOutput(deviceId: string, element?: HTMLMediaElement | null): Promise { + const target: HTMLMediaElement | null = element ?? (this.localVideo as HTMLMediaElement | null); + this.selectedAudioOutputId = deviceId || null; + if (!target) return; + const anyEl = target as unknown as { setSinkId?: (id: string) => Promise }; + if (typeof anyEl.setSinkId === "function") { + try { + await anyEl.setSinkId(deviceId); + } catch (e) { + // surface via event channel + this.emit("error", { error: "set_sink_id_failed", message: e } as never); + } + } else { + this.emit("error", { error: "set_sink_id_unsupported", message: target } as never); + } + await this.refreshDevices(); + } + + /** Update audio constraints to use a specific deviceId and refresh stream. */ + async selectAudioInput(deviceId: string): Promise { + const audio: MediaTrackConstraints = { deviceId: { exact: deviceId } }; + this.constraints = { ...this.constraints, audio }; + this.selectedAudioInputId = deviceId; + // Reacquire only audio and replace in-place (v1 parity) + const onlyAudio = await navigator.mediaDevices.getUserMedia({ audio, video: false }); + const atrack = onlyAudio.getAudioTracks()[0]; + if (atrack) this.replaceLocalAudioTrack(atrack); + await this.refreshDevices(); + } + + /** + * Turn off the camera device: stop and remove local video track(s). + * This turns off the camera light without renegotiation. Remote side will see video muted. + */ + turnOffLocalCamera(): void { + this.cameraDisabled = true; + // Initialize black dummy frame and keep sending at intervals similar to v1 + this.getBlackVideoTrack(); + const vtrack = this.replacementStream?.getVideoTracks()[0] || this.blackVideoTrack; + if (vtrack) this.replaceLocalVideoTrack(vtrack); + if (this.localVideo && this.localVideo.srcObject !== this.localStream) { + this.localVideo.srcObject = this.localStream as MediaStream; + } + } + + /** + * Re-enable camera: if no local video track exists, reacquire one with current constraints and add it. + * Does not renegotiate; callers should use replaceTrack on senders (handled by adaptor.applyLocalTracks). + */ + async turnOnLocalCamera(): Promise { + this.cameraDisabled = false; + this.clearBlackVideoTrackTimer(); + this.stopBlackVideoTrack(); + const videoConstraints = + (this.constraints && (this.constraints as MediaStreamConstraints).video) ?? true; + const cam = await navigator.mediaDevices.getUserMedia({ + video: videoConstraints, + audio: false, + }); + const vtrack = cam.getVideoTracks()[0]; + if (vtrack) this.replaceLocalVideoTrack(vtrack); + } + + // v1 parity helpers for black dummy track + private getBlackVideoTrack(): MediaStreamTrack | null { + const ctx = this.dummyCanvas.getContext("2d"); + if (ctx) { + if (this.dummyCanvas.width !== 320) { + this.dummyCanvas.width = 320; + this.dummyCanvas.height = 240; + } + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, this.dummyCanvas.width, this.dummyCanvas.height); + } + // Always recapture to ensure a fresh live track + this.replacementStream = this.dummyCanvas.captureStream(1); + if (!this.blackFrameTimer) { + this.blackFrameTimer = setInterval(() => { + const c = this.dummyCanvas.getContext("2d"); + if (!c) return; + c.fillStyle = "black"; + c.fillRect(0, 0, this.dummyCanvas.width, this.dummyCanvas.height); + }, 3000); + } + this.blackVideoTrack = this.replacementStream.getVideoTracks()[0] ?? null; + return this.blackVideoTrack; + } + + private clearBlackVideoTrackTimer(): void { + if (this.blackFrameTimer) { + clearInterval(this.blackFrameTimer); + this.blackFrameTimer = null; + } + } + + private stopBlackVideoTrack(): void { + if (this.blackVideoTrack) { + try { + this.blackVideoTrack.stop(); + } catch { + // ignore + } + this.blackVideoTrack = null; + } + this.replacementStream = null; + } + + /** Disable sending from the current local audio track(s). */ + muteLocalMic(): void { + if (!this.localStream) return; + for (const track of this.localStream.getAudioTracks()) { + track.enabled = false; + } + } + + /** Enable sending from the current local audio track(s). */ + unmuteLocalMic(): void { + if (!this.localStream) return; + for (const track of this.localStream.getAudioTracks()) { + track.enabled = true; + } + } + + /** Replace local video track with a screen capture track. If system audio is available, mix with mic. */ + async startScreenShare(): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const screen = (await (navigator.mediaDevices as any).getDisplayMedia({ + video: true, + audio: true, + })) as MediaStream; + const vtrack = screen.getVideoTracks()[0]; + if (!vtrack) return; + this.screenVideoTrack = vtrack; + + // Mix system audio with mic if present + let mixed: MediaStream | null = null; + const hasSystemAudio = screen.getAudioTracks().length > 0; + if (hasSystemAudio) { + const mic = await navigator.mediaDevices.getUserMedia({ + audio: this.constraints.audio ?? true, + video: false, + }); + mixed = this.mixAudioStreams(screen, mic); + } + + this.replaceLocalVideoTrack(vtrack); + const audioTrack = mixed + ? mixed.getAudioTracks()[0] + : ( + await navigator.mediaDevices.getUserMedia({ + audio: this.constraints.audio ?? true, + video: false, + }) + ).getAudioTracks()[0]; + if (audioTrack) this.replaceLocalAudioTrack(audioTrack); + + vtrack.onended = () => { + void this.stopScreenShare(); + }; + } + + /** Restore camera video track by reinitializing getUserMedia with current constraints. */ + async stopScreenShare(): Promise { + if (this.screenVideoTrack) { + try { + this.screenVideoTrack.stop(); + } catch (err) { + console.error(err); + } + this.screenVideoTrack = null; + } + if (this.screenShareAudioTrack) { + try { + this.screenShareAudioTrack.stop(); + } catch { + // ignore + } + this.screenShareAudioTrack = null; + } + await this.initLocalStream(); + } + + /** Start screen+camera overlay mode using canvas composition (v1 parity). */ + async startScreenWithCameraOverlay(): Promise { + // get screen video + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const screen = (await (navigator.mediaDevices as any).getDisplayMedia({ + video: true, + audio: false, + })) as MediaStream; + const screenTrack = screen.getVideoTracks()[0]; + if (!screenTrack) return; + + // get camera video only + const cam = await navigator.mediaDevices.getUserMedia({ + video: this.constraints.video ?? true, + audio: false, + }); + const camTrack = cam.getVideoTracks()[0] ?? null; + this.cameraOverlayTrack = camTrack; + + // prepare elements and canvas + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const screenVideo = document.createElement("video"); + const camVideo = document.createElement("video"); + screenVideo.srcObject = new MediaStream([screenTrack]); + camVideo.srcObject = camTrack ? new MediaStream([camTrack]) : null; + screenVideo.muted = true; + camVideo.muted = true; + await Promise.all([screenVideo.play().catch(() => {}), camVideo.play().catch(() => {})]); + + const canvasStream = canvas.captureStream(15); + + // attach onended to auto-restore + screenTrack.onended = () => { + void this.stopScreenWithCameraOverlay(); + this.emit("notification:screen_share_stopped" as keyof EventMap, undefined as never); + }; + + // draw loop roughly 15fps + const draw = () => { + if (!ctx || screenVideo.videoWidth === 0 || screenVideo.videoHeight === 0) return; + canvas.width = screenVideo.videoWidth; + canvas.height = screenVideo.videoHeight; + ctx.drawImage(screenVideo, 0, 0, canvas.width, canvas.height); + if (!this.cameraDisabled && camVideo.videoWidth > 0 && camVideo.videoHeight > 0) { + const insetW = Math.floor(canvas.width * 0.15); + const insetH = Math.floor((camVideo.videoHeight / camVideo.videoWidth) * insetW); + const x = canvas.width - insetW - 15; + const y = 15; // top-right + ctx.drawImage(camVideo, x, y, insetW, insetH); + } + }; + this.overlayTimer = setInterval(draw, 66); + + // replace outgoing video with canvas stream + const vtrack = canvasStream.getVideoTracks()[0]; + this.screenVideoTrack = screenTrack; + if (vtrack) this.replaceLocalVideoTrack(vtrack); + } + + /** Stop screen+camera overlay and restore camera. */ + async stopScreenWithCameraOverlay(): Promise { + if (this.overlayTimer) { + clearInterval(this.overlayTimer); + this.overlayTimer = null; + } + if (this.screenVideoTrack) { + try { + this.screenVideoTrack.stop(); + } catch { + // ignore + } + this.screenVideoTrack = null; + } + if (this.cameraOverlayTrack) { + try { + this.cameraOverlayTrack.stop(); + } catch { + // ignore + } + this.cameraOverlayTrack = null; + } + await this.initLocalStream(); + } + + // ===== Audio utilities (parity) ===== + private audioContext: AudioContext | null = null; + private primaryGainNode: GainNode | null = null; + private secondaryGainNode: GainNode | null = null; + private localMeterProc?: (level: number) => void; + private localMeterTimer: ReturnType | null = null; + private mutedProbeStream: MediaStream | null = null; + private lastVolume = 1; + private gainInputStream: MediaStream | null = null; + + /** Set output volume (0..1) for local publishing stream. */ + setVolumeLevel(level: number): void { + this.lastVolume = Math.max(0, Math.min(1, level)); + if (!this.primaryGainNode) { + this.installGainNodeForLocalAudio(); + } + if (this.primaryGainNode) this.primaryGainNode.gain.value = this.lastVolume; + } + + /** Ensure local audio runs through a GainNode for volume control and replace the track. + * Important: Use a dedicated mic capture stream as source to avoid feedback loops with localStream. + */ + private installGainNodeForLocalAudio(): void { + if (!this.localStream) return; + // stop previous dedicated mic stream if exists + if (this.gainInputStream) { + for (const t of this.gainInputStream.getTracks()) t.stop(); + this.gainInputStream = null; + } + const audioTracks = this.localStream.getAudioTracks(); + if (audioTracks.length === 0) return; + // Acquire a fresh mic-only stream for processing + const setup = async () => { + const mic = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + this.gainInputStream = mic; + if (!this.audioContext) this.audioContext = new AudioContext(); + const ctx = this.audioContext; + const source = ctx.createMediaStreamSource(mic); + const destination = ctx.createMediaStreamDestination(); + this.primaryGainNode = ctx.createGain(); + this.primaryGainNode.gain.value = this.lastVolume; + source.connect(this.primaryGainNode).connect(destination); + const processedTrack = destination.stream.getAudioTracks()[0]; + if (processedTrack) this.replaceLocalAudioTrack(processedTrack); + }; + // Fire and forget; caller just needs the chain installed + void setup(); + } + + /** Mix system audio (screen) and mic into a single audio track. */ + private mixAudioStreams(screen: MediaStream, mic: MediaStream): MediaStream { + const composed = new MediaStream(); + // Keep screen video in composed stream + screen.getVideoTracks().forEach(t => composed.addTrack(t)); + if (!this.audioContext) this.audioContext = new AudioContext(); + const ctx = this.audioContext; + const destination = ctx.createMediaStreamDestination(); + // system audio (primary) + if (screen.getAudioTracks().length > 0) { + const sys = new MediaStream([screen.getAudioTracks()[0]]); + const s1 = ctx.createMediaStreamSource(sys); + this.primaryGainNode = this.primaryGainNode || ctx.createGain(); + this.primaryGainNode.gain.value = this.lastVolume; + s1.connect(this.primaryGainNode).connect(destination); + this.screenShareAudioTrack = screen.getAudioTracks()[0]; + } + // mic audio (secondary) + if (mic.getAudioTracks().length > 0) { + const m = new MediaStream([mic.getAudioTracks()[0]]); + const s2 = ctx.createMediaStreamSource(m); + this.secondaryGainNode = this.secondaryGainNode || ctx.createGain(); + this.secondaryGainNode.gain.value = 1; + s2.connect(this.secondaryGainNode).connect(destination); + } + destination.stream.getAudioTracks().forEach(t => composed.addTrack(t)); + return composed; + } + + /** Enable/disable secondary (mic) audio in mixed audio mode. */ + enableSecondStreamInMixedAudio(enable: boolean): void { + if (this.secondaryGainNode) this.secondaryGainNode.gain.value = enable ? 1 : 0; + } + + /** Enable simple audio level metering for the local stream. */ + async enableAudioLevelForLocalStream( + callback: (level: number) => void, + periodMs = 200 + ): Promise { + if (!this.localStream) return; + if (!this.audioContext) this.audioContext = new AudioContext(); + const ctx = this.audioContext; + const src = ctx.createMediaStreamSource(this.localStream); + const analyser = ctx.createAnalyser(); + analyser.fftSize = 256; + src.connect(analyser); + const data = new Uint8Array(analyser.frequencyBinCount); + this.localMeterProc = callback; + if (this.localMeterTimer) clearInterval(this.localMeterTimer); + this.localMeterTimer = setInterval(() => { + analyser.getByteTimeDomainData(data); + // rough RMS + let sum = 0; + for (let i = 0; i < data.length; i++) { + const v = (data[i] - 128) / 128; + sum += v * v; + } + const rms = Math.sqrt(sum / data.length); + callback(rms); + }, periodMs); + } + + disableAudioLevelForLocalStream(): void { + if (this.localMeterTimer) { + clearInterval(this.localMeterTimer); + this.localMeterTimer = null; + } + this.localMeterProc = undefined; + } + + /** Probe mic while muted to notify if speaking. */ + async enableAudioLevelWhenMuted( + callback: (speaking: boolean) => void, + threshold = 0.1 + ): Promise { + if (!this.audioContext) this.audioContext = new AudioContext(); + const probe = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + this.mutedProbeStream = probe; + await this.enableAudioLevelForLocalStream(level => { + callback(level > threshold); + }, 200); + } + + disableAudioLevelWhenMuted(): void { + if (this.mutedProbeStream) { + for (const t of this.mutedProbeStream.getTracks()) t.stop(); + this.mutedProbeStream = null; + } + this.disableAudioLevelForLocalStream(); + } + + private replaceLocalVideoTrack(newTrack: MediaStreamTrack): void { + if (!this.localStream) { + this.localStream = new MediaStream([newTrack]); + if (this.localVideo) this.localVideo.srcObject = this.localStream; + return; + } + // remove existing video tracks + for (const t of this.localStream.getVideoTracks()) { + this.localStream.removeTrack(t); + try { + t.stop(); + } catch (err) { + console.error(err); + } + } + this.localStream.addTrack(newTrack); + if (this.localVideo && this.localVideo.srcObject !== this.localStream) { + this.localVideo.srcObject = this.localStream; + } + } + + private replaceLocalAudioTrack(newTrack: MediaStreamTrack): void { + if (!this.localStream) { + this.localStream = new MediaStream([newTrack]); + if (this.localVideo) this.localVideo.srcObject = this.localStream; + this.emit("local_tracks_changed", undefined as never); + return; + } + const prev = this.localStream.getAudioTracks()[0]; + // Add new first to keep continuity, then remove/stop previous shortly after + this.localStream.addTrack(newTrack); + this.emit("local_tracks_changed", undefined as never); + if (prev) { + setTimeout(() => { + try { + this.localStream?.removeTrack(prev); + prev.stop(); + } catch (err) { + console.error(err); + } + }, 50); + } + if (this.localVideo && this.localVideo.srcObject !== this.localStream) { + this.localVideo.srcObject = this.localStream; + } + } +} diff --git a/packages/webrtc-sdk/src/core/peer-stats.ts b/packages/webrtc-sdk/src/core/peer-stats.ts new file mode 100644 index 00000000..0f698569 --- /dev/null +++ b/packages/webrtc-sdk/src/core/peer-stats.ts @@ -0,0 +1,33 @@ +export class PeerStats { + streamId: string; + averageOutgoingBitrate?: number; + currentOutgoingBitrate?: number; + averageIncomingBitrate?: number; + currentIncomingBitrate?: number; + totalBytesSent?: number; + totalBytesReceived?: number; + currentTimestamp?: number; + // Extended parity fields + audioPacketsLost?: number; + videoPacketsLost?: number; + audioPacketsSent?: number; + videoPacketsSent?: number; + audioPacketsReceived?: number; + videoPacketsReceived?: number; + audioRoundTripTime?: number; + videoRoundTripTime?: number; + audioJitter?: number; + videoJitter?: number; + frameWidth?: number; + frameHeight?: number; + framesEncoded?: number; + framesDecoded?: number; + framesDropped?: number; + framesReceived?: number; + availableOutgoingBitrateKbps?: number; + currentRoundTripTime?: number; + + constructor(streamId: string) { + this.streamId = streamId; + } +} diff --git a/packages/webrtc-sdk/src/core/types.ts b/packages/webrtc-sdk/src/core/types.ts new file mode 100644 index 00000000..a1b4815b --- /dev/null +++ b/packages/webrtc-sdk/src/core/types.ts @@ -0,0 +1,155 @@ +/** + * The role of the client in a session. + * - `publisher`: sends local media to Ant Media Server + * - `viewer`: receives remote media from Ant Media Server + */ +export type Role = "publisher" | "viewer"; + +/** + * Common configuration shared across client implementations. + */ +export interface BaseClientOptions { + /** WebSocket signaling URL (e.g. wss://host:5443/App/websocket) */ + websocketURL?: string; + /** HTTP REST endpoint of Ant Media (used as fallback by signaling layer) */ + httpEndpointUrl?: string; + /** If true, initializes in play-only mode and skips getUserMedia */ + isPlayMode?: boolean; + /** If true, creates data-only sessions without capturing audio/video locally */ + onlyDataChannel?: boolean; + /** Default media constraints used for getUserMedia */ + mediaConstraints?: MediaStreamConstraints; + /** Local preview element for publisher (srcObject will be assigned) */ + localVideo?: HTMLVideoElement | null; + /** Remote element to render incoming media (viewer side) */ + remoteVideo?: HTMLVideoElement | null; + /** Optional preconfigured MediaManager instance for advanced integrations */ + mediaManager?: import("./media-manager.js").MediaManager; + /** Enable verbose logging */ + debug?: boolean; + /** Enable automatic reconnection on ICE failure/disconnect (default: true) */ + autoReconnect?: boolean; + /** Configure reconnect backoff; defaults used when omitted */ + reconnectConfig?: { + backoff?: "fixed" | "exp"; + baseMs?: number; + maxMs?: number; + jitter?: number; // 0..1 + }; + /** If true, sanitize string data-channel messages by escaping HTML brackets */ + sanitizeDataChannelStrings?: boolean; +} + +/** + * Options to configure {@link WebRTCClient}. + */ +export interface WebRTCClientOptions extends BaseClientOptions {} + +export interface StreamingClientOptions extends BaseClientOptions {} + +export interface ConferenceClientOptions extends BaseClientOptions {} + +export interface ConferencePublishOptions { + streamId: string; + roomId: string; + token?: string; + subscriberId?: string; + subscriberCode?: string; + streamName?: string; + metaData?: unknown; + role?: string; +} + +export interface ConferencePlayOptions { + streamId: string; + roomId?: string; + token?: string; + enableTracks?: string[]; + subscriberId?: string; + subscriberCode?: string; + subscriberName?: string; + metaData?: unknown; + role?: string; + userPublishId?: string; + disableTracksByDefault?: boolean; +} + +/** + * Options for the one-liner {@link WebRTCClient.join} flow. + */ +export interface JoinOptions { + /** Whether to publish or view a stream */ + role: Role; + /** Unique stream identifier */ + streamId: string; + /** Optional JWT/token for secured streams */ + token?: string; + /** Optional subscriber identification fields */ + subscriberId?: string; + subscriberCode?: string; + /** Optional metadata fields propagated to server */ + streamName?: string; + mainTrack?: string; + metaData?: unknown; + roomId?: string; + /** Track configuration helpers */ + enableTracks?: string[]; + disableTracksByDefault?: boolean; + /** Timeout for join to resolve before rejecting */ + timeoutMs?: number; +} + +/** + * Result returned by {@link WebRTCClient.join} when connection is established. + */ +export interface JoinResult { + /** Stream identifier */ + streamId: string; + /** + * ICE state or first-track state observed that marks the session ready. + * - `connected` | `completed`: ICE connected + * - `track_added`: first remote or local track became active + */ + state: "connected" | "completed" | "track_added"; +} + +/** + * Convenience structure of media devices grouped by kind. + */ +export interface GroupedDevices { + videoInputs: Array<{ deviceId: string; label: string }>; + audioInputs: Array<{ deviceId: string; label: string }>; + audioOutputs: Array<{ deviceId: string; label: string }>; + /** Currently selected input device ids, when available */ + selectedVideoInputId?: string; + selectedAudioInputId?: string; + selectedAudioOutputId?: string; +} + +export interface RoomJoinOptions { + roomId: string; + streamId?: string; + role?: string; + metaData?: unknown; + streamName?: string; + mode?: "mcu" | "amcu" | "multitrack"; + timeoutMs?: number; +} + +export interface UpdateVideoTrackAssignmentsOptions { + streamId: string; + offset: number; + size: number; +} + +export interface PlaySelectiveOptions { + streamId: string; + token?: string; + roomId?: string; + enableTracks?: string[]; + subscriberId?: string; + subscriberCode?: string; + metaData?: unknown; + role?: string; + disableTracksByDefault?: boolean; +} diff --git a/packages/webrtc-sdk/src/core/webrtc-client.ts b/packages/webrtc-sdk/src/core/webrtc-client.ts new file mode 100644 index 00000000..208d6795 --- /dev/null +++ b/packages/webrtc-sdk/src/core/webrtc-client.ts @@ -0,0 +1,1651 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Logger } from "../utils/logger.js"; + +import { Emitter } from "./emitter.js"; +import type { EventMap } from "./events.js"; +import type { + GroupedDevices, + JoinOptions, + JoinResult, + WebRTCClientOptions, + RoomJoinOptions, + PlaySelectiveOptions, +} from "./types.js"; +import { WebSocketAdaptor } from "./websocket-adaptor.js"; +import { MediaManager } from "./media-manager.js"; + +interface PeerContext { + pc: RTCPeerConnection; + dc?: RTCDataChannel; + mode?: "publish" | "play"; + videoSender?: RTCRtpSender; + audioSender?: RTCRtpSender; +} + +/** + * Ant Media Server WebRTC client SDK (v2). + * + * This class is the primary entry point you should use in applications. It + * orchestrates local media (via {@link MediaManager}), signaling (via + * {@link WebSocketAdaptor}), and peer connections, and exposes a modern, + * promise-based API with typed events. + * + * Guidance: + * - Prefer using the methods on {@link WebRTCClient} (publish, play, join, + * listDevices, selectVideoInput, startScreenShare, sendData, getStats, …). + * - The lower-level classes {@link WebSocketAdaptor} and {@link MediaManager} + * are composed internally. Use them directly only for advanced + * customizations (e.g., custom signaling transport, bespoke media capture + * flows). For most apps, you should never need to instantiate or call them + * yourself. + * + * Quick start: + * ```ts + * const sdk = new WebRTCClient({ websocketURL, mediaConstraints: { audio: true, video: true }, localVideo }); + * await sdk.join({ role: 'publisher', streamId: 's1' }); + * sdk.on('publish_started', ({ streamId }) => console.log('publishing', streamId)); + * ``` + */ +export class WebRTCClient extends Emitter { + // ===== Plugin API (v2 style) ===== + static pluginInitMethods: Array<(sdk: WebRTCClient) => void> = []; + static register(initMethod: (sdk: WebRTCClient) => void): void { + WebRTCClient.pluginInitMethods.push(initMethod); + } + /** + * One-liner session helper. Creates a client, awaits ready, joins, and returns both. + * Example: + * ```ts + * const { client } = await WebRTCClient.createSession({ + * websocketURL: getWebSocketURL('wss://host:5443/App/websocket'), + * role: 'viewer', + * streamId: 's1', + * remoteVideo, + * autoPlay: true, + * }); + * ``` + */ + static async createSession( + opts: import("./types.js").WebRTCClientOptions & + Pick & { + /** Attempt to play() on the provided media element after join */ + autoPlay?: boolean; + } + ): Promise<{ client: WebRTCClient; result: import("./types.js").JoinResult }> { + const client = new WebRTCClient(opts); + await client.ready(); + const result = await client.join({ + role: opts.role, + streamId: opts.streamId, + token: opts.token, + timeoutMs: opts.timeoutMs, + }); + if (opts.autoPlay && opts.remoteVideo) { + try { + await opts.remoteVideo.play(); + } catch { + // ignore autoplay errors (e.g., user gesture required) + } + } + return { client, result }; + } + private ws?: WebSocketAdaptor; + private media: MediaManager; + private isReady = false; + isPlayMode: boolean; + private onlyDataChannel = false; + private peers: Map = new Map(); + private log = new Logger("debug"); + private peerConfig: RTCConfiguration = { + iceServers: [{ urls: "stun:stun1.l.google.com:19302" }], + }; + private remoteDescriptionSet: Map = new Map(); + private candidateQueue: Map = new Map(); + private remoteVideo: HTMLVideoElement | null; + private candidateTypes: Array<"udp" | "tcp"> = ["udp", "tcp"]; + private rxChunks: Map = + new Map(); + private autoReconnect = true; + private reconnectConfig: { + backoff: "fixed" | "exp"; + baseMs: number; + maxMs: number; + jitter: number; + } = { + backoff: "exp", + baseMs: 500, + maxMs: 8000, + jitter: 0.2, + }; + private sanitizeDcStrings = false; + private activeStreams: Map = new Map(); + private reconnectTimers: Map> = new Map(); + private lastReconnectAt: Map = new Map(); + private remoteStreams: Map = new Map(); + private audioContext: AudioContext | null = null; + private remoteMeters: Map< + string, + { + analyser: AnalyserNode; + timer: ReturnType; + data: Uint8Array; + source: MediaStreamAudioSourceNode; + } + > = new Map(); + + /** + * Create a new adaptor instance. + * @param opts See {@link WebRTCClientOptions} + */ + constructor(opts: WebRTCClientOptions) { + super(); + this.isPlayMode = !!opts.isPlayMode; + this.autoReconnect = opts.autoReconnect ?? true; + this.sanitizeDcStrings = !!opts.sanitizeDataChannelStrings; + this.onlyDataChannel = !!opts.onlyDataChannel; + if (opts.reconnectConfig) { + this.reconnectConfig = { + backoff: opts.reconnectConfig.backoff ?? this.reconnectConfig.backoff, + baseMs: opts.reconnectConfig.baseMs ?? this.reconnectConfig.baseMs, + maxMs: opts.reconnectConfig.maxMs ?? this.reconnectConfig.maxMs, + jitter: opts.reconnectConfig.jitter ?? this.reconnectConfig.jitter, + }; + } + this.media = new MediaManager({ + mediaConstraints: opts.mediaConstraints, + localVideo: opts.localVideo, + }); + this.remoteVideo = opts.remoteVideo ?? null; + + this.media.on("devices_updated", g => this.emit("devices_updated", g)); + this.media.on("local_tracks_changed", () => { + void this.applyLocalTracks(); + }); + + if (!this.isPlayMode && !this.onlyDataChannel) { + this.media.initLocalStream().catch(() => { + this.emit("error", { error: "getUserMediaIsNotAllowed" }); + }); + } + + if (opts.websocketURL || opts.httpEndpointUrl) { + this.ws = new WebSocketAdaptor({ + websocketURL: opts.websocketURL, + httpEndpointUrl: opts.httpEndpointUrl, + webrtcadaptor: { + notifyEventListeners: (info: string, obj?: unknown) => + this.notify(info as keyof EventMap, obj as never), + }, + debug: opts.debug, + }); + this.on("initialized", () => { + this.isReady = true; + this.log.info("adaptor initialized"); + this.ws?.send(JSON.stringify({ command: "getIceServerConfig" })); + }); + } + + // Initialize plugins + for (const init of WebRTCClient.pluginInitMethods) { + try { + init(this); + } catch (e) { + this.log.warn("plugin init failed", e); + } + } + } + + private notify(info: E, obj: EventMap[E]): void { + if (info === "initialized") this.isReady = true; + + if (info === "start") { + const { streamId } = obj as unknown as { streamId: string }; + + this.log.debug("start received for %s", streamId); + void this.startPublishing(streamId); + } else if (info === "takeConfiguration") { + const { streamId, sdp, type } = obj as unknown as { + streamId: string; + sdp: string; + type: RTCSdpType; + }; + + this.log.debug("takeConfiguration %s %s", streamId, type); + if (type === "answer") { + const ctx = this.peers.get(streamId); + + if (ctx) { + ctx.pc.setRemoteDescription(new RTCSessionDescription({ type, sdp })).then(() => { + this.remoteDescriptionSet.set(streamId, true); + const queued = this.candidateQueue.get(streamId) || []; + queued.forEach(c => ctx.pc.addIceCandidate(new RTCIceCandidate(c))); + this.candidateQueue.set(streamId, []); + }); + } + } else if (type === "offer") { + const pc = this.createPeer(streamId); + pc.setRemoteDescription(new RTCSessionDescription({ type, sdp })) + .then(async () => { + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + this.sendTakeConfiguration(streamId, answer.type, answer.sdp ?? ""); + this.remoteDescriptionSet.set(streamId, true); + + const queued = this.candidateQueue.get(streamId) || []; + queued.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c))); + this.candidateQueue.set(streamId, []); + this.emit("play_started", { streamId }); + }) + .catch(e => this.log.warn("setRemoteDescription failed", e)); + } + } else if (info === "takeCandidate") { + const { streamId, label, candidate } = obj as unknown as { + streamId: string; + label: number | null; + candidate: string; + }; + + this.log.debug("takeCandidate %s", streamId); + const ice: RTCIceCandidateInit = { sdpMLineIndex: label ?? undefined, candidate }; + const ctx = this.peers.get(streamId); + + if (ctx) { + if (this.remoteDescriptionSet.get(streamId)) { + ctx.pc + .addIceCandidate(new RTCIceCandidate(ice)) + .catch(e => this.log.warn("addIceCandidate failed", e)); + } else { + const q = this.candidateQueue.get(streamId) || []; + q.push(ice); + this.candidateQueue.set(streamId, q); + } + } + } else if (info === "iceServerConfig") { + const cfg = obj as unknown as { + stunServerUri?: string; + turnServerUsername?: string; + turnServerCredential?: string; + }; + + if (cfg.stunServerUri) { + if (cfg.stunServerUri.startsWith("turn:")) { + this.peerConfig.iceServers = [ + { urls: "stun:stun1.l.google.com:19302" }, + { + urls: cfg.stunServerUri, + username: cfg.turnServerUsername || "", + credential: cfg.turnServerCredential || "", + }, + ]; + } else if (cfg.stunServerUri.startsWith("stun:")) { + this.peerConfig.iceServers = [{ urls: cfg.stunServerUri }]; + } + this.log.info("updated ice servers"); + } + } else if (info === "stop") { + const { streamId } = obj as unknown as { streamId: string }; + + this.log.info("stop received for %s", streamId); + this.stop(streamId); + } else if (info === "notification") { + const payload = obj as unknown as { + definition?: string; + streamId?: string; + [k: string]: unknown; + }; + const def = payload.definition || ""; + const streamId = payload.streamId || ""; + + if (def === "publish_started") this.emit("publish_started", { streamId }); + if (def === "publish_finished") this.emit("publish_finished", { streamId }); + if (def === "play_started") this.emit("play_started", { streamId }); + if (def === "play_finished") this.emit("play_finished", { streamId }); + if (def === "subscriberCount") this.emit("subscriber_count" as keyof EventMap, obj as never); + if (def === "subscriberList") this.emit("subscriber_list" as keyof EventMap, obj as never); + if (def === "roomInformation") this.emit("room_information" as keyof EventMap, obj as never); + if (def === "broadcastObject") this.emit("broadcast_object" as keyof EventMap, obj as never); + if (def === "videoTrackAssignmentList") + this.emit("video_track_assignments" as keyof EventMap, obj as never); + if (def === "streamInformation") + this.emit("stream_information" as keyof EventMap, obj as never); + if (def === "trackList") this.emit("track_list" as keyof EventMap, obj as never); + if (def === "subtrackList") this.emit("subtrack_list" as keyof EventMap, obj as never); + if (def === "subtrackCount") this.emit("subtrack_count" as keyof EventMap, obj as never); + if (def === "joinedTheRoom") this.emit("room_joined" as keyof EventMap, obj as never); + if (def === "leavedTheRoom") this.emit("room_left" as keyof EventMap, obj as never); + // Also emit dynamic channel for other notifications + if (def) this.emit(`notification:${def}` as keyof EventMap, obj as never); + } else if (info === "closed") { + this.emit("closed", obj); + return; // prevent double-emit below + } else if (info === "server_will_stop") { + this.emit("server_will_stop", obj); + return; // prevent double-emit below + } + + this.emit(info, obj); + } + + /** + * Resolves when underlying signaling is initialized and ready. + */ + async ready(): Promise { + if (this.isReady) return; + await new Promise(resolve => { + this.once("initialized", () => resolve()); + }); + } + + private createPeer(streamId: string): RTCPeerConnection { + const pc = new RTCPeerConnection(this.peerConfig); + pc.onicecandidate = ev => { + if (ev.candidate && this.ws) { + const cand = ev.candidate.candidate || ""; + // protocol filtering similar to v1 + const protocolSupported = this.candidateTypes.some(p => cand.toLowerCase().includes(p)); + if (!protocolSupported && cand !== "") { + this.log.debug("Skipping candidate due to protocol filter: %s", cand); + return; + } + const msg = { + command: "takeCandidate", + streamId, + label: ev.candidate.sdpMLineIndex ?? 0, + id: ev.candidate.sdpMid, + candidate: ev.candidate.candidate, + }; + this.log.debug("send candidate %s", streamId); + this.ws.send(JSON.stringify(msg)); + } + }; + pc.oniceconnectionstatechange = () => { + this.log.info("ice state %s %s", streamId, pc.iceConnectionState); + this.emit("ice_connection_state_changed", { state: pc.iceConnectionState, streamId }); + // Reconnect strategy similar to v1 + if (!this.autoReconnect) return; + + if (!this.activeStreams.has(streamId)) return; + const state = pc.iceConnectionState; + + if (state === "failed" || state === "closed") { + this.reconnectIfRequired(streamId, 0, false); + } else if (state === "disconnected") { + this.reconnectIfRequired(streamId, 3000, false); + } + }; + + pc.ontrack = (event: RTCTrackEvent) => { + this.log.debug("ontrack %s", streamId); + const stream = event.streams[0]; + if (this.remoteVideo && this.remoteVideo.srcObject !== stream) { + this.remoteVideo.srcObject = stream; + } + if (stream) this.remoteStreams.set(streamId, stream); + this.emit("newTrackAvailable", { stream, track: event.track, streamId }); + }; + + this.peers.set(streamId, { pc }); + return pc; + } + + private setupDataChannel(streamId: string, dc: RTCDataChannel): void { + const ctx = this.peers.get(streamId); + if (ctx) ctx.dc = dc; + // Prefer ArrayBuffer delivery for binary frames + try { + (dc as unknown as { binaryType?: string }).binaryType = "arraybuffer"; + } catch (e) { + this.log.warn("setting binaryType failed", e); + } + dc.onerror = error => { + this.log.warn("data channel error", error); + if (dc.readyState !== "closed") + this.emit("error", { error: "data_channel_error", message: error }); + }; + dc.onopen = () => { + this.log.debug("data channel opened %s", streamId); + this.emit("data_channel_opened", { streamId }); + }; + dc.onclose = () => { + this.log.debug("data channel closed %s", streamId); + this.emit("data_channel_closed", { streamId }); + }; + dc.onmessage = event => { + const raw = event.data; + const processBuffer = (u8: Uint8Array) => { + if (u8.byteLength === 8) { + // header [token:int32, total:int32] + const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength); + const token = view.getInt32(0, true); + const total = view.getInt32(4, true); + this.rxChunks.set(token, { expected: total, received: 0, buffers: [] }); + return; + } + + if (u8.byteLength >= 4) { + const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength); + const token = view.getInt32(0, true); + const dataPart = u8.subarray(4); + const st = this.rxChunks.get(token); + + if (!st) { + // Not a chunked transfer we know; pass through + this.emit("data_received", { + streamId, + data: u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength), + }); + return; + } + st.buffers.push(dataPart); + st.received += dataPart.byteLength; + + if (st.received >= st.expected) { + const full = new Uint8Array(st.expected); + let offset = 0; + for (const b of st.buffers) { + full.set(b, offset); + offset += b.byteLength; + } + this.rxChunks.delete(token); + this.emit("data_received", { streamId, data: full.buffer }); + } + return; + } + // Fallback + this.emit("data_received", { + streamId, + data: u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength), + }); + }; + + if (typeof raw === "string") { + const text = this.sanitizeDcStrings ? raw.replace(//g, ">") : raw; + this.emit("data_received", { streamId, data: text }); + return; + } + // Blob (WebKit) → ArrayBuffer + if (typeof Blob !== "undefined" && raw instanceof Blob) { + raw + .arrayBuffer() + .then(ab => processBuffer(new Uint8Array(ab))) + .catch(() => { + this.emit("error", { error: "data_channel_blob_parse_failed", message: raw }); + }); + return; + } + // ArrayBuffer + if (raw instanceof ArrayBuffer) { + processBuffer(new Uint8Array(raw)); + return; + } + // TypedArray/DataView + if (ArrayBuffer.isView(raw)) { + const view = raw as ArrayBufferView; + processBuffer(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)); + return; + } + // Unknown type, forward as-is + this.emit("data_received", { streamId, data: raw }); + }; + } + + private async startPublishing(streamId: string): Promise { + const pc = this.peers.get(streamId)?.pc ?? this.createPeer(streamId); + const stream = this.media.getLocalStream(); + if (!stream && !this.onlyDataChannel) throw new Error("no_local_stream"); + + if (!this.onlyDataChannel && pc.getSenders().length === 0 && stream) { + for (const track of stream.getTracks()) { + const sender = pc.addTrack(track, stream); + if (track.kind === "video") (this.peers.get(streamId) as any).videoSender = sender; + if (track.kind === "audio") (this.peers.get(streamId) as any).audioSender = sender; + } + } else { + // Refresh cached senders if missing + const ctx = this.peers.get(streamId); + if (ctx) { + const senders = pc.getSenders(); + ctx.videoSender = ctx.videoSender || senders.find(s => s.track?.kind === "video"); + ctx.audioSender = ctx.audioSender || senders.find(s => s.track?.kind === "audio"); + } + } + + // create data channel in publish mode like v1 + try { + const dc = pc.createDataChannel + ? pc.createDataChannel(streamId, { ordered: true }) + : undefined; + if (dc) this.setupDataChannel(streamId, dc); + } catch (e) { + this.log.warn("createDataChannel not supported", e); + } + const offer = await pc.createOffer({ offerToReceiveAudio: false, offerToReceiveVideo: false }); + await pc.setLocalDescription(offer); + this.sendTakeConfiguration(streamId, offer.type, offer.sdp ?? ""); + } + + private sendTakeConfiguration(streamId: string, type: RTCSdpType, sdp: string): void { + if (!this.ws) return; + const msg = { command: "takeConfiguration", streamId, type, sdp }; + this.log.debug("send takeConfiguration %s %s", streamId, type); + this.ws.send(JSON.stringify(msg)); + if (type === "offer") { + this.emit("publish_started", { streamId }); + } + } + + /** + * Start publishing local tracks to the server for the given stream. + * Sends a `publish` command first; upon `start` from server, creates SDP offer. + * + * Example: + * ```ts + * const sdk = new WebRTCClient({ websocketURL, mediaConstraints: { audio: true, video: true } }); + * await sdk.publish('stream1', 'OPTIONAL_TOKEN'); + * sdk.on('publish_started', ({ streamId }) => console.log('publishing', streamId)); + * ``` + */ + async publish(streamId: string, token?: string): Promise { + await this.ready(); + + this.log.info("publish %s", streamId); + this.activeStreams.set(streamId, { mode: "publish", token }); + + const stream = this.media.getLocalStream(); + const hasVideo = this.onlyDataChannel ? false : !!stream && stream.getVideoTracks().length > 0; + const hasAudio = this.onlyDataChannel ? false : !!stream && stream.getAudioTracks().length > 0; + + if (this.ws) { + const jsCmd = { + command: "publish", + streamId, + token: token ?? "", + video: hasVideo, + audio: hasAudio, + }; + this.log.debug("send publish %s", streamId); + this.ws.send(JSON.stringify(jsCmd)); + } + } + + /** + * Start playing the given stream. The server will send an SDP offer that we answer. + * + * Example: + * ```ts + * const sdk = new WebRTCClient({ websocketURL, isPlayMode: true, remoteVideo }); + * await sdk.play('stream1'); + * sdk.on('play_started', ({ streamId }) => console.log('playing', streamId)); + * ``` + */ + async play(streamId: string, token?: string): Promise { + await this.ready(); + + this.log.info("play %s", streamId); + this.activeStreams.set(streamId, { mode: "play", token }); + + const pc = this.createPeer(streamId); + // data channel for player: server opens it + pc.ondatachannel = ev => this.setupDataChannel(streamId, ev.channel); + const ctx = this.peers.get(streamId); + if (ctx) ctx.mode = "play"; + + if (this.ws) { + const jsCmd = { + command: "play", + streamId, + token: token ?? "", + room: "", + trackList: [], + subscriberId: "", + subscriberCode: "", + viewerInfo: "", + role: "", + userPublishId: "", + }; + this.ws.send(JSON.stringify(jsCmd)); + } + } + + /** + * Selective play helper to fetch only specific subtracks and/or default-disable tracks. + * + * Example: + * ```ts + * await sdk.playSelective({ + * streamId: 'mainStreamId', + * enableTracks: ['camera_user1', 'screen_user2'], + * disableTracksByDefault: true, + * }); + * ``` + */ + async playSelective(opts: PlaySelectiveOptions): Promise { + await this.ready(); + + this.log.info("playSelective %s", opts.streamId); + this.activeStreams.set(opts.streamId, { mode: "play", token: opts.token }); + + const pc = this.createPeer(opts.streamId); + pc.ondatachannel = ev => this.setupDataChannel(opts.streamId, ev.channel); + + if (this.ws) { + const jsCmd = { + command: "play", + streamId: opts.streamId, + token: opts.token ?? "", + room: opts.roomId ?? "", + trackList: opts.enableTracks ?? [], + subscriberId: opts.subscriberId ?? "", + subscriberCode: opts.subscriberCode ?? "", + viewerInfo: opts.metaData ?? "", + role: opts.role ?? "", + userPublishId: "", + disableTracksByDefault: opts.disableTracksByDefault ?? false, + } as any; + this.ws.send(JSON.stringify(jsCmd)); + } + } + + /** + * Stop an active stream (publish or play) and close its peer connection. + */ + stop(streamId: string): void { + const ctx = this.peers.get(streamId); + if (ctx) { + try { + ctx.pc.close(); + } catch (e) { + this.log.warn("pc.close failed", e); + } + this.peers.delete(streamId); + } + // mark as intentionally stopped; prevents reconnect + this.activeStreams.delete(streamId); + + if (this.ws) { + this.ws.send(JSON.stringify({ command: "stop", streamId })); + } + // optimistic finish events + this.emit("publish_finished", { streamId }); + this.emit("play_finished", { streamId }); + } + + /** Configure reconnect backoff at runtime. */ + configureReconnect( + cfg: Partial<{ backoff: "fixed" | "exp"; baseMs: number; maxMs: number; jitter: number }> + ): void { + this.reconnectConfig = { ...this.reconnectConfig, ...cfg } as typeof this.reconnectConfig; + } + + /** + * High-level one-liner to start a session. Resolves when ICE connects or first track is added. + * + * Examples: + * ```ts + * // Publish + * const sdk = new WebRTCClient({ websocketURL, mediaConstraints: { audio: true, video: true } }); + * await sdk.join({ role: 'publisher', streamId: 's1', token: 'OPTIONAL' }); + * + * // Play + * const viewer = new WebRTCClient({ websocketURL, isPlayMode: true, remoteVideo }); + * await viewer.join({ role: 'viewer', streamId: 's1' }); + * ``` + */ + async join(options: JoinOptions): Promise { + await this.ready(); + const timeout = options.timeoutMs ?? 15000; + + return await new Promise((resolve, reject) => { + const to = setTimeout(() => reject(new Error("join_timeout")), timeout); + const onIce = (obj: EventMap["ice_connection_state_changed"]) => { + if ( + obj.streamId === options.streamId && + (obj.state === "connected" || obj.state === "completed") + ) { + cleanup(); + resolve({ streamId: options.streamId, state: obj.state }); + } + }; + const onPlayStarted = (obj: EventMap["play_started"]) => { + cleanup(); + resolve({ streamId: obj.streamId, state: "track_added" }); + }; + const onPublishStarted = (obj: EventMap["publish_started"]) => { + cleanup(); + resolve({ streamId: obj.streamId, state: "track_added" }); + }; + const onErr = () => { + cleanup(); + reject(new Error("join_failed")); + }; + const cleanup = () => { + clearTimeout(to); + this.off("ice_connection_state_changed", onIce); + this.off("play_started", onPlayStarted); + this.off("publish_started", onPublishStarted); + this.off("error", onErr); + }; + this.on("ice_connection_state_changed", onIce); + this.on("play_started", onPlayStarted); + this.on("publish_started", onPublishStarted); + this.on("error", onErr); + + if (options.role === "publisher") { + this.publish(options.streamId, options.token).catch(onErr); + } else { + this.play(options.streamId, options.token).catch(onErr); + } + }); + } + + /** + * Enumerate and group available media devices. + */ + async listDevices(): Promise { + return this.media.listDevices(); + } + + /** + * Set audio output device (sinkId) for a media element (or local preview by default). + * If the browser does not support sinkId, an `error` event with code `set_sink_id_unsupported` is emitted. + */ + async setAudioOutput(deviceId: string, element?: HTMLMediaElement | null): Promise { + await this.media.setAudioOutput(deviceId, element); + } + + /** + * Switch the active camera. Uses replaceTrack under the hood for ongoing sessions. + * + * Examples: + * ```ts + * // By deviceId + * await sdk.selectVideoInput('abcd-device-id'); + * + * // By facingMode (mobile) + * await sdk.selectVideoInput({ facingMode: 'environment' }); + * ``` + */ + async selectVideoInput(source: string | { facingMode: "user" | "environment" }): Promise { + await this.media.selectVideoInput(source); + await this.applyLocalTracks(); + } + + /** + * Switch the active microphone. Uses replaceTrack under the hood for ongoing sessions. + * + * Example: + * ```ts + * await sdk.selectAudioInput('mic-device-id'); + * ``` + */ + async selectAudioInput(deviceId: string): Promise { + await this.media.selectAudioInput(deviceId); + // If camera is disabled, there may be no video track; still ensure audio sender gets replaced + await this.applyLocalTracks(); + } + + /** Pause sending local track(s) without renegotiation. */ + pauseTrack(kind: "audio" | "video"): void { + this.media.pauseLocalTrack(kind); + } + + /** Resume sending local track(s) without renegotiation. */ + resumeTrack(kind: "audio" | "video"): void { + this.media.resumeLocalTrack(kind); + } + + /** + * Join a room for conference/multitrack scenarios. + * + * Example: + * ```ts + * await sdk.joinRoom({ roomId: 'my-room', streamId: 'publisher1' }); + * ``` + */ + async joinRoom(opts: RoomJoinOptions): Promise { + await this.ready(); + if (!this.ws) return; + const jsCmd = { + command: "joinRoom", + room: opts.roomId, + mainTrack: opts.roomId, + streamId: opts.streamId ?? "", + mode: "mcu", + streamName: "", + role: opts.role ?? "", + metadata: opts.metaData ?? "", + }; + this.ws.send(JSON.stringify(jsCmd)); + } + + /** + * Leave a previously joined room. + * + * Example: + * ```ts + * await sdk.leaveRoom('my-room', 'publisher1'); + * ``` + */ + async leaveRoom(roomId: string, streamId?: string): Promise { + await this.ready(); + if (!this.ws) return; + const jsCmd = { + command: "leaveFromRoom", + room: roomId, + mainTrack: roomId, + streamId: streamId ?? "", + }; + this.ws.send(JSON.stringify(jsCmd)); + } + + /** + * Enable/disable a specific track under a main track on the server. + * + * Example: + * ```ts + * sdk.enableTrack('mainStreamId', 'camera_user3', true); + * ``` + */ + async enableTrack(mainTrackId: string, trackId: string, enabled: boolean): Promise { + await this.ready(); + if (!this.ws) return; + const jsCmd = { + command: "enableTrack", + streamId: mainTrackId, + trackId, + enabled, + }; + this.ws.send(JSON.stringify(jsCmd)); + } + + /** + * Force the stream quality to a given height for ABR scenarios. + * + * Example: + * ```ts + * sdk.forceStreamQuality('mainStreamId', 720); // or 'auto' + * ``` + */ + async forceStreamQuality(streamId: string, height: number | "auto"): Promise { + await this.ready(); + if (!this.ws) return; + const jsCmd = { + command: "forceStreamQuality", + streamId, + streamHeight: height === "auto" ? "auto" : height, + }; + this.ws.send(JSON.stringify(jsCmd)); + } + + /** + * Begin screen sharing by replacing the outgoing video track; auto-restores when the share ends. + * + * Example: + * ```ts + * await sdk.startScreenShare(); + * ``` + */ + async startScreenShare(): Promise { + await this.media.startScreenShare(); + await this.applyLocalTracks(); + } + + /** + * Stop screen sharing and restore the camera track. + */ + async stopScreenShare(): Promise { + await this.media.stopScreenShare(); + await this.applyLocalTracks(); + } + + /** Begin screen share with camera overlay (canvas composition). */ + async startScreenWithCameraOverlay(): Promise { + await this.media.startScreenWithCameraOverlay(); + await this.applyLocalTracks(); + } + + /** Stop screen+camera overlay and restore camera track. */ + async stopScreenWithCameraOverlay(): Promise { + await this.media.stopScreenWithCameraOverlay(); + await this.applyLocalTracks(); + } + + /** + * Turn off camera hardware: stop local camera track and detach from sender without renegotiation. + */ + async turnOffLocalCamera(): Promise { + this.media.turnOffLocalCamera(); + // Replace with black dummy track similar to v1 (keeps sender alive) + for (const ctx of this.peers.values()) { + const primary = ctx.videoSender; + let sender = primary as RTCRtpSender | undefined; + if (!sender) sender = ctx.pc.getSenders().find(s => s.track && s.track.kind === "video"); + if (!sender) continue; + try { + const stream = this.media.getLocalStream(); + const blackTrack = stream?.getVideoTracks()[0] || null; + await sender.replaceTrack(blackTrack); + } catch (e) { + this.log.warn("replaceTrack(black) failed", e); + } + } + } + + /** + * Turn on camera hardware: reacquire a camera track if needed and reattach to sender without renegotiation. + */ + async turnOnLocalCamera(): Promise { + await this.media.turnOnLocalCamera(); + await this.applyLocalTracks(); + } + + /** Mute local microphone (pause audio track). */ + muteLocalMic(): void { + this.media.muteLocalMic(); + } + + /** Unmute local microphone (resume audio track). */ + unmuteLocalMic(): void { + this.media.unmuteLocalMic(); + } + + /** + * Set outgoing audio volume for the published stream (0..1). + * This controls what remote peers hear by applying a GainNode to the audio track. + * @param level Value between 0.0 (mute) and 1.0 (full volume) + */ + setVolumeLevel(level: number): void { + this.media.setVolumeLevel(level); + } + + /** + * Enable local audio level metering. + * Emits sampled RMS levels to the provided callback at the specified interval. + * @param callback Function receiving a level value (0..1 approx) + * @param periodMs Sampling interval in milliseconds (default 200ms) + */ + async enableAudioLevelForLocalStream( + callback: (level: number) => void, + periodMs = 200 + ): Promise { + await this.media.enableAudioLevelForLocalStream(callback, periodMs); + } + + /** Disable the local audio level metering started by enableAudioLevelForLocalStream. */ + disableAudioLevelForLocalStream(): void { + this.media.disableAudioLevelForLocalStream(); + } + + /** + * Enable speaking detection while muted. + * Useful to notify users when they are speaking but their mic is muted. + * @param callback Called with true when level > threshold, else false + * @param threshold Sensitivity threshold (default 0.1) + */ + async enableAudioLevelWhenMuted( + callback: (speaking: boolean) => void, + threshold = 0.1 + ): Promise { + await this.media.enableAudioLevelWhenMuted(callback, threshold); + } + + /** Disable speaking detection started by enableAudioLevelWhenMuted. */ + disableAudioLevelWhenMuted(): void { + this.media.disableAudioLevelWhenMuted(); + } + + /** + * Get a snapshot of WebRTC stats for a given stream and emit `updated_stats`. + * + * Example: + * ```ts + * const stats = await sdk.getStats('s1'); + * if (stats) { + * console.log('bytes sent', stats.totalBytesSent); + * } + * ``` + */ + async getStats(streamId: string): Promise { + const ctx = this.peers.get(streamId); + if (!ctx) return false; + try { + const stats = await ctx.pc.getStats(); + const ps = new (await import("./peer-stats.js")).PeerStats(streamId); + let bytesSent = 0, + bytesRecv = 0, + now = 0; + // iterate RTCStats entries and collect parity fields similar to v1 + stats.forEach(r => { + // totals + if (r.type === "outbound-rtp") { + bytesSent += (r as any).bytesSent || 0; + if ((r as any).packetsSent) { + if ((r as any).kind === "audio") ps.audioPacketsSent = (r as any).packetsSent; + if ((r as any).kind === "video") { + ps.videoPacketsSent = (r as any).packetsSent; + ps.frameWidth = (r as any).frameWidth ?? ps.frameWidth; + ps.frameHeight = (r as any).frameHeight ?? ps.frameHeight; + if ((r as any).framesEncoded != null) ps.framesEncoded = (r as any).framesEncoded; + } + } + now = (r as any).timestamp || now; + } else if (r.type === "inbound-rtp") { + bytesRecv += (r as any).bytesReceived || 0; + if ((r as any).packetsReceived) { + if ((r as any).kind === "audio") ps.audioPacketsReceived = (r as any).packetsReceived; + if ((r as any).kind === "video") ps.videoPacketsReceived = (r as any).packetsReceived; + } + now = (r as any).timestamp || now; + } else if (r.type === "remote-inbound-rtp") { + if ((r as any).kind === "audio") { + if ((r as any).packetsLost != null) ps.audioPacketsLost = (r as any).packetsLost; + if ((r as any).roundTripTime != null) ps.audioRoundTripTime = (r as any).roundTripTime; + if ((r as any).jitter != null) ps.audioJitter = (r as any).jitter; + } else if ((r as any).kind === "video") { + if ((r as any).packetsLost != null) ps.videoPacketsLost = (r as any).packetsLost; + if ((r as any).roundTripTime != null) ps.videoRoundTripTime = (r as any).roundTripTime; + if ((r as any).jitter != null) ps.videoJitter = (r as any).jitter; + } + } else if (r.type === "track") { + if ((r as any).kind === "video") { + if ((r as any).frameWidth != null) ps.frameWidth = (r as any).frameWidth; + if ((r as any).frameHeight != null) ps.frameHeight = (r as any).frameHeight; + if ((r as any).framesDecoded != null) ps.framesDecoded = (r as any).framesDecoded; + if ((r as any).framesDropped != null) ps.framesDropped = (r as any).framesDropped; + if ((r as any).framesReceived != null) ps.framesReceived = (r as any).framesReceived; + } + } else if (r.type === "candidate-pair" && (r as any).state === "succeeded") { + if ((r as any).availableOutgoingBitrate != null) + ps.availableOutgoingBitrateKbps = + ((r as any).availableOutgoingBitrate as number) / 1000; + if ((r as any).currentRoundTripTime != null) + ps.currentRoundTripTime = (r as any).currentRoundTripTime as number; + } + }); + ps.totalBytesSent = bytesSent; + ps.totalBytesReceived = bytesRecv; + ps.currentTimestamp = now; + this.emit("updated_stats", ps); + return ps; + } catch { + return false; + } + } + + /** + * Periodically poll stats for the given stream and emit `updated_stats`. + * + * Example: + * ```ts + * sdk.on('updated_stats', (ps) => console.log('stats', ps)); + * sdk.enableStats('s1', 2000); + * ``` + */ + enableStats(streamId: string, periodMs = 5000): void { + const key = `__stats_${streamId}`; + + if ((this as any)[key]) return; + + (this as any)[key] = setInterval(() => { + this.getStats(streamId); + }, periodMs); + } + + /** Stop periodic stats polling previously enabled by enableStats. */ + disableStats(streamId: string): void { + const key = `__stats_${streamId}`; + const timer = (this as unknown as Record)[key] as unknown as + | ReturnType + | undefined; + if (timer) { + clearInterval(timer); + delete (this as unknown as Record)[key]; + } + } + + /** + * Send data over the data channel. Strings are sent as-is; ArrayBuffers are chunked with backpressure. + * + * Examples: + * ```ts + * // Text message + * sdk.sendData('s1', 'hello world'); + * + * // Binary (ArrayBuffer) + * const bytes = new Uint8Array([1,2,3,4]).buffer; + * await sdk.sendData('s1', bytes); + * + * // Listen + * sdk.on('data_received', ({ streamId, data }) => { + * if (typeof data === 'string') console.log('text', data); + * else console.log('binary', new Uint8Array(data)); + * }); + * ``` + */ + async sendData(streamId: string, data: string | ArrayBuffer): Promise { + const ctx = this.peers.get(streamId); + if (!ctx || !ctx.dc) { + this.log.warn("sendData: data channel not available for %s", streamId); + throw new Error("data_channel_not_available"); + } + const dc = ctx.dc; + if (typeof data === "string") { + dc.send(data); + return; + } + // chunked binary similar to v1 + const CHUNK_SIZE = 16000; + const length = (data as ArrayBuffer).byteLength; + const token = Math.floor(Math.random() * 999999) | 0; + const header = new Int32Array(2); + header[0] = token; + header[1] = length; + dc.send(header); + + let sent = 0; + // backpressure + dc.bufferedAmountLowThreshold = 1 << 20; // 1MB + while (sent < length) { + const size = Math.min(length - sent, CHUNK_SIZE); + const buffer = new Uint8Array(size + 4); + const tokenArray = new Int32Array(1); + tokenArray[0] = token; + buffer.set(new Uint8Array(tokenArray.buffer, 0, 4), 0); + const chunk = new Uint8Array(data as ArrayBuffer, sent, size); + buffer.set(chunk, 4); + // wait if congested + if (dc.bufferedAmount > dc.bufferedAmountLowThreshold) { + await new Promise(resolve => { + const onlow = () => { + ( + dc as unknown as { + removeEventListener: (type: string, listener: (...args: unknown[]) => void) => void; + } + ).removeEventListener("bufferedamountlow", onlow); + resolve(); + }; + ( + dc as unknown as { + addEventListener: ( + type: string, + listener: (...args: unknown[]) => void, + options?: unknown + ) => void; + } + ).addEventListener("bufferedamountlow", onlow, { once: true } as unknown); + }); + } + dc.send(buffer); + sent += size; + } + } + + /** Convenience: send JSON over data channel (stringifies safely). */ + async sendJSON(streamId: string, obj: unknown): Promise { + try { + const text = JSON.stringify(obj); + await this.sendData(streamId, text); + } catch (e) { + this.log.warn("sendJSON stringify failed", e); + throw e; + } + } + + /** Close signaling and all peers; emit closed. */ + close(): void { + for (const streamId of Array.from(this.peers.keys())) { + this.stop(streamId); + } + try { + this.ws?.close(); + } catch (e) { + this.log.warn("ws close failed", e); + } + this.emit("closed", undefined as unknown as never); + } + + /** Toggle sanitization for incoming data-channel strings at runtime. */ + setSanitizeDataChannelStrings(enabled: boolean): void { + this.sanitizeDcStrings = !!enabled; + } + + private reconnectIfRequired(streamId: string, delayMs = 3000, forceReconnect = false): void { + if (!this.autoReconnect) return; + if (!this.activeStreams.has(streamId)) return; + if (delayMs <= 0) delayMs = this.reconnectConfig.baseMs; + if (this.reconnectTimers.has(streamId)) return; + const now = Date.now(); + const last = this.lastReconnectAt.get(streamId) ?? 0; + if (!forceReconnect && now - last < 1000) { + delayMs = Math.max(delayMs, 1000); + } + // notify reconnection attempt similar to v1 + const mode = this.activeStreams.get(streamId)?.mode; + if (mode === "publish") + this.emit("reconnection_attempt_for_publisher" as keyof EventMap, { streamId } as never); + else if (mode === "play") + this.emit("reconnection_attempt_for_player" as keyof EventMap, { streamId } as never); + const nextDelay = this.computeNextDelay(delayMs); + const timer = setTimeout(() => { + this.reconnectTimers.delete(streamId); + this.tryAgain(streamId, forceReconnect); + }, nextDelay); + this.reconnectTimers.set(streamId, timer); + } + + private computeNextDelay(lastDelay: number): number { + const { backoff, baseMs, maxMs, jitter } = this.reconnectConfig; + let next = + backoff === "exp" + ? Math.min(maxMs, Math.max(baseMs, lastDelay * 2)) + : Math.min(maxMs, baseMs); + if (jitter > 0) { + const rand = 1 + (Math.random() * 2 - 1) * jitter; // 1±jitter + next = Math.max(0, Math.floor(next * rand)); + } + return next; + } + + private tryAgain(streamId: string, _forceReconnect: boolean): void { + this.lastReconnectAt.set(streamId, Date.now()); + if (_forceReconnect) { + this.log.info("Force reconnect requested for %s", streamId); + } + const active = this.activeStreams.get(streamId); + if (!active) return; + // stop first to clean up + try { + this.stop(streamId); + } catch (e) { + this.log.warn("stop during reconnect failed", e); + } + setTimeout(() => { + if (active.mode === "publish") { + this.log.info("Re-publish attempt for %s", streamId); + void this.publish(streamId, active.token).catch(e => this.log.warn("republish failed", e)); + } else { + this.log.info("Re-play attempt for %s", streamId); + void this.play(streamId, active.token).catch(e => this.log.warn("replay failed", e)); + } + }, 500); + } + + // ===== Parity signaling helpers (v1 compatibility) ===== + /** Instruct server to enable/disable a remote video track. */ + toggleVideo(streamId: string, trackId: string, enabled: boolean): void { + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "toggleVideo", streamId, trackId, enabled })); + } + + /** Instruct server to enable/disable a remote audio track. */ + toggleAudio(streamId: string, trackId: string, enabled: boolean): void { + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "toggleAudio", streamId, trackId, enabled })); + } + + /** Request stream info; listen on 'notification:streamInformation' or stream_information. */ + async getStreamInfo(streamId: string): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getStreamInfo", streamId })); + } + + /** Request broadcast object; listen on 'notification:broadcastObject' or broadcast_object. */ + async getBroadcastObject(streamId: string): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getBroadcastObject", streamId })); + } + + /** Request room info of roomId; optionally include streamId for context. */ + async getRoomInfo(roomId: string, streamId = ""): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getRoomInfo", room: roomId, streamId })); + } + + /** Request track list under a main stream. */ + async getTracks(streamId: string, token = ""): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getTrackList", streamId, token })); + } + + /** Request subtracks for a main stream with optional paging and role filter. */ + async getSubtracks(streamId: string, role = "", offset = 0, size = 50): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getSubtracks", streamId, role, offset, size })); + } + + /** Request subtrack count for a main stream with optional role/status. */ + async getSubtrackCount(streamId: string, role = "", status = ""): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getSubtracksCount", streamId, role, status })); + } + + /** Request current subscriber count; listen on subscriber_count. */ + async getSubscriberCount(streamId: string): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getSubscriberCount", streamId })); + } + + /** Request current subscriber list; listen on subscriber_list. */ + async getSubscriberList(streamId: string, offset = 0, size = 50): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getSubscribers", streamId, offset, size })); + } + + /** Peer-to-peer messaging helper. */ + peerMessage(streamId: string, definition: string, data: unknown): void { + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "peerMessageCommand", streamId, definition, data })); + } + + /** Register a push notification token with AMS. */ + registerPushNotificationToken( + subscriberId: string, + authToken: string, + pushToken: string, + tokenType: "fcm" | "apn" + ): void { + if (!this.ws) return; + this.ws.send( + JSON.stringify({ + command: "registerPushNotificationToken", + subscriberId, + token: authToken, + pnsRegistrationToken: pushToken, + pnsType: tokenType, + }) + ); + } + + /** Send a push notification to specific subscribers. */ + sendPushNotification( + subscriberId: string, + authToken: string, + pushNotificationContent: Record, + subscriberIdsToNotify: string[] + ): void { + if (!this.ws) return; + if (typeof pushNotificationContent !== "object") { + throw new Error("pushNotificationContent must be an object"); + } + if (!Array.isArray(subscriberIdsToNotify)) { + throw new Error("subscriberIdsToNotify must be an array"); + } + this.ws.send( + JSON.stringify({ + command: "sendPushNotification", + subscriberId, + token: authToken, + pushNotificationContent, + subscriberIdsToNotify, + }) + ); + } + + /** Send a push notification to a topic. */ + sendPushNotificationToTopic( + subscriberId: string, + authToken: string, + pushNotificationContent: Record, + topic: string + ): void { + if (!this.ws) return; + if (typeof pushNotificationContent !== "object") { + throw new Error("pushNotificationContent must be an object"); + } + this.ws.send( + JSON.stringify({ + command: "sendPushNotification", + subscriberId, + token: authToken, + pushNotificationContent, + topic, + }) + ); + } + + /** Request video track assignments list for a main stream. */ + async requestVideoTrackAssignments(streamId: string): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "getVideoTrackAssignmentsCommand", streamId })); + } + + /** + * Assign/unassign a specific video track under a main stream. + * + * Example: + * ```ts + * // Show only a specific participant's camera + * sdk.assignVideoTrack('mainStreamId', 'camera_user3', true); + * // Hide it again + * sdk.assignVideoTrack('mainStreamId', 'camera_user3', false); + * ``` + */ + assignVideoTrack(streamId: string, videoTrackId: string, enabled: boolean): void { + if (!this.ws) return; + this.ws.send( + JSON.stringify({ + command: "assignVideoTrackCommand", + streamId, + videoTrackId, + enabled, + }) + ); + } + + /** + * Update paginated video track assignments for UI pagination scenarios. + * + * Example: + * ```ts + * // Fetch next page of assignments (offset 20, size 10) + * sdk.updateVideoTrackAssignments({ streamId: 'main', offset: 20, size: 10 }); + * ``` + */ + async updateVideoTrackAssignments( + opts: import("./types.js").UpdateVideoTrackAssignmentsOptions + ): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send( + JSON.stringify({ + command: "updateVideoTrackAssignmentsCommand", + streamId: opts.streamId, + offset: opts.offset, + size: opts.size, + }) + ); + } + + /** + * Set the maximum number of video tracks for a main stream (conference pagination). + * + * Example: + * ```ts + * sdk.setMaxVideoTrackCount('mainStreamId', 9); + * ``` + */ + async setMaxVideoTrackCount(streamId: string, maxTrackCount: number): Promise { + await this.ready(); + if (!this.ws) return; + this.ws.send( + JSON.stringify({ + command: "setMaxVideoTrackCountCommand", + streamId, + maxTrackCount, + }) + ); + } + + /** + * Change outbound video bandwidth (kbps) or 'unlimited' via RTCRtpSender.setParameters. + * + * Example: + * ```ts + * await sdk.changeBandwidth('s1', 600); // limit to 600 kbps + * await sdk.changeBandwidth('s1', 'unlimited'); + * ``` + */ + async changeBandwidth(streamId: string, bandwidth: number | "unlimited"): Promise { + const ctx = this.peers.get(streamId); + if (!ctx) return; + const sender = ctx.videoSender || ctx.pc.getSenders().find(s => s.track?.kind === "video"); + if (!sender) return; + const params = sender.getParameters(); + params.encodings = params.encodings || [{}]; + if (bandwidth === "unlimited") + delete (params.encodings[0] as Record).maxBitrate; + else (params.encodings[0] as Record).maxBitrate = bandwidth * 1000; + try { + await sender.setParameters(params); + } catch (e) { + this.log.warn("setParameters(maxBitrate) failed", e); + } + } + + /** + * Set degradationPreference for the video sender. + * + * Example: + * ```ts + * await sdk.setDegradationPreference('s1', 'maintain-framerate'); + * ``` + */ + async setDegradationPreference( + streamId: string, + preference: "maintain-framerate" | "maintain-resolution" | "balanced" + ): Promise { + const ctx = this.peers.get(streamId); + if (!ctx) return; + const sender = ctx.videoSender || ctx.pc.getSenders().find(s => s.track?.kind === "video"); + if (!sender) return; + const params = sender.getParameters(); + try { + (params as unknown as { degradationPreference?: string }).degradationPreference = preference; + await sender.setParameters(params); + this.log.info("Degradation Preference set to %s", preference); + } catch (e) { + this.log.warn("setParameters(degradationPreference) failed", e); + } + } + + /** Update stream metadata on the server side. */ + updateStreamMetaData(streamId: string, metaData: unknown): void { + if (!this.ws) return; + this.ws.send(JSON.stringify({ command: "updateStreamMetaData", streamId, metaData })); + } + + private async applyLocalTracks(): Promise { + const stream = this.media.getLocalStream(); + if (!stream) return; + for (const ctx of this.peers.values()) { + const senders = ctx.pc.getSenders(); + // v1 parity: update video first, then audio + const videoTracks = stream.getVideoTracks(); + for (const track of videoTracks) { + let sender = + (track.kind === "video" ? ctx.videoSender : ctx.audioSender) || + senders.find(s => s.track && s.track.kind === track.kind); + if (sender && sender.replaceTrack) { + try { + await sender.replaceTrack(track); + if (track.kind === "video") ctx.videoSender = sender; + if (track.kind === "audio") ctx.audioSender = sender; + } catch (e) { + this.log.warn("replaceTrack failed", e); + } + } else { + try { + sender = ctx.pc.addTrack(track, stream); + if (track.kind === "video") ctx.videoSender = sender; + if (track.kind === "audio") ctx.audioSender = sender; + } catch (e) { + this.log.warn("addTrack failed", e); + } + } + } + const audioTracks = stream.getAudioTracks(); + for (const track of audioTracks) { + let sender = ctx.audioSender || senders.find(s => s.track && s.track.kind === "audio"); + if (sender && sender.replaceTrack) { + try { + await sender.replaceTrack(track); + ctx.audioSender = sender; + } catch (e) { + this.log.warn("replaceTrack failed", e); + } + } else { + try { + sender = ctx.pc.addTrack(track, stream); + ctx.audioSender = sender; + } catch (e) { + this.log.warn("addTrack failed", e); + } + } + } + } + } + + // ===== Remote audio level metering (viewer side) ===== + /** Measure audio level for remote stream and invoke callback periodically. */ + async enableRemoteAudioLevel( + streamId: string, + callback: (level: number) => void, + periodMs = 200 + ): Promise { + const stream = + this.remoteStreams.get(streamId) || + (this.remoteVideo?.srcObject as MediaStream | null) || + null; + if (!stream) return; + if (!this.audioContext) this.audioContext = new AudioContext(); + const ctx = this.audioContext; + const source = ctx.createMediaStreamSource(stream); + const analyser = ctx.createAnalyser(); + analyser.fftSize = 256; + source.connect(analyser); + const data = new Uint8Array(analyser.frequencyBinCount); + if (this.remoteMeters.has(streamId)) this.disableRemoteAudioLevel(streamId); + const timer = setInterval(() => { + analyser.getByteTimeDomainData(data); + let sum = 0; + for (let i = 0; i < data.length; i++) { + const v = (data[i] - 128) / 128; + sum += v * v; + } + const rms = Math.sqrt(sum / data.length); + try { + callback(rms); + } catch (e) { + this.log.warn("remote audio level callback failed", e); + } + }, periodMs); + this.remoteMeters.set(streamId, { analyser, timer, data, source }); + } + + /** Stop remote audio level metering. */ + disableRemoteAudioLevel(streamId: string): void { + const meter = this.remoteMeters.get(streamId); + if (!meter) return; + clearInterval(meter.timer); + try { + meter.source.disconnect(); + } catch (e) { + this.log.warn("remote audio source disconnect failed", e); + } + try { + meter.analyser.disconnect(); + } catch (e) { + this.log.warn("remote audio analyser disconnect failed", e); + } + this.remoteMeters.delete(streamId); + } +} diff --git a/packages/webrtc-sdk/src/core/websocket-adaptor.ts b/packages/webrtc-sdk/src/core/websocket-adaptor.ts new file mode 100644 index 00000000..c84ac4d3 --- /dev/null +++ b/packages/webrtc-sdk/src/core/websocket-adaptor.ts @@ -0,0 +1,142 @@ +import { Logger, type LogLevel } from "../utils/logger.js"; + +import { Emitter } from "./emitter.js"; +import type { EventMap } from "./events.js"; + +/** + * Minimal interface implemented by the signaling transport. + */ +export interface IWebSocketAdaptor { + isConnected(): boolean; + isConnecting(): boolean; + send(text: string): void; + close(): void; +} + +/** + * Configuration options for {@link WebSocketAdaptor}. + */ +export interface WebSocketAdaptorOptions { + websocketURL?: string; + httpEndpointUrl?: string; + webrtcadaptor: { notifyEventListeners: (info: string, obj?: unknown) => void }; + debug?: boolean | LogLevel; +} + +/** + * Thin wrapper around WebSocket that adapts Ant Media's signaling protocol + * and emits typed events to the adaptor. + */ +export class WebSocketAdaptor extends Emitter implements IWebSocketAdaptor { + private ws?: WebSocket; + private connecting = false; + private connected = false; + private opts: WebSocketAdaptorOptions; + private log: Logger; + private pingTimer: ReturnType | null = null; + + /** + * Create a new WebSocket adaptor. + */ + constructor(opts: WebSocketAdaptorOptions) { + super(); + this.opts = opts; + this.log = new Logger( + typeof opts.debug === "string" ? opts.debug : opts.debug ? "debug" : "info" + ); + if (opts.websocketURL || opts.httpEndpointUrl) { + this.init(); + } + } + + private startPing(): void { + this.clearPing(); + this.pingTimer = setInterval(() => { + this.send(JSON.stringify({ command: "ping" })); + }, 3000); + } + + private clearPing(): void { + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + } + + private init(): void { + if (!this.opts.websocketURL) return; + this.connecting = true; + this.connected = false; + this.log.info("connecting to websocket %s", this.opts.websocketURL); + const ws = new WebSocket(this.opts.websocketURL); + this.ws = ws; + + ws.onopen = () => { + this.connected = true; + this.connecting = false; + this.log.info("websocket connected"); + this.startPing(); + this.opts.webrtcadaptor.notifyEventListeners("initialized"); + }; + + ws.onmessage = ev => { + this.log.debug("ws message: %s", ev.data); + try { + const obj = JSON.parse(ev.data); + if (obj && obj.command) { + this.opts.webrtcadaptor.notifyEventListeners(obj.command, obj); + } + } catch (e) { + this.log.warn("ws message parse failed", e); + } + }; + + ws.onerror = e => { + this.connected = false; + this.connecting = false; + this.clearPing(); + this.log.error("websocket error", e); + this.opts.webrtcadaptor.notifyEventListeners("error", { + error: "WebSocketNotConnected", + message: "websocket error", + }); + }; + + ws.onclose = ev => { + this.connected = false; + this.connecting = false; + this.clearPing(); + this.log.warn("websocket closed"); + this.opts.webrtcadaptor.notifyEventListeners("closed", ev); + }; + } + + isConnected(): boolean { + return this.connected; + } + + isConnecting(): boolean { + return this.connecting; + } + + send(text: string): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.log.warn("send while not connected"); + try { + this.ws?.send(text); + } catch {} + this.opts.webrtcadaptor.notifyEventListeners("error", { + error: "WebSocketNotConnected", + message: text, + }); + return; + } + this.log.debug("send: %s", text); + this.ws.send(text); + } + + close(): void { + this.clearPing(); + this.ws?.close(); + } +} diff --git a/packages/webrtc-sdk/src/index.ts b/packages/webrtc-sdk/src/index.ts new file mode 100644 index 00000000..266afc86 --- /dev/null +++ b/packages/webrtc-sdk/src/index.ts @@ -0,0 +1,12 @@ +export * from "./core/types.js"; +export * from "./core/events.js"; +export * from "./core/peer-stats.js"; +export * from "./core/errors.js"; +export * from "./core/emitter.js"; +export * from "./core/websocket-adaptor.js"; +export * from "./core/media-manager.js"; +export * from "./core/webrtc-client.js"; +export * from "./utils/utility.js"; +export * from "./client/base-client.js"; +export * from "./client/streaming-client.js"; +export * from "./client/conference-client.js"; diff --git a/packages/webrtc-sdk/src/utils/logger.ts b/packages/webrtc-sdk/src/utils/logger.ts new file mode 100644 index 00000000..1693bd55 --- /dev/null +++ b/packages/webrtc-sdk/src/utils/logger.ts @@ -0,0 +1,46 @@ +export type LogLevel = "debug" | "info" | "warn" | "error" | "none"; + +export interface ILogger { + level: LogLevel; + debug(message: string, ...args: unknown[]): void; + info(message: string, ...args: unknown[]): void; + warn(message: string, ...args: unknown[]): void; + error(message: string, ...args: unknown[]): void; +} + +const order: Record, number> = { + debug: 10, + info: 20, + warn: 30, + error: 40, +}; + +export class Logger implements ILogger { + level: LogLevel; + + constructor(level: LogLevel = "info") { + this.level = level; + } + + private enabled(target: Exclude): boolean { + if (this.level === "none") return false; + if (this.level === target) return true; + // allow higher-severity logs when level is lower number (debug < info < warn < error) + const min = order[this.level as Exclude]; + const cur = order[target]; + return cur >= min; + } + + debug(message: string, ...args: unknown[]): void { + if (this.enabled("debug")) console.debug(`[AMS][DEBUG] ${message}`, ...args); + } + info(message: string, ...args: unknown[]): void { + if (this.enabled("info")) console.info(`[AMS][INFO] ${message}`, ...args); + } + warn(message: string, ...args: unknown[]): void { + if (this.enabled("warn")) console.warn(`[AMS][WARN] ${message}`, ...args); + } + error(message: string, ...args: unknown[]): void { + if (this.enabled("error")) console.error(`[AMS][ERROR] ${message}`, ...args); + } +} diff --git a/packages/webrtc-sdk/src/utils/utility.ts b/packages/webrtc-sdk/src/utils/utility.ts new file mode 100644 index 00000000..51cc571a --- /dev/null +++ b/packages/webrtc-sdk/src/utils/utility.ts @@ -0,0 +1,19 @@ +export function generateRandomString(n: number): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let out = ""; + for (let i = 0; i < n; i++) out += chars.charAt(Math.floor(Math.random() * chars.length)); + return out; +} + +export function getWebSocketURL(location: Location, rtmpForward?: string): string { + const appName = location.pathname.substring(1, location.pathname.indexOf("/", 1) + 1); + let path = `${location.hostname}:${location.port}/${appName}websocket`; + if (typeof rtmpForward !== "undefined") { + path += `?rtmpForward=${rtmpForward}`; + } + let websocketURL = `ws://${path}`; + if (location.protocol.startsWith("https")) { + websocketURL = `wss://${path}`; + } + return websocketURL; +} diff --git a/packages/webrtc-sdk/test/candidate-queue.test.ts b/packages/webrtc-sdk/test/candidate-queue.test.ts new file mode 100644 index 00000000..d95a7c5b --- /dev/null +++ b/packages/webrtc-sdk/test/candidate-queue.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +class MockPC { + localDescription: any; + iceConnectionState = 'new'; + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + ondatachannel: ((ev: any) => void) | null = null; + addCount = 0; + getSenders() { return []; } + addTrack() {} + async createAnswer() { return { type: 'answer' as const, sdp: 'v=0\n' }; } + async setLocalDescription(desc: any) { this.localDescription = desc; } + async setRemoteDescription() {} + async addIceCandidate() { this.addCount++; } +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; +// @ts-ignore +(global as any).RTCSessionDescription = function (x: any) { return x; }; +// @ts-ignore +(global as any).RTCIceCandidate = function (x: any) { return x; }; + + +describe('candidate queueing', () => { + it('queues until remote description set then flushes', async () => { + const sent: any[] = []; + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: true }); + // @ts-ignore + adaptor['ws'] = { send: (t: string) => sent.push(JSON.parse(t)) } as any; + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + await adaptor.play('q1'); + + // Before remote description, deliver candidate + // @ts-ignore + adaptor['notify']('takeCandidate', { streamId: 'q1', label: 0, candidate: 'candidate udp 0' } as any); + + // Now deliver offer to trigger setRemote and answer + // @ts-ignore + adaptor['notify']('takeConfiguration', { streamId: 'q1', sdp: 'v=0\n', type: 'offer' } as any); + + await new Promise((r) => setTimeout(r, 0)); + + const pc: MockPC = (adaptor as any)['peers'].get('q1').pc; + expect(pc.addCount).toBe(1); + }); +}); diff --git a/packages/webrtc-sdk/test/data-channel.test.ts b/packages/webrtc-sdk/test/data-channel.test.ts new file mode 100644 index 00000000..db3fce3d --- /dev/null +++ b/packages/webrtc-sdk/test/data-channel.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect, vi } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +// mock MediaStream/getUserMedia +try { + Object.defineProperty(globalThis, 'navigator', { + value: { + mediaDevices: { + getUserMedia: vi.fn(async () => { + const ms = new MediaStream() as any; + ms.getTracks = () => [ + { kind: 'audio', enabled: true }, + { kind: 'video', enabled: true }, + ]; + ms.getVideoTracks = () => [{ kind: 'video', enabled: true }]; + ms.getAudioTracks = () => [{ kind: 'audio', enabled: true }]; + return ms as MediaStream; + }), + enumerateDevices: vi.fn(async () => []), + }, + }, + configurable: true, + }); +} catch {} + +class MockDC { + readyState = 'open'; + onmessage: ((ev: MessageEvent) => void) | null = null; + onerror: ((ev: any) => void) | null = null; + onopen: (() => void) | null = null; + onclose: (() => void) | null = null; + bufferedAmount = 0; + bufferedAmountLowThreshold = 0; + private listeners: Record = {}; + send(_d: any) {} + addEventListener(type: string, fn: any) { + this.listeners[type] ||= []; + this.listeners[type].push(fn); + } + removeEventListener(type: string, fn: any) { + this.listeners[type] = (this.listeners[type]||[]).filter(f => f!==fn); + } + emit(type: string) { + (this.listeners[type]||[]).forEach(f => f()); + } +} + +class MockPC { + ondatachannel: ((ev: any) => void) | null = null; + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + iceConnectionState = 'connected'; + getSenders(){ return []; } + addTrack(){} + createDataChannel(_label: string){ return new MockDC() as any; } + async createOffer(){ return { type: 'offer' as const, sdp: 'v=0\n' }; } + async setLocalDescription(){ } +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; +// @ts-ignore +(global as any).RTCSessionDescription = function(x:any){ return x; }; + +describe('Data channel chunking and reassembly', () => { + it('reassembles binary chunks into a single ArrayBuffer', async () => { + const sent: any[] = []; + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: false, mediaConstraints: { video: false, audio: false } }); + // @ts-ignore + adaptor['ws'] = { send: (t: string) => sent.push(JSON.parse(t)) } as any; + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + const streamId = 'sbin'; + await adaptor.publish(streamId); + // trigger start -> creates DC + // @ts-ignore + adaptor['notify']('start', { streamId } as any); + + // wait until data channel is set + let dc: MockDC | undefined; + for (let i = 0; i < 10; i++) { + // @ts-ignore + const ctx = adaptor['peers'].get(streamId); + dc = ctx?.dc as unknown as MockDC | undefined; + if (dc) break; + await new Promise((r) => setTimeout(r, 0)); + } + expect(dc).toBeTruthy(); + + const received: any[] = []; + adaptor.on('data_received', (e) => received.push(e)); + + // simulate incoming header and chunks + const payload = new Uint8Array(50000); + for (let i=0;i { + beforeEach(() => { + // @ts-expect-error mock + global.RTCPeerConnection = vi.fn(() => ({ + addTrack: vi.fn(), + getSenders: vi.fn(() => []), + close: vi.fn(), + onicecandidate: null, + oniceconnectionstatechange: null, + ontrack: null, + createDataChannel: vi.fn(() => ({ + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + send: vi.fn(), + })), + })); + // @ts-expect-error mock + global.WebSocket = vi.fn(() => ({ send: vi.fn(), close: vi.fn(), readyState: 1 })); + }); + + it('escapes angle brackets in text messages when enabled', async () => { + const sdk = new WebRTCClient({ websocketURL: 'ws://x', sanitizeDataChannelStrings: true, mediaConstraints: { audio: true, video: true } }); + const dc: any = { addEventListener: vi.fn(), removeEventListener: vi.fn(), send: vi.fn(), onmessage: null }; + // create a peer and inject data channel handler + (sdk as any).peers.set('s1', { pc: new (global as any).RTCPeerConnection({}) }); + (sdk as any).setupDataChannel('s1', dc); + + let received: string | ArrayBuffer | null = null; + sdk.on('data_received', ({ data }) => { received = data as any; }); + + dc.onmessage({ data: 'tag' }); + expect(received).toBe('<b>tag</b>'); + }); +}); + + diff --git a/packages/webrtc-sdk/test/emitter.test.ts b/packages/webrtc-sdk/test/emitter.test.ts new file mode 100644 index 00000000..c856acce --- /dev/null +++ b/packages/webrtc-sdk/test/emitter.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from 'vitest'; +import { Emitter } from '../src/core/emitter.js'; + +interface Map { + a: { x: number }; + b: void; + error: { error: string }; +} + +describe('Emitter', () => { + it('emits and listens', () => { + const e = new Emitter(); + const fn = vi.fn(); + e.on('a', fn); + e.emit('a', { x: 1 }); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith({ x: 1 }); + }); + + it('once only fires once', () => { + const e = new Emitter(); + const fn = vi.fn(); + e.once('a', fn); + e.emit('a', { x: 1 }); + e.emit('a', { x: 2 }); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('off removes handler', () => { + const e = new Emitter(); + const fn = vi.fn(); + e.on('a', fn); + e.off('a', fn); + e.emit('a', { x: 3 }); + expect(fn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/webrtc-sdk/test/errors.test.ts b/packages/webrtc-sdk/test/errors.test.ts new file mode 100644 index 00000000..0af8e4e9 --- /dev/null +++ b/packages/webrtc-sdk/test/errors.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +class MockPC { + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + getSenders() { return []; } + addTrack() {} +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; + +describe('error codes', () => { + it('emits structured error events for data channel parse failures', async () => { + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: true }); + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + // install fake peer and dc (trigger onmessage parsing path) + const dc: any = { readyState: 'open', onerror: null, onopen: null, onclose: null, onmessage: null, addEventListener(){}, removeEventListener(){} }; + // @ts-ignore + adaptor['peers'].set('s1', { pc: new RTCPeerConnection() as any, dc }); + // @ts-ignore + adaptor['setupDataChannel']('s1', dc); + + let err: any = null; + adaptor.on('error', (e) => { err = e; }); + // simulate blob parse failure path + class FakeBlob { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(public _v: any) {} + async arrayBuffer(): Promise { + return Promise.reject(new Error('fail')); + } + } + // @ts-ignore + globalThis.Blob = FakeBlob as any; + const blob = new FakeBlob('x') as unknown as Blob; + dc.onmessage({ data: blob }); + await new Promise((r) => setTimeout(r, 0)); + expect(err).toBeTruthy(); + }); +}); + + diff --git a/packages/webrtc-sdk/test/notifications.test.ts b/packages/webrtc-sdk/test/notifications.test.ts new file mode 100644 index 00000000..1b10f3f2 --- /dev/null +++ b/packages/webrtc-sdk/test/notifications.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +class MockPC { + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + getSenders() { return []; } + addTrack() {} + async createOffer(){ return { type: 'offer' as const, sdp: 'v=0\n' }; } + async setLocalDescription() {} +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; +// @ts-ignore +(global as any).RTCSessionDescription = function(x:any){ return x; }; + +describe('notifications mapping', () => { + it('maps server notifications and closed/server_will_stop', async () => { + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: false, mediaConstraints: { video: false, audio: false } }); + const events: string[] = []; + adaptor.on('publish_started', () => events.push('publish_started')); + adaptor.on('publish_finished', () => events.push('publish_finished')); + adaptor.on('play_started', () => events.push('play_started')); + adaptor.on('play_finished', () => events.push('play_finished')); + adaptor.on('closed', () => events.push('closed')); + adaptor.on('server_will_stop', () => events.push('server_will_stop')); + adaptor.on('notification:subscriberCount' as any, () => events.push('notif:subscriberCount')); + adaptor.on('subscriber_count' as any, () => events.push('evt:subscriber_count')); + + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + // @ts-ignore + adaptor['notify']('notification', { definition: 'publish_started', streamId: 's' } as any); + // @ts-ignore + adaptor['notify']('notification', { definition: 'publish_finished', streamId: 's' } as any); + // @ts-ignore + adaptor['notify']('notification', { definition: 'play_started', streamId: 's' } as any); + // @ts-ignore + adaptor['notify']('notification', { definition: 'play_finished', streamId: 's' } as any); + // @ts-ignore + adaptor['notify']('closed', {} as any); + // @ts-ignore + adaptor['notify']('server_will_stop', {} as any); + // dynamic + // @ts-ignore + adaptor['notify']('notification', { definition: 'subscriberCount', streamId: 's' } as any); + + expect(events).toEqual([ + 'publish_started', + 'publish_finished', + 'play_started', + 'play_finished', + 'closed', + 'server_will_stop', + 'evt:subscriber_count', + 'notif:subscriberCount', + ]); + }); +}); + + diff --git a/packages/webrtc-sdk/test/overlay-audio.test.ts b/packages/webrtc-sdk/test/overlay-audio.test.ts new file mode 100644 index 00000000..d8172b49 --- /dev/null +++ b/packages/webrtc-sdk/test/overlay-audio.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +// Minimal DOM/media mocks +class MockTrack { + kind: 'audio' | 'video'; + enabled = true; + onended: (() => void) | null = null; + constructor(kind: 'audio' | 'video') { this.kind = kind; } + stop() { /* noop */ } +} +class MockStream { + private tracks: MediaStreamTrack[]; + constructor(tracks: MediaStreamTrack[]) { this.tracks = tracks; } + getTracks() { return this.tracks; } + getVideoTracks() { return this.tracks.filter(t => t.kind === 'video'); } + getAudioTracks() { return this.tracks.filter(t => t.kind === 'audio'); } + addTrack(t: MediaStreamTrack) { this.tracks.push(t); } + removeTrack(t: MediaStreamTrack) { this.tracks = this.tracks.filter(x => x !== t); } +} + +describe('overlay and audio utilities', () => { + beforeEach(() => { + // @ts-expect-error mock + global.RTCPeerConnection = vi.fn(() => ({ + addTrack: vi.fn(), + getSenders: vi.fn(() => []), + close: vi.fn(), + createDataChannel: vi.fn(() => ({ + send: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })), + createOffer: vi.fn(async () => ({ type: 'offer', sdp: 'v=0' })), + setLocalDescription: vi.fn(async () => {}), + onicecandidate: null, + oniceconnectionstatechange: null, + ontrack: null, + })); + + const getUserMedia = vi.fn(async (constraints: MediaStreamConstraints) => { + const v = constraints.video ? [new MockTrack('video') as unknown as MediaStreamTrack] : []; + const a = constraints.audio ? [new MockTrack('audio') as unknown as MediaStreamTrack] : []; + return new MockStream([...v, ...a]) as unknown as MediaStream; + }); + const getDisplayMedia = vi.fn(async () => { + return new MockStream([new MockTrack('video') as unknown as MediaStreamTrack]) as unknown as MediaStream; + }); + // @ts-expect-error mock + global.navigator = Object.defineProperty(global, 'navigator', { + value: { + mediaDevices: { + getUserMedia, + getDisplayMedia, + enumerateDevices: vi.fn(async () => []), + }, + }, + configurable: true, + }); + + // @ts-expect-error mock + global.WebSocket = vi.fn(() => ({ + send: vi.fn(), + close: vi.fn(), + readyState: 1, + addEventListener: vi.fn(), + })); + }); + + it('starts and stops screen+camera overlay without throwing', async () => { + const local = { srcObject: null } as unknown as HTMLVideoElement; + const sdk = new WebRTCClient({ websocketURL: 'ws://x', localVideo: local, mediaConstraints: { audio: true, video: true } }); + // we don’t exercise ws init here + await sdk['media'].initLocalStream(); + await expect(sdk.startScreenWithCameraOverlay()).resolves.toBeUndefined(); + await expect(sdk.stopScreenWithCameraOverlay()).resolves.toBeUndefined(); + }); + + it('enables and disables audio level meter', async () => { + const local = { srcObject: null } as unknown as HTMLVideoElement; + const sdk = new WebRTCClient({ websocketURL: 'ws://x', localVideo: local, mediaConstraints: { audio: true, video: true } }); + await sdk['media'].initLocalStream(); + let last = 0; + await expect(sdk.enableAudioLevelForLocalStream(v => { last = v; }, 50)).resolves.toBeUndefined(); + // let a couple of intervals tick + await new Promise(r => setTimeout(r, 120)); + expect(typeof last).toBe('number'); + sdk.disableAudioLevelForLocalStream(); + }); +}); + + diff --git a/packages/webrtc-sdk/test/ping.test.ts b/packages/webrtc-sdk/test/ping.test.ts new file mode 100644 index 00000000..1c9a2c67 --- /dev/null +++ b/packages/webrtc-sdk/test/ping.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { WebSocketAdaptor } from '../src/core/websocket-adaptor.js'; + +// Test ping without creating a real WebSocket + +describe('websocket ping', () => { + it('sends periodic ping after startPing', async () => { + const sends: any[] = []; + const wsa = new WebSocketAdaptor({ webrtcadaptor: { notifyEventListeners: () => {} } } as any); + // @ts-ignore private + wsa['ws'] = { readyState: 1, send: (t: string) => sends.push(JSON.parse(t)) } as any; + // @ts-ignore private + wsa['connected'] = true; + // @ts-ignore private + wsa['startPing'](); + + await new Promise((r) => setTimeout(r, 3100)); + // @ts-ignore private + wsa['clearPing'](); + + expect(sends.find((m) => m.command === 'ping')).toBeTruthy(); + }); +}); diff --git a/packages/webrtc-sdk/test/reconnect.test.ts b/packages/webrtc-sdk/test/reconnect.test.ts new file mode 100644 index 00000000..724c8021 --- /dev/null +++ b/packages/webrtc-sdk/test/reconnect.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +// mock MediaStream/getUserMedia +try { + Object.defineProperty(globalThis, 'navigator', { + value: { + mediaDevices: { + getUserMedia: async () => { + const ms = new MediaStream() as any; + ms.getTracks = () => []; + ms.getVideoTracks = () => []; + ms.getAudioTracks = () => []; + return ms as MediaStream; + }, + enumerateDevices: async () => [], + }, + }, + configurable: true, + }); +} catch {} + +class MockPC { + localDescription: any; + iceConnectionState = 'connected'; + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + getSenders() { return []; } + addTrack() {} + async createOffer(){ return { type: 'offer' as const, sdp: 'v=0\n' }; } + async setLocalDescription(desc: any) { this.localDescription = desc; } + close() { this.iceConnectionState = 'closed'; } +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; +// @ts-ignore +(global as any).RTCSessionDescription = function(x:any){ return x; }; + +describe('auto reconnect', () => { + it('schedules reconnect on ice disconnected/failed', async () => { + const sent: any[] = []; + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: false, mediaConstraints: { audio: false, video: false }, autoReconnect: true }); + // @ts-ignore + adaptor['ws'] = { send: (t: string) => sent.push(JSON.parse(t)) } as any; + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + await adaptor.publish('s1'); + // Simulate server start to create offer + // @ts-ignore + adaptor['notify']('start', { streamId: 's1' } as any); + await new Promise((r) => setTimeout(r, 0)); + + // Flip state to failed (immediate reconnect path) and trigger + // @ts-ignore + const ctx = adaptor['peers'].get('s1'); + ctx.pc.iceConnectionState = 'failed'; + ctx.pc.oniceconnectionstatechange && ctx.pc.oniceconnectionstatechange(); + + // allow reconnect timers (~500ms + 500ms) + await new Promise((r) => setTimeout(r, 1400)); + + // Should attempt to stop and then publish again -> look for at least another publish after initial + const pubs = sent.filter(m => m.command === 'publish'); + expect(pubs.length).toBeGreaterThanOrEqual(2); + }); +}); + + diff --git a/packages/webrtc-sdk/test/room-assignments.test.ts b/packages/webrtc-sdk/test/room-assignments.test.ts new file mode 100644 index 00000000..ba418288 --- /dev/null +++ b/packages/webrtc-sdk/test/room-assignments.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +describe('room and assignments signaling', () => { + beforeEach(() => { + // @ts-expect-error mock + global.WebSocket = vi.fn(() => ({ send: vi.fn(), close: vi.fn(), readyState: 1 })); + }); + + it('joinRoom and leaveRoom send expected payloads', async () => { + const sdk = new WebRTCClient({ websocketURL: 'ws://x' }); + const sent: string[] = []; + // @ts-expect-error private + sdk.ws = { send: (m: string) => sent.push(m), close: vi.fn() } as any; + + await sdk.joinRoom({ roomId: 'r1', streamId: 's1', role: 'publisher', metaData: { a: 1 } }); + await sdk.leaveRoom('r1', 's1'); + + const cmds = sent.map(s => JSON.parse(s)); + expect(cmds[0].command).toBe('joinRoom'); + expect(cmds[0].room).toBe('r1'); + expect(cmds[1].command).toBe('leaveFromRoom'); + expect(cmds[1].room).toBe('r1'); + }); + + it('assignment signals are formatted correctly', () => { + const sdk = new WebRTCClient({ }); + const sent: string[] = []; + // @ts-expect-error private + sdk.ws = { send: (m: string) => sent.push(m) } as any; + sdk.requestVideoTrackAssignments('main'); + sdk.assignVideoTrack('main', 'trackA', true); + sdk.updateVideoTrackAssignments({ streamId: 'main', offset: 10, size: 5 }); + sdk.setMaxVideoTrackCount('main', 9); + const cmds = sent.map(s => JSON.parse(s).command); + expect(cmds).toEqual([ + 'getVideoTrackAssignmentsCommand', + 'assignVideoTrackCommand', + 'updateVideoTrackAssignmentsCommand', + 'setMaxVideoTrackCountCommand', + ]); + }); +}); + + diff --git a/packages/webrtc-sdk/test/signaling-parity.test.ts b/packages/webrtc-sdk/test/signaling-parity.test.ts new file mode 100644 index 00000000..c82a52ba --- /dev/null +++ b/packages/webrtc-sdk/test/signaling-parity.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +describe('signaling parity sends correct commands', () => { + beforeEach(() => { + // minimal PC mock + // @ts-expect-error mock + global.RTCPeerConnection = vi.fn(() => ({ + addTrack: vi.fn(), + getSenders: vi.fn(() => []), + close: vi.fn(), + onicecandidate: null, + oniceconnectionstatechange: null, + ontrack: null, + createDataChannel: vi.fn(), + })); + }); + + it('toggleVideo/Audio and info APIs send expected payloads', async () => { + const sdk = new WebRTCClient({ mediaConstraints: { audio: true, video: true } }); + const sent: string[] = []; + // inject mock ws + (sdk as unknown as { ws: { send: (m: string) => void } }).ws = { send: (m: string) => sent.push(m) }; + + sdk.toggleVideo('s1', 't1', true); + sdk.toggleAudio('s1', 't1', false); + sdk.getStreamInfo('s1'); + sdk.getBroadcastObject('s1'); + sdk.getRoomInfo('roomA', 's1'); + sdk.getTracks('s1'); + sdk.getSubtracks('s1', 'role', 0, 10); + sdk.getSubtrackCount('s1', 'role', 'active'); + sdk.getSubscriberCount('s1'); + sdk.getSubscriberList('s1', 0, 5); + sdk.peerMessage('s1', 'PING', { x: 1 }); + sdk.requestVideoTrackAssignments('s1'); + sdk.assignVideoTrack('s1', 'vId', true); + sdk.updateVideoTrackAssignments({ streamId: 's1', offset: 0, size: 10 }); + sdk.setMaxVideoTrackCount('s1', 6); + + const cmds = sent.map(s => JSON.parse(s).command); + expect(cmds).toEqual([ + 'toggleVideo', + 'toggleAudio', + 'getStreamInfo', + 'getBroadcastObject', + 'getRoomInfo', + 'getTrackList', + 'getSubtracks', + 'getSubtracksCount', + 'getSubscriberCount', + 'getSubscribers', + 'peerMessageCommand', + 'getVideoTrackAssignmentsCommand', + 'assignVideoTrackCommand', + 'updateVideoTrackAssignmentsCommand', + 'setMaxVideoTrackCountCommand', + ]); + }); +}); + + diff --git a/packages/webrtc-sdk/test/stats-disable-close.test.ts b/packages/webrtc-sdk/test/stats-disable-close.test.ts new file mode 100644 index 00000000..1962618b --- /dev/null +++ b/packages/webrtc-sdk/test/stats-disable-close.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, vi } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +class MockPC { + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + getSenders() { return []; } + addTrack() {} +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; + +describe('disableStats and close', () => { + it('stops stats timer and emits closed on close()', async () => { + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: true }); + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + // Fake ws close + // @ts-ignore + adaptor['ws'] = { close: vi.fn() } as any; + + // install a dummy timer + adaptor.enableStats('s1', 10); + adaptor.disableStats('s1'); + // @ts-ignore check private map + expect((adaptor as any)['__stats_s1']).toBeUndefined(); + + let sawClosed = false; + adaptor.on('closed', () => { sawClosed = true; }); + adaptor.close(); + expect(sawClosed).toBe(true); + }); +}); + + diff --git a/packages/webrtc-sdk/test/stats-parity.test.ts b/packages/webrtc-sdk/test/stats-parity.test.ts new file mode 100644 index 00000000..b4f959c7 --- /dev/null +++ b/packages/webrtc-sdk/test/stats-parity.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +class MockPC { + async getStats() { + const base = Date.now(); + return new Map([ + ['out1', { type: 'outbound-rtp', kind: 'video', bytesSent: 5000, packetsSent: 50, frameWidth: 1280, frameHeight: 720, framesEncoded: 100, timestamp: base }], + ['in1', { type: 'inbound-rtp', kind: 'audio', bytesReceived: 3000, packetsReceived: 30, timestamp: base }], + ['rin1', { type: 'remote-inbound-rtp', kind: 'video', packetsLost: 2, roundTripTime: 0.06, jitter: 0.003 }], + ['trk1', { type: 'track', kind: 'video', frameWidth: 1280, frameHeight: 720, framesDecoded: 90, framesDropped: 3, framesReceived: 95 }], + ['pair', { type: 'candidate-pair', state: 'succeeded', availableOutgoingBitrate: 4000000, currentRoundTripTime: 0.08 }], + ]); + } +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; + +describe('stats parity', () => { + it('collects extended fields similar to v1', async () => { + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: true }); + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + // @ts-ignore + adaptor['peers'].set('s1', { pc: new RTCPeerConnection() as any }); + const ps = await adaptor.getStats('s1'); + expect(ps).toBeTruthy(); + // @ts-ignore + expect(ps.frameWidth).toBe(1280); + // @ts-ignore + expect(ps.videoPacketsSent).toBe(50); + // @ts-ignore + expect(ps.totalBytesSent).toBe(5000); + // @ts-ignore + expect(ps.availableOutgoingBitrateKbps).toBe(4000); + }); +}); + + diff --git a/packages/webrtc-sdk/test/utility.test.ts b/packages/webrtc-sdk/test/utility.test.ts new file mode 100644 index 00000000..5f2436a2 --- /dev/null +++ b/packages/webrtc-sdk/test/utility.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { generateRandomString, getWebSocketURL } from '../src/utils/utility.js'; + +describe('utility', () => { + it('generates string of length', () => { + const s = generateRandomString(8); + expect(s).toHaveLength(8); + }); + it('builds ws URL', () => { + const loc = { + protocol: 'https:', + hostname: 'example.com', + port: '5443', + pathname: '/LiveApp/index.html', + } as unknown as Location; + const url = getWebSocketURL(loc); + expect(url).toBe('wss://example.com:5443/LiveApp/websocket'); + }); +}); diff --git a/packages/webrtc-sdk/test/webrtc-adaptor.play.test.ts b/packages/webrtc-sdk/test/webrtc-adaptor.play.test.ts new file mode 100644 index 00000000..e480fe3d --- /dev/null +++ b/packages/webrtc-sdk/test/webrtc-adaptor.play.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +// Mock PC with answer +class MockPC { + localDescription: any; + iceConnectionState = 'new'; + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + ondatachannel: ((ev: any) => void) | null = null; + getSenders() { return []; } + addTrack() {} + async createAnswer() { return { type: 'answer' as const, sdp: 'v=0\n' }; } + async setLocalDescription(desc: any) { this.localDescription = desc; } + async setRemoteDescription() {} +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; +// @ts-ignore +(global as any).RTCSessionDescription = function (x: any) { return x; }; + + +describe('WebRTCClient play flow', () => { + it('answers on server offer', async () => { + const sent: any[] = []; + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: true }); + // @ts-ignore + adaptor['ws'] = { send: (t: string) => sent.push(JSON.parse(t)) } as any; + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + await adaptor.play('v1'); + + // server sends offer + // @ts-ignore + adaptor['notify']('takeConfiguration', { streamId: 'v1', sdp: 'v=0\n', type: 'offer' } as any); + await new Promise((r) => setTimeout(r, 0)); + + // next should send takeConfiguration answer + const msg = sent.find((m) => m.command === 'takeConfiguration' && m.type === 'answer'); + expect(msg).toBeTruthy(); + expect(msg.streamId).toBe('v1'); + }); +}); diff --git a/packages/webrtc-sdk/test/webrtc-adaptor.publish.test.ts b/packages/webrtc-sdk/test/webrtc-adaptor.publish.test.ts new file mode 100644 index 00000000..b6462c33 --- /dev/null +++ b/packages/webrtc-sdk/test/webrtc-adaptor.publish.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, vi } from 'vitest'; +import { WebRTCClient } from '../src/core/webrtc-client.js'; + +// Provide MediaStream in Node if missing +if (typeof (global as any).MediaStream === 'undefined') { + (global as any).MediaStream = class {} as any; +} + +// mock MediaStream and getUserMedia for Node +try { + Object.defineProperty(globalThis, 'navigator', { + value: { + mediaDevices: { + getUserMedia: vi.fn(async () => { + const ms = new MediaStream() as any; + ms.getTracks = () => []; + ms.getVideoTracks = () => []; + ms.getAudioTracks = () => []; + return ms as MediaStream; + }), + }, + }, + configurable: true, + }); +} catch {} + +// Mock RTCPeerConnection minimal API +class MockPC { + localDescription: any; + iceConnectionState = 'new'; + onicecandidate: ((ev: any) => void) | null = null; + oniceconnectionstatechange: (() => void) | null = null; + getSenders() { return []; } + addTrack() {} + async createOffer() { return { type: 'offer' as const, sdp: 'v=0\n' }; } + async setLocalDescription(desc: any) { this.localDescription = desc; } +} +// @ts-ignore +(global as any).RTCPeerConnection = MockPC; +// @ts-ignore +(global as any).RTCSessionDescription = function (x: any) { return x; }; + +// Mock WebSocketAdaptor inside instance by monkey patching send + +describe('WebRTCClient publish flow', () => { + it('sends publish then takeConfiguration after start', async () => { + const sent: any[] = []; + const adaptor = new WebRTCClient({ websocketURL: 'wss://x', isPlayMode: false, mediaConstraints: { video: false, audio: false } }); + // @ts-ignore access private + adaptor['ws'] = { send: (t: string) => sent.push(JSON.parse(t)) } as any; + + // Simulate initialized + // @ts-ignore + adaptor['notify']('initialized', undefined as any); + + await adaptor.publish('s1'); + // find first publish ignoring the initial getIceServerConfig + const firstPublish = sent.find((m) => m.command === 'publish'); + expect(firstPublish).toBeTruthy(); + expect(firstPublish.streamId).toBe('s1'); + + // Simulate server start + // @ts-ignore + adaptor['notify']('start', { streamId: 's1' } as any); + + // wait a tick for async offer path + await new Promise((r) => setTimeout(r, 0)); + + // takeConfiguration should be sent next + const msg = sent.find((m) => m.command === 'takeConfiguration'); + expect(msg).toBeTruthy(); + expect(msg.streamId).toBe('s1'); + expect(msg.type).toBe('offer'); + }); +}); diff --git a/packages/webrtc-sdk/tsconfig.json b/packages/webrtc-sdk/tsconfig.json new file mode 100644 index 00000000..2ccfbe47 --- /dev/null +++ b/packages/webrtc-sdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "ES2020", + "moduleResolution": "Bundler", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "types": ["vitest/globals", "node"] + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} \ No newline at end of file diff --git a/packages/webrtc-sdk/typedoc.json b/packages/webrtc-sdk/typedoc.json new file mode 100644 index 00000000..55e9da1e --- /dev/null +++ b/packages/webrtc-sdk/typedoc.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"], + "out": "docs", + "excludeExternals": true, + "excludePrivate": true, + "excludeProtected": false, + "hideGenerator": true, + "exclude": [ + "**/examples/**", + "**/test/**", + "**/*.test.ts" + ] +} \ No newline at end of file diff --git a/redeploy.sh b/redeploy.sh index 2d3b8e49..0bcda680 100755 --- a/redeploy.sh +++ b/redeploy.sh @@ -2,6 +2,16 @@ AMS_DIR=~/softwares/ant-media-server +# Build WebRTC SDK so its artifacts can be included in the WAR +cd packages/webrtc-sdk +rm -rf dist +npm run build +OUT=$? +if [ $OUT -ne 0 ]; then + exit $OUT +fi +cd ../.. + #Latest sdk is to be deployed to src/main/webapp rm -rf dist npm run compile diff --git a/rollup.config.browser.cjs b/rollup.config.browser.cjs index 106b8f6f..4f6dcd12 100644 --- a/rollup.config.browser.cjs +++ b/rollup.config.browser.cjs @@ -1,16 +1,22 @@ - const babel = require('@rollup/plugin-babel').default; const builds = { - input: [ 'src/main/js/index.js'], - output: [{ - name: 'webrtc_adaptor', - file: 'dist/browser/webrtc_adaptor.js', - format: 'umd' - }, - ], - plugins: [babel({ babelHelpers: 'bundled' })] + input: ['src/main/js/index.js'], + output: [{ + name: 'webrtc_adaptor', + file: 'dist/browser/webrtc_adaptor.js', + format: 'umd', + globals: { + '@mediapipe/selfie_segmentation': 'SelfieSegmentation' + } + }], + external: ['@mediapipe/selfie_segmentation'], + plugins: [babel({ babelHelpers: 'bundled' })], + onwarn(warning, warn) { + if (warning.code === 'THIS_IS_UNDEFINED') return; + warn(warning); + } }; -module.exports = builds \ No newline at end of file +module.exports = builds; diff --git a/rollup.config.module.cjs b/rollup.config.module.cjs index 96110b84..33f3e4e4 100644 --- a/rollup.config.module.cjs +++ b/rollup.config.module.cjs @@ -1,39 +1,42 @@ - const babel = require('@rollup/plugin-babel').default; const nodeResolve = require('@rollup/plugin-node-resolve').default; const commonjs = require('@rollup/plugin-commonjs').default; const css = require("rollup-plugin-import-css"); - - const builds = { - input: [ 'src/main/js/index.js', - 'src/main/js/webrtc_adaptor.js', - 'src/main/js/fetch.stream.js', - 'src/main/js/video-effect.js', - 'src/main/js/soundmeter.js', - 'src/main/js/volume-meter-processor.js', - 'src/main/js/external/loglevel.min.js', - 'src/main/js/utility.js', - 'src/main/js/media_manager.js', - 'src/main/js/stream_merger.js', - ], - output: [{ - dir: 'dist', - format: 'cjs' - }, - { - dir: 'dist/es', - format: 'es' - } - ], - plugins: [ - babel({ babelHelpers: 'bundled' }), - nodeResolve(), - commonjs(), - css() - ] + input: [ + 'src/main/js/index.js', + 'src/main/js/webrtc_adaptor.js', + 'src/main/js/fetch.stream.js', + 'src/main/js/video-effect.js', + 'src/main/js/soundmeter.js', + 'src/main/js/volume-meter-processor.js', + 'src/main/js/external/loglevel.min.js', + 'src/main/js/utility.js', + 'src/main/js/media_manager.js', + 'src/main/js/stream_merger.js', + ], + output: [ + { + dir: 'dist', + format: 'cjs' + }, + { + dir: 'dist/es', + format: 'es' + } + ], + plugins: [ + babel({ babelHelpers: 'bundled' }), + nodeResolve(), + commonjs(), + css() + ], + onwarn(warning, warn) { + if (warning.code === 'THIS_IS_UNDEFINED') return; + warn(warning); + } }; -module.exports = builds +module.exports = builds; diff --git a/src/main/webapp/.gitignore b/src/main/webapp/.gitignore new file mode 100644 index 00000000..bcd8413e --- /dev/null +++ b/src/main/webapp/.gitignore @@ -0,0 +1,2 @@ +*-v2.html +v2/ \ No newline at end of file diff --git a/src/main/webapp/js/.gitignore b/src/main/webapp/js/.gitignore index bc943b40..854b98cd 100644 --- a/src/main/webapp/js/.gitignore +++ b/src/main/webapp/js/.gitignore @@ -1,2 +1,6 @@ /*.js /*.ts +/client/* +/core/* +/utils/* +index.d.ts.map \ No newline at end of file