zero-query 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -442
- package/cli/commands/build.js +254 -247
- package/cli/commands/bundle.js +1228 -1224
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -220
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +661 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6614
- package/dist/zquery.min.js +8 -631
- package/index.d.ts +570 -371
- package/index.js +311 -240
- package/package.json +76 -70
- package/src/component.js +1709 -1691
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -255
- package/src/router.js +843 -843
- package/src/ssr.js +418 -418
- package/src/store.js +318 -318
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1103
- package/tests/compare.test.js +497 -486
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -489
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -1650
- package/tests/electron-features.test.js +864 -864
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -146
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
package/dist/API.md
CHANGED
|
@@ -169,6 +169,23 @@ Complete API documentation for every module, method, option, and type in zQuery.
|
|
|
169
169
|
- [Environment Properties](#environment-properties)
|
|
170
170
|
- [Platform Detection](#platform-detection)
|
|
171
171
|
- [Quick Reference](#quick-reference)
|
|
172
|
+
- [WebRTC](#webrtc)
|
|
173
|
+
- [Overview](#overview)
|
|
174
|
+
- [Surface Status](#surface-status)
|
|
175
|
+
- [Quick Start](#quick-start)
|
|
176
|
+
- [SignalingClient](#signalingclient)
|
|
177
|
+
- [Peer (Perfect Negotiation)](#peer-perfect-negotiation)
|
|
178
|
+
- [Room](#room)
|
|
179
|
+
- [Reactive Composables](#reactive-composables)
|
|
180
|
+
- [z-stream Directive](#z-stream-directive)
|
|
181
|
+
- [TURN Credentials](#turn-credentials)
|
|
182
|
+
- [End-to-End Encryption (SFrame)](#end-to-end-encryption-sframe)
|
|
183
|
+
- [SFU Adapters](#sfu-adapters)
|
|
184
|
+
- [Join Tokens](#join-tokens)
|
|
185
|
+
- [Observability (getStats)](#observability-getstats)
|
|
186
|
+
- [SDP + ICE Helpers](#sdp-ice-helpers)
|
|
187
|
+
- [Error Family](#error-family)
|
|
188
|
+
- [Wire Protocol](#wire-protocol)
|
|
172
189
|
|
|
173
190
|
---
|
|
174
191
|
|
|
@@ -6517,6 +6534,609 @@ const API_BASE = $.platform === 'electron'
|
|
|
6517
6534
|
|
|
6518
6535
|
---
|
|
6519
6536
|
|
|
6537
|
+
## WebRTC
|
|
6538
|
+
|
|
6539
|
+
|
|
6540
|
+
zQuery ships a complete WebRTC client that speaks the wire protocol of `@zero-server/webrtc` — from a low-level `SignalingClient` + perfect-negotiation `Peer` up to a reactive multi-peer `Room` with composables, TURN credential rotation, SFrame end-to-end encryption, mediasoup & LiveKit SFU adapters, join-token decoding, and `getStats()` observability. The `z-stream` directive binds remote `MediaStream`s straight to `` / `` elements — no `URL.createObjectURL` dance.
|
|
6541
|
+
|
|
6542
|
+
|
|
6543
|
+
### Overview
|
|
6544
|
+
|
|
6545
|
+
|
|
6546
|
+
The WebRTC surface is layered — pick the level you need:
|
|
6547
|
+
|
|
6548
|
+
|
|
6549
|
+
| Layer | API | Use when… |
|
|
6550
|
+
| --- | --- | --- |
|
|
6551
|
+
| **Raw** | `SignalingClient`, `Peer` | You want full control over JSEP and trickle. |
|
|
6552
|
+
| **Mid-level** | `Room`, `$.webrtc.join(url, opts)` | You need a multi-peer container with publish / data channels. |
|
|
6553
|
+
| **Reactive** | `useRoom`, `usePeer`, `useTracks`, `useDataChannel`, `useConnectionQuality` | You want to wire room state straight into components. |
|
|
6554
|
+
| **Pluggable** | `loadSfuAdapter('mediasoup' \| 'livekit')` | You're routing through an SFU instead of mesh. |
|
|
6555
|
+
| **Hardening** | `SFrameContext`, `attachE2ee`, TURN refresher | You need E2EE and rotating TURN credentials. |
|
|
6556
|
+
|
|
6557
|
+
|
|
6558
|
+
> **Tip:** Want a working starting point? Run `npx zero-query create my-app --webrtc-demo` (alias `-w`) to scaffold a one-page video room with local + remote tiles, mic/cam controls, and a status overlay.
|
|
6559
|
+
|
|
6560
|
+
|
|
6561
|
+
### Surface Status
|
|
6562
|
+
|
|
6563
|
+
|
|
6564
|
+
Everything in the table below is shipping today. Green dots mean stable + tested, blue dots mark optional peer-dependency surfaces (you install the dep yourself).
|
|
6565
|
+
|
|
6566
|
+
|
|
6567
|
+
| Surface | Status | Notes |
|
|
6568
|
+
| --- | --- | --- |
|
|
6569
|
+
| `SignalingClient` | Shipping | Exponential-backoff reconnect, coalesced ICE trickle |
|
|
6570
|
+
| `Peer` | Shipping | Perfect-negotiation polite/impolite collision handling |
|
|
6571
|
+
| `Room` / `$.webrtc.join()` | Shipping | Mesh topology with reactive `peers` map |
|
|
6572
|
+
| `useRoom` / `usePeer` / `useTracks` / `useDataChannel` / `useConnectionQuality` | Shipping | All return disposable handles |
|
|
6573
|
+
| `z-stream` directive | Shipping | SSR-safe; writes `srcObject` directly |
|
|
6574
|
+
| TURN client (`fetchTurnCredentials`, `mergeIceServers`, `createTurnRefresher`) | Shipping | Auto-refresh before TTL |
|
|
6575
|
+
| SFrame E2EE (`SFrameContext`, `attachE2ee`) | Shipping | AES-GCM-128 with epoch rotation |
|
|
6576
|
+
| `loadSfuAdapter('mediasoup')` | Shipping | Install `mediasoup-client` yourself |
|
|
6577
|
+
| `loadSfuAdapter('livekit')` | Shipping | Install `livekit-client` yourself |
|
|
6578
|
+
| `decodeJoinToken` / `isJoinTokenExpired` | Shipping | UX-only — server re-validates every join |
|
|
6579
|
+
| `samplePeerStats` / `createStatsSampler` / `classifyStats` | Shipping | `good` / `fair` / `poor` buckets |
|
|
6580
|
+
| SDP + ICE helpers (`parseSdp`, `validateSdp`, `parseCandidate`, `filterCandidates`) | Shipping | Zero-dep, isomorphic |
|
|
6581
|
+
| `WebRtcError` family (Signaling / Ice / Sdp / Turn / E2ee / Sfu) | Shipping | Stable string codes; flows through `$.onError` |
|
|
6582
|
+
|
|
6583
|
+
|
|
6584
|
+
### Quick Start
|
|
6585
|
+
|
|
6586
|
+
|
|
6587
|
+
Join a room and render every remote peer's video in three steps:
|
|
6588
|
+
|
|
6589
|
+
|
|
6590
|
+
|
|
6591
|
+
```javascript
|
|
6592
|
+
// 1) Get local media
|
|
6593
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
|
6594
|
+
|
|
6595
|
+
// 2) Join the room (opens signaling, completes handshake)
|
|
6596
|
+
const room = await $.webrtc.join('wss://api.example.com/rtc', {
|
|
6597
|
+
room: 'lobby',
|
|
6598
|
+
user: { id: 'me' },
|
|
6599
|
+
tracks: stream.getTracks(),
|
|
6600
|
+
});
|
|
6601
|
+
|
|
6602
|
+
// 3) Render each remote peer with the z-stream directive
|
|
6603
|
+
$.component('video-room', {
|
|
6604
|
+
state: () => ({ peers: [] }),
|
|
6605
|
+
mounted() {
|
|
6606
|
+
this._unsub = room.peers.subscribe((map) => {
|
|
6607
|
+
this.state.peers = [...map.values()];
|
|
6608
|
+
});
|
|
6609
|
+
},
|
|
6610
|
+
destroyed() { this._unsub?.(); room.leave(); },
|
|
6611
|
+
render() {
|
|
6612
|
+
return \`
|
|
6613
|
+
<video z-stream="local" autoplay muted playsinline></video>
|
|
6614
|
+
<div class="tiles">
|
|
6615
|
+
<video z-for="p in peers" z-stream="p.stream" autoplay playsinline></video>
|
|
6616
|
+
</div>
|
|
6617
|
+
\`;
|
|
6618
|
+
},
|
|
6619
|
+
});
|
|
6620
|
+
```
|
|
6621
|
+
|
|
6622
|
+
|
|
6623
|
+
> **Tip:** `$.webrtc.join()` is the one-liner that bundles `new SignalingClient(…)` + `connect()` + `send('join')` + `Room` wiring. Use it whenever you don't need raw control of the JSEP loop.
|
|
6624
|
+
|
|
6625
|
+
|
|
6626
|
+
### SignalingClient
|
|
6627
|
+
|
|
6628
|
+
|
|
6629
|
+
Lightweight WebSocket client that handles connect, exponential-backoff reconnect, the server's initial `hello` handshake, and outbound ICE coalescing (so trickle bursts don't trip the server's per-peer rate cap).
|
|
6630
|
+
|
|
6631
|
+
|
|
6632
|
+
|
|
6633
|
+
```javascript
|
|
6634
|
+
import { SignalingClient } from 'zero-query';
|
|
6635
|
+
|
|
6636
|
+
const client = new SignalingClient('wss://api.example.com/rtc', {
|
|
6637
|
+
// Exponential backoff between reconnect attempts (defaults shown)
|
|
6638
|
+
reconnect: { baseMs: 250, capMs: 8000, maxRetries: 10 },
|
|
6639
|
+
// ICE coalescing window: at most 10 frames per 200ms
|
|
6640
|
+
iceFlushMs: 200,
|
|
6641
|
+
iceBatch: 10,
|
|
6642
|
+
});
|
|
6643
|
+
|
|
6644
|
+
client.on('hello', ({ peerId }) => console.log('I am', peerId));
|
|
6645
|
+
client.on('joined', ({ room, peers }) => console.log(room, peers));
|
|
6646
|
+
client.on('peer-joined', ({ id }) => console.log('peer-joined', id));
|
|
6647
|
+
client.on('offer', ({ from, sdp }) => /* accept + answer */);
|
|
6648
|
+
client.on('ice', ({ from, candidate }) => /* addIceCandidate */);
|
|
6649
|
+
client.on('error', (err) => console.warn(err.code, err.message));
|
|
6650
|
+
|
|
6651
|
+
await client.connect();
|
|
6652
|
+
client.send('join', { room: 'lobby' });
|
|
6653
|
+
// trickle - auto-batched into safe-rate windows
|
|
6654
|
+
client.send('ice', { to: 'peer-x', candidate: '...' });
|
|
6655
|
+
client.close();
|
|
6656
|
+
```
|
|
6657
|
+
|
|
6658
|
+
|
|
6659
|
+
| Member | Type | Description |
|
|
6660
|
+
| --- | --- | --- |
|
|
6661
|
+
| `new SignalingClient(url, opts?)` | | Construct (does not open the socket) |
|
|
6662
|
+
| `.connect()` | `Promise` | Open the socket; resolves on first `open` |
|
|
6663
|
+
| `.send(type, payload?)` | `void` | Send a frame; `ice` frames are coalesced |
|
|
6664
|
+
| `.on(type, cb)` | `() => void` | Subscribe to a server frame type or lifecycle event (`open`, `close`, `reconnect`, `error`) |
|
|
6665
|
+
| `.off(type, cb)` | `void` | Unsubscribe |
|
|
6666
|
+
| `.close()` | `void` | Send `bye` and stop reconnecting |
|
|
6667
|
+
| `.peerId` | `string \| null` | Server-assigned peer id (set after first `hello`) |
|
|
6668
|
+
| `.connected` | `boolean` | `true` while the underlying WebSocket is open |
|
|
6669
|
+
|
|
6670
|
+
|
|
6671
|
+
> All signaling errors derive from `SignalingError`: `ZQ_WEBRTC_SIGNALING_BAD_URL`, `ZQ_WEBRTC_SIGNALING_BAD_HANDSHAKE`, `ZQ_WEBRTC_SIGNALING_BAD_FRAME`, `ZQ_WEBRTC_SIGNALING_NOT_CONNECTED`, `ZQ_WEBRTC_SIGNALING_CLOSED`.
|
|
6672
|
+
|
|
6673
|
+
|
|
6674
|
+
### Peer (Perfect Negotiation)
|
|
6675
|
+
|
|
6676
|
+
|
|
6677
|
+
The `Peer` class wraps a single `RTCPeerConnection` and routes JSEP messages through a shared `SignalingClient` for one remote peer. It implements the W3C **perfect-negotiation** pattern, so simultaneous `negotiationneeded` events on both ends resolve deterministically based on the locally-assigned `polite` flag — no glare, no manual rollback.
|
|
6678
|
+
|
|
6679
|
+
|
|
6680
|
+
|
|
6681
|
+
```javascript
|
|
6682
|
+
import { SignalingClient, Peer } from 'zero-query';
|
|
6683
|
+
|
|
6684
|
+
const signaling = new SignalingClient('wss://api.example.com/rtc');
|
|
6685
|
+
await signaling.connect();
|
|
6686
|
+
signaling.send('join', { room: 'lobby' });
|
|
6687
|
+
|
|
6688
|
+
signaling.on('peer-joined', ({ id }) => {
|
|
6689
|
+
// Decide polite/impolite however you like - lexicographic id is fine
|
|
6690
|
+
const polite = signaling.peerId < id;
|
|
6691
|
+
const peer = new Peer(id, signaling, {
|
|
6692
|
+
polite,
|
|
6693
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
6694
|
+
});
|
|
6695
|
+
|
|
6696
|
+
peer.on('track', (ev) => attachToVideoEl(ev.streams[0]));
|
|
6697
|
+
peer.on('connectionstatechange', (state) => console.log(state));
|
|
6698
|
+
peer.on('error', (err) => console.warn(err.code, err.message));
|
|
6699
|
+
|
|
6700
|
+
// Push local media
|
|
6701
|
+
for (const track of localStream.getTracks()) {
|
|
6702
|
+
peer.addTrack(track, localStream);
|
|
6703
|
+
}
|
|
6704
|
+
});
|
|
6705
|
+
|
|
6706
|
+
signaling.on('peer-left', ({ id }) => peers.get(id)?.close());
|
|
6707
|
+
```
|
|
6708
|
+
|
|
6709
|
+
|
|
6710
|
+
| Member | Type | Description |
|
|
6711
|
+
| --- | --- | --- |
|
|
6712
|
+
| `new Peer(peerId, signaling, opts?)` | | Construct (creates the underlying `RTCPeerConnection` eagerly) |
|
|
6713
|
+
| `opts.polite` | `boolean` | Perfect-negotiation polite flag. Polite peers yield on collision |
|
|
6714
|
+
| `opts.iceServers` | `RTCIceServer[]` | STUN/TURN servers forwarded to `RTCPeerConnection` |
|
|
6715
|
+
| `opts.maxIceCandidates` | `number` | Hard cap on trickled candidates per peer (default `30`, matching server SDP cap) |
|
|
6716
|
+
| `.addTrack(track, ...streams)` | `RTCRtpSender` | Add a local track. Triggers `negotiationneeded` |
|
|
6717
|
+
| `.createDataChannel(label, init?)` | `RTCDataChannel` | Open a data channel; remote peer observes a `datachannel` event |
|
|
6718
|
+
| `.restartIce()` | `void` | Force ICE restart (also fires automatically on `connectionState = "failed"`) |
|
|
6719
|
+
| `.on(event, cb)` | `() => void` | Subscribe to `track` / `datachannel` / `connectionstatechange` / `close` / `error` |
|
|
6720
|
+
| `.close()` | `void` | Close the underlying connection. Idempotent |
|
|
6721
|
+
| `.pc` | `RTCPeerConnection` | Escape hatch for direct access (stats, sender params, etc.) |
|
|
6722
|
+
|
|
6723
|
+
|
|
6724
|
+
> **Tip:** mDNS (`*.local`) candidates are filtered before send, and trickled candidates are capped per-peer so we stay inside the server's `a=candidate:` ceiling.
|
|
6725
|
+
|
|
6726
|
+
|
|
6727
|
+
### Room
|
|
6728
|
+
|
|
6729
|
+
|
|
6730
|
+
A `Room` is the multi-peer container above `SignalingClient` + `Peer`. It tracks every remote peer in a reactive `Signal`, fans out local tracks via `publish()` / `unpublish()`, and multiplexes named data channels across every connected peer. `$.webrtc.join(url, opts)` opens the signaling socket, waits for `hello`, sends `join`, and resolves once the server returns `joined`.
|
|
6731
|
+
|
|
6732
|
+
|
|
6733
|
+
|
|
6734
|
+
```javascript
|
|
6735
|
+
import { join } from 'zero-query';
|
|
6736
|
+
|
|
6737
|
+
const room = await $.webrtc.join('wss://api.example.com/rtc', {
|
|
6738
|
+
room: 'lobby',
|
|
6739
|
+
media: { audio: true, video: true }, // calls getUserMedia automatically
|
|
6740
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
6741
|
+
});
|
|
6742
|
+
|
|
6743
|
+
room.on('peer-joined', (info) => console.log('joined', info.id));
|
|
6744
|
+
room.on('peer-left', (info) => console.log('left', info.id));
|
|
6745
|
+
room.on('error', (err) => console.warn(err.code, err.message));
|
|
6746
|
+
|
|
6747
|
+
// Publish a stream later (e.g. after a screen-share button):
|
|
6748
|
+
const screen = await navigator.mediaDevices.getDisplayMedia();
|
|
6749
|
+
await room.publish(screen);
|
|
6750
|
+
|
|
6751
|
+
// Open a chat channel that every peer joins:
|
|
6752
|
+
const chat = room.dataChannel('chat');
|
|
6753
|
+
chat.on('message', (text, fromPeerId) => console.log(fromPeerId, text));
|
|
6754
|
+
chat.send('hello room');
|
|
6755
|
+
|
|
6756
|
+
await room.leave();
|
|
6757
|
+
```
|
|
6758
|
+
|
|
6759
|
+
|
|
6760
|
+
| Member | Type | Description |
|
|
6761
|
+
| --- | --- | --- |
|
|
6762
|
+
| `room.id` | `string` | Room identifier passed to `join` |
|
|
6763
|
+
| `room.self` | `string` | Local peer id assigned by the server |
|
|
6764
|
+
| `room.peers` | `Signal>` | Reactive map of every remote peer in the room |
|
|
6765
|
+
| `room.localTracks` | `Signal` | Reactive snapshot of currently-published local tracks |
|
|
6766
|
+
| `room.publish(stream)` | `Promise` | Add every track in the stream to every peer |
|
|
6767
|
+
| `room.unpublish(stream)` | `Promise` | Remove every previously-published track in the stream |
|
|
6768
|
+
| `room.dataChannel(label, opts?)` | `RoomDataChannel` | Multiplexed handle `{ label, send, on, close }`. Same label returns the same handle |
|
|
6769
|
+
| `room.on(event, cb)` | `() => void` | Subscribe to `peer-joined` / `peer-left` / `mute` / `unmute` / `error` |
|
|
6770
|
+
| `room.leave()` | `Promise` | Send `leave`, close every peer and the socket. Idempotent |
|
|
6771
|
+
|
|
6772
|
+
|
|
6773
|
+
### Reactive Composables
|
|
6774
|
+
|
|
6775
|
+
|
|
6776
|
+
Each composable returns a disposable reactive handle — `{ value, peek, subscribe(cb), dispose() }` — that you can render with `z-text` / `z-html` or wire into component re-renders. Call `dispose()` in your unmount path so listeners and intervals are torn down.
|
|
6777
|
+
|
|
6778
|
+
|
|
6779
|
+
|
|
6780
|
+
```javascript
|
|
6781
|
+
import { useRoom, usePeer, useTracks, useDataChannel, useConnectionQuality } from 'zero-query';
|
|
6782
|
+
|
|
6783
|
+
const room = await $.useRoom('wss://api.example.com/rtc', { room: 'lobby' });
|
|
6784
|
+
|
|
6785
|
+
// Track a specific remote peer reactively:
|
|
6786
|
+
const peerHandle = $.usePeer(room, 'peer-abc');
|
|
6787
|
+
peerHandle.subscribe((info) => {
|
|
6788
|
+
if (!info) return console.log('peer gone');
|
|
6789
|
+
console.log('connection:', info.connection);
|
|
6790
|
+
});
|
|
6791
|
+
|
|
6792
|
+
// Live track list for that peer (re-emits on addtrack/removetrack):
|
|
6793
|
+
const tracksHandle = $.useTracks(peerHandle.peek());
|
|
6794
|
+
|
|
6795
|
+
// A chat channel with bounded message history:
|
|
6796
|
+
const chat = $.useDataChannel(room, 'chat', { history: 200 });
|
|
6797
|
+
chat.send('hi');
|
|
6798
|
+
chat.messages.subscribe((entries) => render(entries)); // [{data, from, at}]
|
|
6799
|
+
|
|
6800
|
+
// Sampled connection-quality bucket from RTCPeerConnection.getStats():
|
|
6801
|
+
const quality = $.useConnectionQuality(peerHandle.peek(), { intervalMs: 2000 });
|
|
6802
|
+
quality.subscribe((bucket) => console.log(bucket)); // 'good' | 'fair' | 'poor'
|
|
6803
|
+
|
|
6804
|
+
// On unmount:
|
|
6805
|
+
peerHandle.dispose();
|
|
6806
|
+
tracksHandle.dispose();
|
|
6807
|
+
chat.dispose();
|
|
6808
|
+
quality.dispose();
|
|
6809
|
+
```
|
|
6810
|
+
|
|
6811
|
+
|
|
6812
|
+
| Composable | Returns |
|
|
6813
|
+
| --- | --- |
|
|
6814
|
+
| `useRoom(urlOrRoom, opts?)` | `Promise` — resolves an existing Room or calls `join` |
|
|
6815
|
+
| `usePeer(room, peerId)` | `{ value: PeerInfo \| null, peek, subscribe, dispose }` |
|
|
6816
|
+
| `useTracks(peerInfo)` | `{ value: MediaStreamTrack[], refresh, peek, subscribe, dispose }` |
|
|
6817
|
+
| `useDataChannel(room, label, { history? })` | `{ messages, send, close, dispose }` |
|
|
6818
|
+
| `useConnectionQuality(peerInfo, { intervalMs?, getStats? })` | `{ value: "good" \| "fair" \| "poor", peek, subscribe, dispose }` |
|
|
6819
|
+
|
|
6820
|
+
|
|
6821
|
+
> **Warning:** Forgetting `dispose()` on unmount leaks getStats intervals and signal subscriptions — `useConnectionQuality` in particular keeps a 2-second timer alive until disposed.
|
|
6822
|
+
|
|
6823
|
+
|
|
6824
|
+
### z-stream Directive
|
|
6825
|
+
|
|
6826
|
+
|
|
6827
|
+
Bind a reactive `MediaStream` (or any object exposing `getTracks()`) to a `` or `` element by setting `srcObject` directly — no `URL.createObjectURL` dance. SSR-safe and writes `null` when the expression is nullish.
|
|
6828
|
+
|
|
6829
|
+
|
|
6830
|
+
|
|
6831
|
+
```javascript
|
|
6832
|
+
import { component } from 'zero-query';
|
|
6833
|
+
|
|
6834
|
+
component('peer-video', {
|
|
6835
|
+
state: { stream: null },
|
|
6836
|
+
template: () => \`<video z-stream="stream" autoplay playsinline muted></video>\`,
|
|
6837
|
+
});
|
|
6838
|
+
|
|
6839
|
+
// Anywhere you have a PeerInfo (from room.peers or usePeer):
|
|
6840
|
+
const tile = $('peer-video').first();
|
|
6841
|
+
tile.instance.state.stream = peerInfo.stream;
|
|
6842
|
+
```
|
|
6843
|
+
|
|
6844
|
+
|
|
6845
|
+
> **Tip:** `z-stream` tolerates plain `MediaStream`s, anything with `.stream`, or any object that exposes `getTracks()`. Reassign the expression to `null` to detach without removing the element.
|
|
6846
|
+
|
|
6847
|
+
|
|
6848
|
+
### TURN Credentials
|
|
6849
|
+
|
|
6850
|
+
|
|
6851
|
+
The backend's `issueTurnCredentials()` handler returns short-lived `{ username, credential, urls, ttl }` bundles. `fetchTurnCredentials()` normalizes the response and validates its shape; `mergeIceServers()` concatenates the TURN entry with your existing STUN list (deduping any overlapping URLs); `createTurnRefresher()` schedules an automatic refetch ahead of expiry so a long-running room never sees a 401 mid-call. Any failure surfaces as a typed `TurnError`.
|
|
6852
|
+
|
|
6853
|
+
|
|
6854
|
+
|
|
6855
|
+
```javascript
|
|
6856
|
+
import { fetchTurnCredentials, mergeIceServers, createTurnRefresher } from 'zero-query';
|
|
6857
|
+
|
|
6858
|
+
// One-shot fetch (e.g. right before $.webrtc.join):
|
|
6859
|
+
const creds = await $.fetchTurnCredentials('/webrtc/turn-credentials');
|
|
6860
|
+
const iceServers = $.mergeIceServers(
|
|
6861
|
+
[{ urls: 'stun:stun.l.google.com:19302' }],
|
|
6862
|
+
creds
|
|
6863
|
+
);
|
|
6864
|
+
|
|
6865
|
+
const room = await $.webrtc.join('wss://api.example.com/rtc', {
|
|
6866
|
+
room: 'lobby',
|
|
6867
|
+
iceServers,
|
|
6868
|
+
});
|
|
6869
|
+
|
|
6870
|
+
// Long-lived refresher (re-fetches ~30s before ttl expires):
|
|
6871
|
+
const turn = $.createTurnRefresher({
|
|
6872
|
+
url: '/webrtc/turn-credentials',
|
|
6873
|
+
leadMs: 30000,
|
|
6874
|
+
onRefresh: (next) => console.log('TURN rotated', next.ttl),
|
|
6875
|
+
onError: (err) => console.warn('TURN refresh failed', err.code),
|
|
6876
|
+
});
|
|
6877
|
+
await turn.start();
|
|
6878
|
+
console.log(turn.value); // latest credentials
|
|
6879
|
+
turn.stop(); // cancel timer on unmount
|
|
6880
|
+
```
|
|
6881
|
+
|
|
6882
|
+
|
|
6883
|
+
| Helper | Returns |
|
|
6884
|
+
| --- | --- |
|
|
6885
|
+
| `fetchTurnCredentials(url, { fetch?, ...RequestInit })` | `Promise` |
|
|
6886
|
+
| `mergeIceServers(base?, turn?)` | `RTCIceServer[]` — base entries first, TURN appended, dup URLs dropped |
|
|
6887
|
+
| `createTurnRefresher({ url, fetch?, leadMs?, minIntervalMs?, onRefresh?, onError?, requestInit? })` | `{ value, peek, start, refresh, stop }` |
|
|
6888
|
+
|
|
6889
|
+
|
|
6890
|
+
> All TURN errors derive from `TurnError`: `ZQ_WEBRTC_TURN_BAD_URL`, `ZQ_WEBRTC_TURN_NO_FETCH`, `ZQ_WEBRTC_TURN_NETWORK`, `ZQ_WEBRTC_TURN_HTTP`, `ZQ_WEBRTC_TURN_BAD_JSON`, `ZQ_WEBRTC_TURN_BAD_BODY`.
|
|
6891
|
+
|
|
6892
|
+
|
|
6893
|
+
### End-to-End Encryption (SFrame)
|
|
6894
|
+
|
|
6895
|
+
|
|
6896
|
+
Optional per-frame AES-GCM-128 encryption that runs after the encoder and before the decoder via `RTCRtpSender.createEncodedStreams()`. Keys are bound to a 1-byte epoch so a room can rotate without dropping in-flight frames; up to `maxEpochs` (default 4) past keys are retained for late decryptors. Each frame is laid out as `[1-byte epoch][12-byte IV][ciphertext + GCM tag]`.
|
|
6897
|
+
|
|
6898
|
+
|
|
6899
|
+
|
|
6900
|
+
```javascript
|
|
6901
|
+
import { deriveSFrameKey, SFrameContext, attachE2ee, join } from 'zero-query/webrtc';
|
|
6902
|
+
|
|
6903
|
+
// 1) Derive a key everyone in the room shares (passphrase out-of-band).
|
|
6904
|
+
const key = await deriveSFrameKey('correct horse battery', 'room-42');
|
|
6905
|
+
|
|
6906
|
+
// 2) Track it under epoch 0.
|
|
6907
|
+
const ctx = new SFrameContext();
|
|
6908
|
+
ctx.setKey(0, key);
|
|
6909
|
+
|
|
6910
|
+
// 3) Join, then wire transforms onto every sender + receiver.
|
|
6911
|
+
const room = await join('wss://example.com/ws', { room: 'room-42', /* ... */ });
|
|
6912
|
+
room.on('peer', (peer) => {
|
|
6913
|
+
const handle = attachE2ee(peer.pc, ctx);
|
|
6914
|
+
peer.on('track', () => handle.refresh()); // re-walk for late tracks
|
|
6915
|
+
});
|
|
6916
|
+
|
|
6917
|
+
// 4) Rotate by installing a new key under the next epoch.
|
|
6918
|
+
ctx.setKey(1, await deriveSFrameKey('correct horse battery v2', 'room-42'));
|
|
6919
|
+
```
|
|
6920
|
+
|
|
6921
|
+
|
|
6922
|
+
| Helper | Result |
|
|
6923
|
+
| --- | --- |
|
|
6924
|
+
| `deriveSFrameKey(passphrase, salt)` | `Promise` — PBKDF2-SHA256 (100k) → HKDF-SHA256 → AES-GCM-128 |
|
|
6925
|
+
| `generateSFrameKey()` | `Promise` — random AES-GCM-128 |
|
|
6926
|
+
| `new SFrameContext({ maxEpochs? })` | Tracks `epoch → key`; `setKey(epoch, key)` advances `currentEpoch` |
|
|
6927
|
+
| `encryptFrame(ctx, payload)` | `Promise` — `[epoch][iv][cipher+tag]` |
|
|
6928
|
+
| `decryptFrame(ctx, frame)` | `Promise` — reads epoch byte and decrypts with matching key |
|
|
6929
|
+
| `attachE2ee(pc, ctx)` | `{ refresh(), detach() }` — installs transforms on every sender + receiver |
|
|
6930
|
+
|
|
6931
|
+
|
|
6932
|
+
> **Warning:** SFrame requires `RTCRtpSender.createEncodedStreams()` (Insertable Streams). Browsers without it throw `ZQ_WEBRTC_E2EE_NO_WEBCRYPTO` or fail silently on `attachE2ee` — feature-detect before promising E2EE to users.
|
|
6933
|
+
|
|
6934
|
+
|
|
6935
|
+
> All E2EE errors derive from `E2eeError`: `ZQ_WEBRTC_E2EE_NO_WEBCRYPTO`, `ZQ_WEBRTC_E2EE_NO_RANDOM`, `ZQ_WEBRTC_E2EE_BAD_PASSPHRASE`, `ZQ_WEBRTC_E2EE_BAD_SALT`, `ZQ_WEBRTC_E2EE_BAD_INPUT`, `ZQ_WEBRTC_E2EE_BAD_CTX`, `ZQ_WEBRTC_E2EE_NO_KEY`, `ZQ_WEBRTC_E2EE_SHORT_FRAME`, `ZQ_WEBRTC_E2EE_UNKNOWN_EPOCH`, `ZQ_WEBRTC_E2EE_AUTH_FAILED`.
|
|
6936
|
+
|
|
6937
|
+
|
|
6938
|
+
### SFU Adapters
|
|
6939
|
+
|
|
6940
|
+
|
|
6941
|
+
`loadSfuAdapter(name, opts?)` dynamic-imports an optional peer dependency and returns an adapter wrapping its native client. Both **mediasoup** and **LiveKit** adapters are shipped today. The peer dependencies are *not* bundled — consuming apps install them themselves; if missing, the adapter throws `ZQ_WEBRTC_SFU_PEER_MISSING` with an actionable message.
|
|
6942
|
+
|
|
6943
|
+
|
|
6944
|
+
| Adapter | Peer dep | Install |
|
|
6945
|
+
| --- | --- | --- |
|
|
6946
|
+
| `'mediasoup'` | `mediasoup-client` | `npm i mediasoup-client` |
|
|
6947
|
+
| `'livekit'` | `livekit-client` | `npm i livekit-client` |
|
|
6948
|
+
|
|
6949
|
+
|
|
6950
|
+
|
|
6951
|
+
```javascript
|
|
6952
|
+
import { loadSfuAdapter } from 'zero-query/webrtc';
|
|
6953
|
+
|
|
6954
|
+
// mediasoup adapter:
|
|
6955
|
+
const sfu = await loadSfuAdapter('mediasoup');
|
|
6956
|
+
await sfu.load(routerRtpCapabilities); // from your SFU signaling
|
|
6957
|
+
if (sfu.canProduce('audio')) {
|
|
6958
|
+
const sendTransport = sfu.createSendTransport(sendTransportParams);
|
|
6959
|
+
// wire transport.on('connect', ...) / on('produce', ...) to your signaling
|
|
6960
|
+
}
|
|
6961
|
+
const recvTransport = sfu.createRecvTransport(recvTransportParams);
|
|
6962
|
+
|
|
6963
|
+
// LiveKit adapter:
|
|
6964
|
+
const lk = await loadSfuAdapter('livekit');
|
|
6965
|
+
await lk.connect('wss://lk.example', accessToken);
|
|
6966
|
+
// ... use lk.room directly (livekit-client Room) ...
|
|
6967
|
+
await lk.disconnect();
|
|
6968
|
+
```
|
|
6969
|
+
|
|
6970
|
+
|
|
6971
|
+
> All SFU errors derive from `SfuError`: `ZQ_WEBRTC_SFU_UNKNOWN`, `ZQ_WEBRTC_SFU_PEER_MISSING`, `ZQ_WEBRTC_SFU_BAD_MODULE`, `ZQ_WEBRTC_SFU_DEVICE_FAILED`, `ZQ_WEBRTC_SFU_ROOM_FAILED`, `ZQ_WEBRTC_SFU_BAD_RTP_CAPS`, `ZQ_WEBRTC_SFU_BAD_URL`, `ZQ_WEBRTC_SFU_BAD_TOKEN`, `ZQ_WEBRTC_SFU_LOAD_FAILED`, `ZQ_WEBRTC_SFU_CONNECT_FAILED`, `ZQ_WEBRTC_SFU_NOT_LOADED`, `ZQ_WEBRTC_SFU_JOIN_UNAVAILABLE`.
|
|
6972
|
+
|
|
6973
|
+
|
|
6974
|
+
### Join Tokens
|
|
6975
|
+
|
|
6976
|
+
|
|
6977
|
+
`decodeJoinToken(token)` is a UX-only helper that base64url-decodes the payload of a server-issued join token (as minted by `signJoinToken({ secret, user, room, exp })` in `@zero-server/webrtc`). The client **never trusts** the payload — the server re-validates the signature on every `join` — but the decoded fields are useful for UI like "expires in 5 minutes" or showing the user's display name before sending. Tokens may be 1-, 2-, or 3-segment (JWT-like).
|
|
6978
|
+
|
|
6979
|
+
|
|
6980
|
+
|
|
6981
|
+
```javascript
|
|
6982
|
+
import { decodeJoinToken, isJoinTokenExpired } from 'zero-query';
|
|
6983
|
+
|
|
6984
|
+
const t = decodeJoinToken(tokenFromServer);
|
|
6985
|
+
// { user: { id: 'u1', name: 'Ada' }, room: 'lobby', exp: 1700000000, raw: {...} }
|
|
6986
|
+
|
|
6987
|
+
if (isJoinTokenExpired(t, { skewMs: 30_000 })) {
|
|
6988
|
+
// refresh token before calling `join`
|
|
6989
|
+
}
|
|
6990
|
+
```
|
|
6991
|
+
|
|
6992
|
+
|
|
6993
|
+
| Helper | Returns |
|
|
6994
|
+
| --- | --- |
|
|
6995
|
+
| `decodeJoinToken(token)` | `{ user, room, exp, raw }` |
|
|
6996
|
+
| `isJoinTokenExpired(decoded, { nowMs?, skewMs? })` | `boolean` |
|
|
6997
|
+
|
|
6998
|
+
|
|
6999
|
+
> **Warning:** `decodeJoinToken` performs **no** signature verification — it is a UI helper only. Never gate auth or permissions on its output; the server is the source of truth.
|
|
7000
|
+
|
|
7001
|
+
|
|
7002
|
+
> Errors derive from `WebRtcError`: `ZQ_WEBRTC_TOKEN_BAD_INPUT`, `ZQ_WEBRTC_TOKEN_BAD_SHAPE`, `ZQ_WEBRTC_TOKEN_BAD_PAYLOAD`.
|
|
7003
|
+
|
|
7004
|
+
|
|
7005
|
+
### Observability (getStats)
|
|
7006
|
+
|
|
7007
|
+
|
|
7008
|
+
Low-level `RTCPeerConnection.getStats()` helpers — useful for dashboards, logging, and feeding the reactive `useConnectionQuality` composable. `samplePeerStats(pc)` reduces a getStats report to a flat summary plus the raw arrays. `createStatsSampler(pc, opts)` polls on an interval. `classifyStats(sample)` buckets a sample into `'good' | 'fair' | 'poor' | 'unknown'`.
|
|
7009
|
+
|
|
7010
|
+
|
|
7011
|
+
|
|
7012
|
+
```javascript
|
|
7013
|
+
import { samplePeerStats, createStatsSampler, classifyStats } from 'zero-query';
|
|
7014
|
+
|
|
7015
|
+
// One-shot snapshot:
|
|
7016
|
+
const s = await samplePeerStats(pc);
|
|
7017
|
+
console.log(s.summary, classifyStats(s));
|
|
7018
|
+
// { rttMs: 42, lossPct: 0.3, bytesSent: 18234, bytesReceived: 30210 } 'good'
|
|
7019
|
+
|
|
7020
|
+
// Periodic sampler:
|
|
7021
|
+
const sampler = createStatsSampler(pc, {
|
|
7022
|
+
intervalMs: 2000,
|
|
7023
|
+
onSample: (s) => dashboardSignal.value = s.summary,
|
|
7024
|
+
onError: (err) => console.warn('stats failed', err.code, err.message),
|
|
7025
|
+
});
|
|
7026
|
+
// ... later
|
|
7027
|
+
sampler.stop();
|
|
7028
|
+
```
|
|
7029
|
+
|
|
7030
|
+
|
|
7031
|
+
| Bucket | Heuristic |
|
|
7032
|
+
| --- | --- |
|
|
7033
|
+
| `'good'` | Loss ≤ 1% *and* RTT ≤ 200 ms |
|
|
7034
|
+
| `'fair'` | Loss ≤ 5% *and* RTT ≤ 400 ms |
|
|
7035
|
+
| `'poor'` | Loss > 5% *or* RTT > 400 ms |
|
|
7036
|
+
| `'unknown'` | No `candidate-pair` in the report yet (early/closed connection) |
|
|
7037
|
+
|
|
7038
|
+
|
|
7039
|
+
> Errors derive from `WebRtcError`: `ZQ_WEBRTC_OBSERVE_BAD_PC`, `ZQ_WEBRTC_OBSERVE_GETSTATS_FAILED`.
|
|
7040
|
+
|
|
7041
|
+
|
|
7042
|
+
### SDP + ICE Helpers
|
|
7043
|
+
|
|
7044
|
+
|
|
7045
|
+
Zero-dependency, read-only ports of the parser surface from `@zero-server/webrtc`. Useful for sanity-checking an SDP before sending it through signaling, lifting structured fields out for stats / debugging, and filtering ICE candidates against a local privacy policy. All errors derive from `SdpError` / `IceError`.
|
|
7046
|
+
|
|
7047
|
+
|
|
7048
|
+
|
|
7049
|
+
```javascript
|
|
7050
|
+
import {
|
|
7051
|
+
parseSdp, validateSdp,
|
|
7052
|
+
parseCandidate, filterCandidates, isMdnsHostname,
|
|
7053
|
+
} from 'zero-query';
|
|
7054
|
+
|
|
7055
|
+
// Parse + validate the SDP server-side rules apply (UDP/TLS/RTP/SAVPF,
|
|
7056
|
+
// ice-ufrag, ice-pwd, fingerprint). Throws SdpError if any required
|
|
7057
|
+
// attribute is missing on a non-rejected m-line.
|
|
7058
|
+
const desc = validateSdp(offer.sdp);
|
|
7059
|
+
console.log(desc.media[0].iceUfrag, desc.media[0].fingerprint);
|
|
7060
|
+
|
|
7061
|
+
// Filter a raw candidate batch against a local privacy policy:
|
|
7062
|
+
const safe = filterCandidates(offer.candidates, {
|
|
7063
|
+
blockPrivate: true,
|
|
7064
|
+
blockMdns: true,
|
|
7065
|
+
allowedTypes: ['srflx', 'relay'],
|
|
7066
|
+
maxCandidates: 30,
|
|
7067
|
+
});
|
|
7068
|
+
|
|
7069
|
+
// Parse a single candidate line:
|
|
7070
|
+
const c = parseCandidate('candidate:1 1 udp 2122194687 1.2.3.4 50001 typ srflx raddr 192.168.1.5 rport 50000');
|
|
7071
|
+
if (c.type === 'relay') console.log('via TURN');
|
|
7072
|
+
if (isMdnsHostname(c.address)) console.log('mDNS - skip');
|
|
7073
|
+
```
|
|
7074
|
+
|
|
7075
|
+
|
|
7076
|
+
| Helper | Returns | Throws |
|
|
7077
|
+
| --- | --- | --- |
|
|
7078
|
+
| `parseSdp(text, opts?)` | `ParsedSdp` | SdpError |
|
|
7079
|
+
| `validateSdp(text)` | `ParsedSdp` | SdpError |
|
|
7080
|
+
| `parseCandidate(line)` | `IceCandidate` | IceError |
|
|
7081
|
+
| `stringifyCandidate(c)` | `string` | IceError |
|
|
7082
|
+
| `filterCandidates(list, policy?)` | same shape as input | — |
|
|
7083
|
+
| `isPrivateIp / isLoopbackIp / isLinkLocalIp / isMdnsHostname` | `boolean` | — |
|
|
7084
|
+
|
|
7085
|
+
|
|
7086
|
+
### Error Family
|
|
7087
|
+
|
|
7088
|
+
|
|
7089
|
+
Every WebRTC error derives from `WebRtcError`, which itself derives from the shared `ZQueryError`. They participate in `$.onError(handler)` like any other library error and carry a stable `code` string.
|
|
7090
|
+
|
|
7091
|
+
|
|
7092
|
+
| Class | Default Code | Surface |
|
|
7093
|
+
| --- | --- | --- |
|
|
7094
|
+
| `WebRtcError` | `ZQ_WEBRTC` | Base — token decode, getStats |
|
|
7095
|
+
| `SignalingError` | `ZQ_WEBRTC_SIGNALING` | WebSocket / handshake / framing |
|
|
7096
|
+
| `IceError` | `ZQ_WEBRTC_ICE` | Candidate parsing / validation |
|
|
7097
|
+
| `SdpError` | `ZQ_WEBRTC_SDP` | SDP parsing / validation |
|
|
7098
|
+
| `TurnError` | `ZQ_WEBRTC_TURN` | TURN credential fetch / refresh |
|
|
7099
|
+
| `E2eeError` | `ZQ_WEBRTC_E2EE` | SFrame key + frame transform |
|
|
7100
|
+
| `SfuError` | `ZQ_WEBRTC_SFU` | SFU adapter / peer-dep loader |
|
|
7101
|
+
|
|
7102
|
+
|
|
7103
|
+
|
|
7104
|
+
```javascript
|
|
7105
|
+
import { WebRtcError, SignalingError } from 'zero-query';
|
|
7106
|
+
|
|
7107
|
+
try {
|
|
7108
|
+
await client.connect();
|
|
7109
|
+
} catch (err) {
|
|
7110
|
+
if (err instanceof SignalingError) {
|
|
7111
|
+
console.warn('signaling failed:', err.code, err.message);
|
|
7112
|
+
} else if (err instanceof WebRtcError) {
|
|
7113
|
+
console.warn('webrtc failed:', err.code, err.message);
|
|
7114
|
+
} else {
|
|
7115
|
+
throw err;
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
```
|
|
7119
|
+
|
|
7120
|
+
|
|
7121
|
+
> **Tip:** Use `$.onError(handler)` to centralize WebRTC error reporting alongside HTTP / router / component errors. Every WebRTC error code starts with `ZQ_WEBRTC_` so a single prefix check picks them all out.
|
|
7122
|
+
|
|
7123
|
+
|
|
7124
|
+
### Wire Protocol
|
|
7125
|
+
|
|
7126
|
+
|
|
7127
|
+
The `SignalingClient` speaks the JSON-over-WebSocket protocol of `@zero-server/webrtc`. The first server message after a successful connect is always `{ type: 'hello', peerId }`; everything else is type-tagged.
|
|
7128
|
+
|
|
7129
|
+
|
|
7130
|
+
| Direction | Frame types |
|
|
7131
|
+
| --- | --- |
|
|
7132
|
+
| **Client → Server** | `join`, `leave`, `offer`, `answer`, `ice`, `mute`, `unmute`, `bye`, `e2ee-key` |
|
|
7133
|
+
| **Server → Client** | `hello`, `joined`, `peer-joined`, `peer-left`, `offer`, `answer`, `ice`, `mute`, `unmute`, `e2ee-key`, `error` |
|
|
7134
|
+
|
|
7135
|
+
|
|
7136
|
+
> Frames missing the required `type` field, the initial `hello`, or with malformed JSON raise a `SignalingError` on the `error` event — matching the server's own validation contract.
|
|
7137
|
+
|
|
7138
|
+
---
|
|
7139
|
+
|
|
6520
7140
|
## ES Module Exports (for npm/bundler usage)
|
|
6521
7141
|
|
|
6522
7142
|
When used as an ES module (not the built bundle), the library provides named exports for every public API:
|
|
@@ -6560,6 +7180,47 @@ import {
|
|
|
6560
7180
|
guardAsync,
|
|
6561
7181
|
validate,
|
|
6562
7182
|
formatError,
|
|
7183
|
+
webrtc,
|
|
7184
|
+
SignalingClient,
|
|
7185
|
+
Peer,
|
|
7186
|
+
Room,
|
|
7187
|
+
webrtcJoin,
|
|
7188
|
+
useRoom,
|
|
7189
|
+
usePeer,
|
|
7190
|
+
useTracks,
|
|
7191
|
+
useDataChannel,
|
|
7192
|
+
useConnectionQuality,
|
|
7193
|
+
fetchTurnCredentials,
|
|
7194
|
+
mergeIceServers,
|
|
7195
|
+
createTurnRefresher,
|
|
7196
|
+
deriveSFrameKey,
|
|
7197
|
+
generateSFrameKey,
|
|
7198
|
+
SFrameContext,
|
|
7199
|
+
encryptFrame,
|
|
7200
|
+
decryptFrame,
|
|
7201
|
+
attachE2ee,
|
|
7202
|
+
loadSfuAdapter,
|
|
7203
|
+
SfuError,
|
|
7204
|
+
decodeJoinToken,
|
|
7205
|
+
isJoinTokenExpired,
|
|
7206
|
+
samplePeerStats,
|
|
7207
|
+
createStatsSampler,
|
|
7208
|
+
classifyStats,
|
|
7209
|
+
parseSdp,
|
|
7210
|
+
validateSdp,
|
|
7211
|
+
parseCandidate,
|
|
7212
|
+
stringifyCandidate,
|
|
7213
|
+
filterCandidates,
|
|
7214
|
+
isPrivateIp,
|
|
7215
|
+
isLoopbackIp,
|
|
7216
|
+
isLinkLocalIp,
|
|
7217
|
+
isMdnsHostname,
|
|
7218
|
+
WebRtcError,
|
|
7219
|
+
SignalingError,
|
|
7220
|
+
IceError,
|
|
7221
|
+
SdpError,
|
|
7222
|
+
TurnError,
|
|
7223
|
+
E2eeError,
|
|
6563
7224
|
debounce,
|
|
6564
7225
|
throttle,
|
|
6565
7226
|
pipe,
|
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|