@origonai/web-sdk 0.1.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/docs/voice.md ADDED
@@ -0,0 +1,165 @@
1
+ # Voice (WebTransport + WASM Opus)
2
+
3
+ ## Wire summary
4
+
5
+ | Layer | Tech |
6
+ |--------------|----------------------------------------------------------------------|
7
+ | Transport | WebTransport (`https://<media-server>/web`), one bidi control sub-stream + datagrams |
8
+ | Control | B2BUA verbs (Connect/ConnectOk/Mute/Resume/PeerAttached/…), `msg_type (vi) \| payload_len (vi) \| payload` |
9
+ | Audio | Opus 48 kHz mono, 20 ms frames (50 per wall-second), `ObjectDatagram` per frame |
10
+ | Group / object IDs | `group_id = floor(Date.now()/1000) - server_epoch_unix`, `object_id ∈ [0..49]`, `end_of_group` on object 49 |
11
+ | Reconnect | Tier-3 `Resume { call_token, last_event_seq }` only — browsers expose no QUIC migration / 0-RTT |
12
+ | Audio engine | WASM, prebuilt + committed at `src/voice/audio/wasm-gen/` (vendored libopus; see "The WASM audio engine is prebuilt") |
13
+
14
+ See [`backend-integration.md`](backend-integration.md) (canonical spec) for the full wire definitions.
15
+
16
+ ## Required browser features (the SDK-wide floor)
17
+
18
+ | Feature | Needed by |
19
+ |------------------------|--------------------------------------------------|
20
+ | WebTransport | **Chat AND voice** — chat is orpc-over-WebTransport only (no SSE fallback), voice rides connection + datagrams |
21
+ | AudioWorklet | Voice only — the real-time audio thread |
22
+
23
+ **WebTransport is the floor for the whole SDK**: chat has no non-WT
24
+ transport (owner decision 2026-08-06), so browsers without WebTransport
25
+ (Safari, Firefox today) are unsupported for chat and voice alike.
26
+
27
+ Datagrams cross the main ↔ worklet boundary as port messages with
28
+ transferred buffers — no `SharedArrayBuffer`, so the page needs **no
29
+ COOP/COEP / cross-origin-isolation headers**. Voice works on any page that
30
+ has WebTransport (including the embed widget on a third-party host page).
31
+
32
+ ## The WASM audio engine is prebuilt
33
+
34
+ The WASM audio engine ships **prebuilt and committed** at
35
+ `src/voice/audio/wasm-gen/` (`origon_web_audio.js` + `origon_web_audio_bg.wasm`
36
+ + type declarations). Building or publishing this repo needs **no Rust, no
37
+ wasm toolchain** — `pnpm i && pnpm build` is the whole story.
38
+
39
+ The engine's Rust source lives in the Origon monorepo (`apps/sdk/web`, the
40
+ `web-audio` crate) and is regenerated there with
41
+ `apps/sdk/web/scripts/build-wasm.sh`, which rebuilds the artifact into this
42
+ repo's `wasm-gen/` and stamps the `PROVENANCE` file beside it (source git SHA,
43
+ crate + toolchain versions, artifact byte size). Treat `wasm-gen/` as
44
+ generated output: never edit it by hand, and take updates only through the
45
+ ship script so `PROVENANCE` stays truthful.
46
+
47
+ ### The wasm-bindgen glue inside the AudioWorklet
48
+
49
+ The AudioWorklet thread has no ES-module loader, so `src/voice/audio/audio-processor.ts` imports the generated `wasm-gen/origon_web_audio.js` directly and `pnpm build:worklet` (Vite, IIFE) bundles the two into a single self-contained `audio-processor.js` (an intermediate `.worklet-build/` output that the main build then ships as the `audio/audio-processor.js` asset). After a reshipped `wasm-gen/`, just re-run `pnpm build` — no hand-editing of glue entries.
50
+
51
+ ## Voice assets: emitted by YOUR bundler
52
+
53
+ The voice path needs two files at runtime — the AudioWorklet bundle
54
+ (`audio/audio-processor.js`) and the Opus pipeline wasm
55
+ (`audio/wasm-gen/origon_web_audio_bg.wasm`). The shipped SDK bundle
56
+ references them as relative, unresolved
57
+ `new URL('./audio/…?no-inline', import.meta.url)` literals, so **your
58
+ bundler emits and relocates them automatically** when you build — there
59
+ is nothing to configure and nothing to host by hand.
60
+
61
+ Requirements:
62
+
63
+ - **Vite ≥ 6.3.3** (or another bundler that supports the
64
+ `new URL(..., import.meta.url)` asset pattern and strips the
65
+ `?no-inline` query from the emitted URL). Below 6.3.3 the query leaks
66
+ into the emitted asset URL and the fetch 404s — and an SPA-fallback
67
+ server can mask that 404 as a 200 `text/html` response that only
68
+ fails later, inside `audioWorklet.addModule()`
69
+ (`AbortError: Unable to load a worklet's module`). **After any
70
+ bundler upgrade, re-verify both asset fetches return
71
+ `text/javascript` / `application/wasm`, never `text/html`.**
72
+ - **CSP**: the pipeline compiles its wasm from fetched bytes
73
+ (`WebAssembly.compile`), so a page with a Content-Security-Policy
74
+ needs `'wasm-unsafe-eval'` in `script-src`. No COOP/COEP is needed
75
+ (see above).
76
+
77
+ ### `assetBaseUrl` — the copy-step escape hatch
78
+
79
+ If your pipeline cannot relocate the literals (e.g. esbuild-only),
80
+ copy the two files to a served location **preserving the `audio/…`
81
+ layout**, and point the SDK at it:
82
+
83
+ ```ts
84
+ client.initialize({
85
+ endpoint: 'https://your-control-plane',
86
+ assetBaseUrl: 'https://cdn.example.com/web-sdk/',
87
+ // voice then fetches <base>/audio/audio-processor.js
88
+ // and <base>/audio/wasm-gen/origon_web_audio_bg.wasm
89
+ })
90
+ ```
91
+
92
+ Cost note: the bundler-relocation branch is emitted at build time
93
+ regardless, so a bundling consumer that sets `assetBaseUrl` still emits
94
+ (and deploys) ~490 KB of wasm it never fetches. The override changes
95
+ which URLs are fetched at call time; it does not remove the emitted
96
+ copies.
97
+
98
+ ## Public voice API
99
+
100
+ ```ts
101
+ import { getSessionManager } from '@origonai/web-sdk'
102
+
103
+ const client = getSessionManager()
104
+ client.initialize({
105
+ endpoint: 'https://your-control-plane',
106
+ token: '<bearer>',
107
+ userId: '<your-user>',
108
+ // assetBaseUrl: '…' — only for non-relocating pipelines, see above
109
+ })
110
+ client.setCallbacks({
111
+ onConnected: (sessionId) => console.log('voice connected:', sessionId),
112
+ onReconnecting: (id, ch, { attempt, reason }) => console.log('reconnecting', attempt, reason),
113
+ onReconnected: (sessionId) => console.log('back online'),
114
+ onPeerAttached: (id, ch, peer) => console.log('peer joined:', peer),
115
+ onPeerDetached: (id, ch, peer) => console.log('peer left:', peer),
116
+ onDisconnected: (id, ch, { reason }) => console.log('disconnected:', reason),
117
+ onCallError: (id, ch, msg) => console.warn('call error:', msg),
118
+ })
119
+
120
+ // Supplied out of band by the application's control plane.
121
+ const listenOffer = { sessionId: '<id>', url: '<media-url>', token: '<call-token>' }
122
+ const { sessionId } = await client.joinSession({
123
+ channel: 'voice',
124
+ sessionId: listenOffer.sessionId,
125
+ url: listenOffer.url,
126
+ token: listenOffer.token,
127
+ voice: { receiveOnly: true },
128
+ })
129
+
130
+ await client.setMute(sessionId, 'uplink') // 'uplink' | 'downlink' | 'both' | 'none'
131
+
132
+ // For a provisioned receive-only join, enter and leave microphone capture
133
+ // without exposing an unmuted permission/acquisition window.
134
+ await client.enableCapture(sessionId)
135
+ await client.releaseCapture(sessionId)
136
+
137
+ const unsubscribe = client.subscribeAudioStats(sessionId, (stats) => {
138
+ // stats.outboundLevel — local mic RMS (0..1)
139
+ // stats.inboundLevel — playback RMS (0..1)
140
+ // stats.pipeline — { tracks: [{ alias, jitter_ms, plc_count, … }] }
141
+ })
142
+
143
+ // Later
144
+ unsubscribe()
145
+ await client.endSession(sessionId)
146
+ ```
147
+
148
+ ## Reconnect behaviour
149
+
150
+ When `wt.closed` fires unexpectedly (no `EndpointHangup` or terminal `MoqError` arrived first), the SDK runs:
151
+
152
+ ```
153
+ 1. wait 100ms
154
+ 2. open new WebTransport
155
+ 3. send Resume { call_token, last_event_seq }
156
+ 4. on ResumeOk → restore peer aliases + mute scope from EndpointSnapshot
157
+ 5. fire onReconnected
158
+ ```
159
+
160
+ Backoff schedule: `[100, 250, 500, 1000]` ms. Fires `onReconnecting({attempt, reason})` before each attempt. After the four retries, fires `onDisconnected({reason: 'networkLoss'})`.
161
+
162
+ Terminal close paths that skip the retry loop:
163
+ - We called `endSession()` (`reason: 'localClose'`)
164
+ - Received `EndpointHangup` (`reason: 'remoteHangup'` for `origin: 'peer'`)
165
+ - Received `MoqError` with a terminal code (`tokenInvalid`, `tokenExpired`, `tokenReplayed`, `sessionEnded`, `replayLost`, `endpointNotProvisioned`, `endpointAlreadyConnected`)
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@origonai/web-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Origon Web SDK - chat and voice session client",
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.5.1",
7
+ "main": "./dist/origon-web-sdk.js",
8
+ "module": "./dist/origon-web-sdk.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/origon-web-sdk.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "docs"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/origon/web-sdk.git"
23
+ },
24
+ "publishConfig": {
25
+ "registry": "https://registry.npmjs.org/",
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "check:wasm": "test -f src/voice/audio/wasm-gen/origon_web_audio.js && test -f src/voice/audio/wasm-gen/origon_web_audio_bg.wasm || { echo '✗ wasm-gen incomplete — it ships prebuilt and committed; restore it from git (regenerated only from the Origon monorepo: apps/sdk/web/scripts/build-wasm.sh)' >&2; exit 1; }",
30
+ "check:orpc": "test -f src/orpc-gen/index.ts && test -f src/orpc-gen/chat_pb.ts && test -f src/orpc-gen/PROVENANCE || { echo '✗ orpc-gen incomplete — it ships vendored and committed; restore it from git (regenerated only from the Origon monorepo: web/kit/scripts/ship-orpc.sh)' >&2; exit 1; }",
31
+ "build": "pnpm check:wasm && pnpm check:orpc && pnpm build:worklet && pnpm build:main",
32
+ "build:main": "vite build --mode production",
33
+ "build:worklet": "BUILD_TARGET=worklet vite build --mode production",
34
+ "dev": "pnpm build && pnpm build:main --watch --mode development",
35
+ "test": "vitest run",
36
+ "typecheck": "tsc --noEmit",
37
+ "deploy": "pnpm build && npm publish --access public"
38
+ },
39
+ "keywords": [
40
+ "chat",
41
+ "voice",
42
+ "sdk",
43
+ "origon",
44
+ "moq",
45
+ "webtransport"
46
+ ],
47
+ "author": "Origon",
48
+ "license": "MIT",
49
+ "dependencies": {
50
+ "@bufbuild/protobuf": "^2.12.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20",
54
+ "magic-string": "^0.30.21",
55
+ "typescript": "^5.4.0",
56
+ "vite": "^6.0.0",
57
+ "vite-plugin-dts": "^4.0.0",
58
+ "vitest": "^4.1.10"
59
+ },
60
+ "engines": {
61
+ "node": ">=20.0.0"
62
+ }
63
+ }