@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/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# @origonai/web-sdk
|
|
2
|
+
|
|
3
|
+
Origon Web SDK — chat and voice session client for browsers.
|
|
4
|
+
|
|
5
|
+
> **Status**: v0.5.0 (unpublished). Chat (orpc over WebTransport) and
|
|
6
|
+
> voice (WebTransport + WASM Opus) are both implemented — see
|
|
7
|
+
> [Status](#status). **WebTransport is required for both channels**;
|
|
8
|
+
> browsers without it (Safari, Firefox today) are unsupported.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @origonai/web-sdk
|
|
14
|
+
# or
|
|
15
|
+
pnpm add @origonai/web-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start (chat)
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { getSessionManager } from '@origonai/web-sdk'
|
|
22
|
+
|
|
23
|
+
const client = getSessionManager()
|
|
24
|
+
|
|
25
|
+
client.initialize({
|
|
26
|
+
endpoint: 'https://your-backend.example.com',
|
|
27
|
+
token: '<bearer>', // optional; omit for anonymous users
|
|
28
|
+
userId: '<your-id>', // optional; auto-generated UUID v7 if not provided
|
|
29
|
+
bundleId: 'com.example.app',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
await client.authenticate()
|
|
33
|
+
|
|
34
|
+
client.setCallbacks({
|
|
35
|
+
onMessageAdded: (sessionId, channel, msg) => render(msg),
|
|
36
|
+
onMessageUpdated: (sessionId, channel, { id, message }) => update(id, message),
|
|
37
|
+
onTyping: (sessionId, channel, isTyping) => setTyping(isTyping),
|
|
38
|
+
onDisconnected: (sessionId, channel, { reason }) => console.log('closed:', reason),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const { sessionId } = await client.startSession({ channel: 'chat' })
|
|
42
|
+
|
|
43
|
+
await client.sendMessage(sessionId, { text: 'Hello!' })
|
|
44
|
+
|
|
45
|
+
client.notifyTyping(sessionId) // call on each keystroke; SDK debounces
|
|
46
|
+
|
|
47
|
+
await client.endSession(sessionId)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Public API
|
|
51
|
+
|
|
52
|
+
### Lifecycle
|
|
53
|
+
- `initialize(credentials)` — set endpoint, token, userId, bundleId, attributes
|
|
54
|
+
- `authenticate()` — POST `/config`, cache and return server config
|
|
55
|
+
- `setCallbacks(callbacks)` — register event handlers (see below)
|
|
56
|
+
- `setAttributes(attrs)` — update session-level attributes forwarded on `/session/start`
|
|
57
|
+
|
|
58
|
+
### Sessions
|
|
59
|
+
- `startSession({ channel, sessionId?, data? })` — originate a chat or voice session (issues `POST /session/start`)
|
|
60
|
+
- `joinSession({ channel, sessionId, url, token, voice? })` — attach to a session provisioned out of band, skipping `POST /session/start`; `voice.receiveOnly` starts playout with no microphone and awaits server uplink mute before connecting (see [Joining a provisioned session](#joining-a-provisioned-session))
|
|
61
|
+
- `endSession(sessionId)` — close a session
|
|
62
|
+
- `endAllSessions()` — close every active session
|
|
63
|
+
- `migrateSessionId(oldId, newId)` — re-key a live session whose id the control plane reassigned mid-call, without a media re-dial
|
|
64
|
+
- `activeSessionIds()` — list active sessions
|
|
65
|
+
- `getSessions()` — GET `/sessions` (prior sessions; every `SessionSummary`
|
|
66
|
+
includes required live-owner `active: boolean`)
|
|
67
|
+
- `getSession(sessionId)` — GET `/session/{id}` (transcript + control)
|
|
68
|
+
|
|
69
|
+
### Chat
|
|
70
|
+
- `sendMessage(sessionId, payload)` — post a message; SDK fires `onMessageAdded` immediately with `status: 'sending'`, then `onMessageUpdated` with `'delivered'` or `'failed'` after the server acks
|
|
71
|
+
- `notifyTyping(sessionId)` — call on each keystroke; SDK debounces (3s window) and emits typing on/off automatically
|
|
72
|
+
- `stopTyping(sessionId)` — flush typing-off immediately
|
|
73
|
+
|
|
74
|
+
### Attachments
|
|
75
|
+
Attachments are **widget-scoped, not session-scoped** — both verbs address the
|
|
76
|
+
`endpoint` given to `initialize()` and need no live session, so a file can be the
|
|
77
|
+
first thing a visitor sends.
|
|
78
|
+
|
|
79
|
+
- `uploadAttachment(file, { uploadId, onProgress? })` — streamed upload with progress
|
|
80
|
+
- `deleteAttachment(idOrUploadId)` — dual-purpose: cancels in-flight uploads if `idOrUploadId` matches a pending `uploadId`; otherwise DELETEs the stored attachment
|
|
81
|
+
|
|
82
|
+
## Callbacks
|
|
83
|
+
|
|
84
|
+
`active` is directory liveness, not stored status. The SDK requires the field
|
|
85
|
+
but does not auto-restore browser chats; opening or resuming a row remains a host
|
|
86
|
+
action.
|
|
87
|
+
|
|
88
|
+
All callbacks receive `sessionId, channel` as the first two args so one
|
|
89
|
+
consumer can host multiple sessions.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
{
|
|
93
|
+
// Chat
|
|
94
|
+
onMessageAdded?: (sessionId, channel, message)
|
|
95
|
+
onMessageUpdated?: (sessionId, channel, { id, message })
|
|
96
|
+
onTyping?: (sessionId, channel, isTyping)
|
|
97
|
+
onSessionUpdated?: (sessionId, channel)
|
|
98
|
+
|
|
99
|
+
// Lifecycle (both channels — chat terminals land on onDisconnected too)
|
|
100
|
+
onConnected?: (sessionId, channel)
|
|
101
|
+
onDisconnected?: (sessionId, channel, { reason })
|
|
102
|
+
|
|
103
|
+
// Voice
|
|
104
|
+
onReconnecting?: (sessionId, channel, { attempt, reason })
|
|
105
|
+
onReconnected?: (sessionId, channel)
|
|
106
|
+
onPeerAttached?: (sessionId, channel, { peerEndpointId, alias })
|
|
107
|
+
onPeerDetached?: (sessionId, channel, { peerEndpointId, alias })
|
|
108
|
+
onCallError?: (sessionId, channel, error)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Message roles
|
|
113
|
+
|
|
114
|
+
| Role | Sender |
|
|
115
|
+
|--------------|-------------------------------------------------------------------|
|
|
116
|
+
| `ai` | AI bot / assistant |
|
|
117
|
+
| `external` | End-user / customer (default for `sendMessage` from a widget) |
|
|
118
|
+
| `user` | Internal staff / operator (default for `sendMessage` from a dashboard) |
|
|
119
|
+
| `system` | System-injected messages |
|
|
120
|
+
|
|
121
|
+
The outbound `payload.role` is a local-only hint for the provisional
|
|
122
|
+
`onMessageAdded` row; it's stripped before POST.
|
|
123
|
+
|
|
124
|
+
## Errors
|
|
125
|
+
|
|
126
|
+
REST-path errors are `ClientError` with a structured shape:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
class ClientError extends Error {
|
|
130
|
+
kind: 'notInitialized' | 'noSession' | 'session' | 'missingField' |
|
|
131
|
+
'serverUnavailable' | 'http' | 'attachment' | 'cancelled' | 'other'
|
|
132
|
+
status?: number // HTTP status when kind === 'http' or 'serverUnavailable'
|
|
133
|
+
code?: string // server error code (kind === 'http') or attachment policy code
|
|
134
|
+
message: string
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Server error envelopes (`{ "error": { "code", "message" } }`) parse
|
|
139
|
+
through to `ClientError.http`.
|
|
140
|
+
|
|
141
|
+
Chat **wire refusals** (the orpc lane) throw `OrpcError` instead, with
|
|
142
|
+
the server's numeric status preserved:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
class OrpcError extends Error {
|
|
146
|
+
code: number // foundation ErrorCode, e.g. 0x0001 session_not_found
|
|
147
|
+
reason: string // the server's message
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Voice (WebTransport + WASM Opus)
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
client.initialize({
|
|
155
|
+
endpoint: 'https://your-control-plane',
|
|
156
|
+
token: '<bearer>',
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const { sessionId } = await client.startSession({ channel: 'voice' })
|
|
160
|
+
await client.setMute(sessionId, 'uplink') // 'uplink' | 'downlink' | 'both' | 'none'
|
|
161
|
+
|
|
162
|
+
const off = client.subscribeAudioStats(sessionId, ({ outboundLevel, inboundLevel }) => {
|
|
163
|
+
// VU meters at ~5 Hz
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await client.endSession(sessionId)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Voice requires:
|
|
170
|
+
|
|
171
|
+
- WebTransport — Chromium / Edge today. (Chat requires it too — it is
|
|
172
|
+
the SDK-wide floor; see `docs/voice.md`.)
|
|
173
|
+
- A WASM audio engine — ships **prebuilt and committed** at `src/voice/audio/wasm-gen/`; no Rust or wasm toolchain needed (see [`docs/voice.md`](docs/voice.md)).
|
|
174
|
+
- A bundler that relocates `new URL(..., import.meta.url)` assets (Vite ≥ 6.3.3) — the SDK's two voice assets are emitted by *your* build. Non-relocating pipelines use the `assetBaseUrl` copy-step instead. If the page sets a CSP, `script-src` needs `'wasm-unsafe-eval'`. Details: [`docs/voice.md`](docs/voice.md).
|
|
175
|
+
|
|
176
|
+
The browser opens a single WebTransport to `<media-server>/web`, sends a B2BUA `Connect` over the control sub-stream, and pumps 20 ms Opus frames as `ObjectDatagram`s with `group_id = floor(Date.now()/1000) - serverEpochUnix`. Reconnect is Tier-3 only (`Resume` verb) with `[100, 250, 500, 1000]` ms backoff.
|
|
177
|
+
|
|
178
|
+
## Joining a provisioned session
|
|
179
|
+
|
|
180
|
+
`startSession` originates a session by calling `POST /session/start` itself.
|
|
181
|
+
Consumers that are instead *handed* a session out of band — a Connect agent
|
|
182
|
+
answering a pushed call offer, a supervisor joining a live call — attach with
|
|
183
|
+
`joinSession`. The `{ sessionId, url, token }` come from the caller's own
|
|
184
|
+
control plane; without additional join options, the media/chat path is
|
|
185
|
+
byte-for-byte identical thereafter.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
client.initialize({ endpoint: 'https://your-control-plane' })
|
|
189
|
+
client.setCallbacks({ onConnected, onPeerAttached, onDisconnected })
|
|
190
|
+
|
|
191
|
+
// `offer` = { sessionId, url, token }, delivered by your control channel.
|
|
192
|
+
await client.joinSession({ channel: 'voice', ...offer })
|
|
193
|
+
|
|
194
|
+
// Listen-only provisioned leg: starts the worklet + receive transport, never
|
|
195
|
+
// asks for microphone permission, and fires onConnected only after MuteOk.
|
|
196
|
+
await client.joinSession({
|
|
197
|
+
channel: 'voice',
|
|
198
|
+
...listenOffer,
|
|
199
|
+
voice: { receiveOnly: true },
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// Listen → Coach: the SDK acquires capture while server-muted, then unmutes.
|
|
203
|
+
await client.enableCapture(listenOffer.sessionId)
|
|
204
|
+
|
|
205
|
+
// Coach → Listen: the SDK waits for the server mute acknowledgement before
|
|
206
|
+
// disabling capture and releasing the microphone, worklet graph, and diagnostics.
|
|
207
|
+
await client.releaseCapture(listenOffer.sessionId)
|
|
208
|
+
|
|
209
|
+
// If the control plane later reassigns the session id (e.g. a warm-transfer or
|
|
210
|
+
// conference completes), re-key in place — the media endpoint is not re-dialed:
|
|
211
|
+
client.migrateSessionId(offer.sessionId, newId)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`initialize()` is still required (the endpoint stays mandatory), but
|
|
215
|
+
`authenticate()` / `POST /config` is **not** needed on this path. The SDK is
|
|
216
|
+
control-plane-agnostic: it never names or imports the source of the
|
|
217
|
+
`{ sessionId, url, token }` — a consumer that wires a specific producer's offer
|
|
218
|
+
(e.g. a `SessionOffered` event) owns and registers that coupling.
|
|
219
|
+
|
|
220
|
+
`voice.receiveOnly` changes only provisioned joins. Ordinary `startSession`
|
|
221
|
+
and `joinSession` calls without the option retain microphone capture. The
|
|
222
|
+
receive-only sequence is Worklet/transport → `ConnectOk` → `Mute(uplink)` →
|
|
223
|
+
`MuteOk` → `onConnected`; `getUserMedia` is never called.
|
|
224
|
+
|
|
225
|
+
Capture transitions are serialized and teardown-safe. If permission, capture,
|
|
226
|
+
or unmute fails, the SDK returns to uplink-muted state and releases the local
|
|
227
|
+
microphone. A late permission result cannot revive capture after Listen or
|
|
228
|
+
disconnect wins.
|
|
229
|
+
|
|
230
|
+
## Status
|
|
231
|
+
|
|
232
|
+
- ✅ **Phase 1 — Chat** — orpc over WebTransport (protobuf wire),
|
|
233
|
+
multi-session, attachments, overflow replay
|
|
234
|
+
- ✅ **Phase 2 — Voice** — WebTransport, B2BUA verb codec, WASM Opus engine, reconnect
|
|
235
|
+
- ✅ **Phase 3 — Voice polish** — audio stats subscription, peer attach/detach
|
|
236
|
+
|
|
237
|
+
## Extending the chat protocol
|
|
238
|
+
|
|
239
|
+
The chat wire is the `chat.v1` protobuf schema; a new event kind
|
|
240
|
+
requires a proto arm before any SDK surface can exist for it. See
|
|
241
|
+
[`docs/new-chat-protocol.md`](docs/new-chat-protocol.md).
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";const M=globalThis;if(typeof M.TextDecoder>"u"){class n{constructor(t,r){}decode(t){if(!t)return"";const r=t instanceof Uint8Array?t:t instanceof ArrayBuffer?new Uint8Array(t):new Uint8Array(t.buffer,t.byteOffset,t.byteLength);let s="",i=0;for(;i<r.length;){const o=r[i];if(o<128)s+=String.fromCharCode(o),i+=1;else if((o&224)===192)s+=String.fromCharCode((o&31)<<6|r[i+1]&63),i+=2;else if((o&240)===224)s+=String.fromCharCode((o&15)<<12|(r[i+1]&63)<<6|r[i+2]&63),i+=3;else{const c=(o&7)<<18|(r[i+1]&63)<<12|(r[i+2]&63)<<6|r[i+3]&63;s+=String.fromCodePoint(c),i+=4}}return s}}M.TextDecoder=n}if(typeof M.TextEncoder>"u"){class n{encode(t=""){const r=[];for(let s=0;s<t.length;s++){let i=t.charCodeAt(s);if(i>=55296&&i<=56319&&s+1<t.length){const o=t.charCodeAt(s+1);o>=56320&&o<=57343&&(i=65536+(i-55296<<10)+(o-56320),s++)}i<128?r.push(i):i<2048?r.push(192|i>>6,128|i&63):i<65536?r.push(224|i>>12,128|i>>6&63,128|i&63):r.push(240|i>>18,128|i>>12&63,128|i>>6&63,128|i&63)}return new Uint8Array(r)}}M.TextEncoder=n}class T{__destroy_into_raw(){const e=this.__wbg_ptr;return this.__wbg_ptr=0,O.unregister(this),e}free(){const e=this.__destroy_into_raw();a.__wbg_audiopipeline_free(e,0)}add_track(e,t){const r=$(t,a.__wbindgen_malloc,a.__wbindgen_realloc),s=p,i=a.audiopipeline_add_track(this.__wbg_ptr,e,r,s);if(i[1])throw D(i[0])}disable_capture(){a.audiopipeline_disable_capture(this.__wbg_ptr)}enable_capture(e){const t=a.audiopipeline_enable_capture(this.__wbg_ptr,e);if(t[1])throw D(t[0])}feedback_pending(){return a.audiopipeline_feedback_pending(this.__wbg_ptr)!==0}ingest_uplink_feedback(e){const t=C(e,a.__wbindgen_malloc),r=p;a.audiopipeline_ingest_uplink_feedback(this.__wbg_ptr,t,r)}constructor(e,t){const r=a.audiopipeline_new(e,t);return this.__wbg_ptr=r,O.register(this,this.__wbg_ptr,this),this}process(e,t,r,s,i,o){const c=L(e,a.__wbindgen_malloc),u=p;var k=L(t,a.__wbindgen_malloc),l=p;const h=C(r,a.__wbindgen_malloc),A=p;var Q=C(s,a.__wbindgen_malloc),Z=p;return a.audiopipeline_process(this.__wbg_ptr,c,u,k,l,t,h,A,Q,Z,s,i,o)>>>0}remove_track(e){a.audiopipeline_remove_track(this.__wbg_ptr,e)}set_server_epoch_unix(e){a.audiopipeline_set_server_epoch_unix(this.__wbg_ptr,e)}stats(){let e,t;try{const r=a.audiopipeline_stats(this.__wbg_ptr);return e=r[0],t=r[1],E(r[0],r[1])}finally{a.__wbindgen_free(e,t,1)}}take_feedback(){const e=a.audiopipeline_take_feedback(this.__wbg_ptr);var t=B(e[0],e[1]).slice();return a.__wbindgen_free(e[0],e[1]*1,1),t}}Symbol.dispose&&(T.prototype[Symbol.dispose]=T.prototype.free);function j(){return{__proto__:null,"./origon_web_audio_bg.js":{__proto__:null,__wbg___wbindgen_copy_to_typed_array_4db0cbe2cc60dbee:function(e,t,r){new Uint8Array(r.buffer,r.byteOffset,r.byteLength).set(B(e,t))},__wbg___wbindgen_throw_344f42d3211c4765:function(e,t){throw new Error(E(e,t))},__wbindgen_cast_0000000000000001:function(e,t){return E(e,t)},__wbindgen_init_externref_table:function(){const e=a.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}}}}const O=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(n=>a.__wbg_audiopipeline_free(n,1));function B(n,e){return n=n>>>0,f().subarray(n/1,n/1+e)}let y=null;function q(){return(y===null||y.byteLength===0)&&(y=new Float32Array(a.memory.buffer)),y}function E(n,e){return N(n>>>0,e)}let w=null;function f(){return(w===null||w.byteLength===0)&&(w=new Uint8Array(a.memory.buffer)),w}function C(n,e){const t=e(n.length*1,1)>>>0;return f().set(n,t/1),p=n.length,t}function L(n,e){const t=e(n.length*4,4)>>>0;return q().set(n,t/4),p=n.length,t}function $(n,e,t){if(t===void 0){const c=m.encode(n),u=e(c.length,1)>>>0;return f().subarray(u,u+c.length).set(c),p=c.length,u}let r=n.length,s=e(r,1)>>>0;const i=f();let o=0;for(;o<r;o++){const c=n.charCodeAt(o);if(c>127)break;i[s+o]=c}if(o!==r){o!==0&&(n=n.slice(o)),s=t(s,r,r=o+n.length*3,1)>>>0;const c=f().subarray(s+o,s+r),u=m.encodeInto(n,c);o+=u.written,s=t(s,r,o,1)>>>0}return p=o,s}function D(n){const e=a.__wbindgen_externrefs.get(n);return a.__externref_table_dealloc(n),e}let v=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});v.decode();const G=2146435072;let I=0;function N(n,e){return I+=e,I>=G&&(v=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),v.decode(),I=e),v.decode(f().subarray(n,n+e))}const m=new TextEncoder;"encodeInto"in m||(m.encodeInto=function(n,e){const t=m.encode(n);return e.set(t),{read:n.length,written:t.length}});let p=0,a;function Y(n,e){return a=n.exports,y=null,w=null,a.__wbindgen_start(),a}async function H(n,e){if(typeof Response=="function"&&n instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(n,e)}catch(s){if(n.ok&&t(n.type)&&n.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",s);else throw s}const r=await n.arrayBuffer();return await WebAssembly.instantiate(r,e)}else{const r=await WebAssembly.instantiate(n,e);return r instanceof WebAssembly.Instance?{instance:r,module:n}:r}function t(r){switch(r){case"basic":case"cors":case"default":return!0}return!1}}async function J(n){if(a!==void 0)return a;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module_or_path:n}=n:console.warn("using deprecated parameters for the initialization function; pass a single object instead"));const e=j();(typeof n=="string"||typeof Request=="function"&&n instanceof Request||typeof URL=="function"&&n instanceof URL)&&(n=fetch(n));const{instance:t,module:r}=await H(await n,e);return Y(t)}const V=new Uint8Array(0);let _=null,d=[],b=0,z=64*1024,F=0,x=null,R=0,U=!1,S=null,W=null,g=null;async function X(n,e,t){await J({module_or_path:n}),_=new T(e,t)}function P(n){if(n.length===0)return 0;let e=0;for(let t=0;t<n.length;t++)e+=n[t]*n[t];return Math.sqrt(e/n.length)}class K extends AudioWorkletProcessor{constructor(){super(),this.initialized=!1,this.initFailed=!1,this.pending=[],this.port.onmessage=e=>this.handleMessage(e.data)}handleMessage(e){if(!this.initialized&&e.type!=="init"){if(this.initFailed||e.type==="datagram"){e.type==="datagram"&&F++;return}this.pending.push(e);return}switch(e.type){case"init":{const t=e.wasmBytes,r=e.recvCapacity,s=e.sendCapacity,i=e.sampleRate,o=e.ptimeMs;X(t,i,o).then(()=>{z=r,x=new Uint8Array(s),this.initialized=!0;for(const c of this.pending)this.handleMessage(c);this.pending=[],this.port.postMessage({type:"ready"})}).catch(c=>{this.initFailed=!0,this.pending=[],this.port.postMessage({type:"error",message:String(c)})});break}case"datagram":{const t=e.frame;for(d.push(t),b+=t.length;b>z&&d.length>0;)b-=d.shift().length,F++;break}case"set_server_epoch_unix":_?.set_server_epoch_unix(BigInt(e.serverEpochUnix));break;case"add_track":try{_?.add_track(e.trackAlias,e.peerEndpointId??"")}catch(t){this.port.postMessage({type:"error",message:`add_track: ${String(t)}`})}break;case"remove_track":_?.remove_track(e.trackAlias);break;case"uplink_feedback":_?.ingest_uplink_feedback(e.payload);break;case"enable_capture":try{if(!_)throw new Error("pipeline unavailable");_.enable_capture(e.trackAlias),S=e.trackAlias,W=e.generation,g=null,this.port.postMessage({type:"capture_enabled",generation:e.generation,commandId:e.commandId})}catch(t){this.port.postMessage({type:"capture_enable_error",generation:e.generation,commandId:e.commandId,message:`enable_capture: ${String(t)}`})}break;case"open_capture":S!==null&&W===e.generation?(g=e.generation,this.port.postMessage({type:"capture_opened",generation:g,commandId:e.commandId})):this.port.postMessage({type:"capture_enable_error",generation:e.generation,commandId:e.commandId,message:"open_capture: capture generation is not enabled"});break;case"disable_capture":_?.disable_capture(),S=null,W=null,g=null;break;case"subscribe_stats":U=!0;break;case"unsubscribe_stats":U=!1;break}}process(e,t){if(!_||!x)return!0;const r=e[0]?.[0]??new Float32Array(0),s=t[0]?.[0];if(!s)return!0;let i;if(d.length===0)i=V;else if(d.length===1)i=d[0],d=[],b=0;else{i=new Uint8Array(b);let l=0;for(const h of d)i.set(h,l),l+=h.length;d=[],b=0}const o=Date.now(),c=Math.floor(o/4294967296)>>>0,u=o>>>0>>>0;let k=0;try{k=_.process(r,s,i,x,c,u)}catch(l){return this.port.postMessage({type:"error",message:`process failed: ${l instanceof Error?l.stack??l.message:String(l)}`}),!1}if(k>0&&g!==null){const l=x.slice(0,k);this.port.postMessage({type:"datagram_out",generation:g,payload:l},[l.buffer])}if(_.feedback_pending()){const l=_.take_feedback();l.length>0&&this.port.postMessage({type:"downlink_feedback",payload:l},[l.buffer])}if(R++,U&&R>=187){R=0;const l=P(r),h=P(s);let A;try{A=JSON.parse(_.stats())}catch{A=null}this.port.postMessage({type:"stats",data:{pipeline:A,captureAlias:S,outboundLevel:l,inboundLevel:h,droppedInbound:F}})}return!0}}registerProcessor("origon-audio-processor",K)})();
|
|
Binary file
|