@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/README.md +245 -0
- package/dist/audio/audio-processor.js +1 -0
- package/dist/audio/wasm-gen/origon_web_audio_bg.wasm +0 -0
- package/dist/index.d.ts +472 -0
- package/dist/origon-web-sdk.js +5465 -0
- package/dist/origon-web-sdk.js.map +1 -0
- package/docs/backend-integration.md +485 -0
- package/docs/contract.md +71 -0
- package/docs/new-chat-protocol.md +91 -0
- package/docs/voice.md +165 -0
- package/package.json +63 -0
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
|
+
}
|