@loop-voice-agent/web 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.
@@ -0,0 +1,78 @@
1
+ import type { WebCallSession } from "./controlPlane.js";
2
+ /**
3
+ * Transceiver ORDER is a contract with the server.
4
+ *
5
+ * The worker's SmallWebRTC connection reads tracks by m-line index — audio 0,
6
+ * camera 1, screen 2 — so the offer has to present them in exactly that order.
7
+ * A screen track offered at index 1 arrives labelled as the participant's
8
+ * camera, which is not an error anywhere: it simply records the wrong thing.
9
+ * That is why a screen share always creates the camera slot first, even on an
10
+ * audio-only call where nothing will ever fill it.
11
+ */
12
+ export interface SessionCallbacks {
13
+ /** A frame arrived on the data channel. Raw string; decoding happens above. */
14
+ readonly onMessage: (raw: string) => void;
15
+ /** Media is flowing. */
16
+ readonly onConnected: () => void;
17
+ /** The connection failed after being established, or never established. */
18
+ readonly onFailed: (reason: string) => void;
19
+ /** The remote side went away — a normal end as far as the transport knows. */
20
+ readonly onDisconnected: () => void;
21
+ /** A remote track arrived (agent audio, and the avatar video on video calls). */
22
+ readonly onTrack: (event: RTCTrackEvent) => void;
23
+ }
24
+ export interface SessionOptions {
25
+ readonly iceServers: RTCIceServer[];
26
+ readonly fetchImpl: (input: string, init?: RequestInit) => Promise<Response>;
27
+ }
28
+ /**
29
+ * A live media session. Owns exactly one `RTCPeerConnection` and the data
30
+ * channel on it, and can be torn down exactly once.
31
+ */
32
+ export declare class MediaSession {
33
+ private readonly options;
34
+ private readonly callbacks;
35
+ private pc;
36
+ private dataChannel;
37
+ private keepaliveTimer;
38
+ private closed;
39
+ /** The worker's handle for this peer connection; required to renegotiate. */
40
+ private pcId;
41
+ /** The transceiver carrying the screen track, once one is attached. */
42
+ private screenTransceiver;
43
+ private session;
44
+ constructor(options: SessionOptions, callbacks: SessionCallbacks);
45
+ /**
46
+ * Negotiate the media path for `session`, sending `localStream`'s audio.
47
+ *
48
+ * Resolves once the SDP answer has been applied. That is NOT the same as the
49
+ * call being connected — connection is signalled through `onConnected`, and
50
+ * the caller is responsible for bounding how long it waits for it.
51
+ */
52
+ connect(session: WebCallSession, localStream: MediaStream, wantVideo: boolean, screenStream?: MediaStream): Promise<void>;
53
+ /**
54
+ * Attach a pre-captured display stream to a call that is already running.
55
+ *
56
+ * "Pre-captured" is the point: the caller owns `getDisplayMedia`, so the
57
+ * browser's picker has already been shown and consented to. Capturing here
58
+ * instead would raise a SECOND picker mid-call, which is the exact behaviour
59
+ * the vendor wrapper this SDK replaces had to be bypassed to avoid.
60
+ */
61
+ addScreenShare(stream: MediaStream): Promise<void>;
62
+ /** Whether a screen track is currently attached. */
63
+ hasScreenShare(): boolean;
64
+ private attachScreenTrack;
65
+ /** Enable or disable the outgoing audio track. */
66
+ setMuted(stream: MediaStream, muted: boolean): void;
67
+ /** Tear down. Idempotent — safe to call from several paths at once. */
68
+ close(): void;
69
+ private wireDataChannel;
70
+ private handleIceStateChange;
71
+ /**
72
+ * Resolve when gathering completes OR the budget elapses, whichever is first.
73
+ * Waiting for completion unconditionally is what makes call setup feel slow
74
+ * behind a STUN server that is unreachable.
75
+ */
76
+ private waitForIceGathering;
77
+ private exchangeSdp;
78
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Hop 2: open the media path.
3
+ *
4
+ * The choreography here is ported from the two hand-rolled clients this package
5
+ * replaces, and several steps are load-bearing in ways that are not obvious
6
+ * from reading them. Each one is commented with what breaks without it, because
7
+ * every one of them was originally discovered by something breaking.
8
+ */
9
+ /** Frames sent to keep the data channel from being reclaimed while idle. */
10
+ const KEEPALIVE_FRAME = "ping";
11
+ const KEEPALIVE_INTERVAL_MS = 1_000;
12
+ /**
13
+ * How long to wait for ICE gathering before offering what we have.
14
+ *
15
+ * Gathering "completes" only after every configured STUN/TURN server has been
16
+ * tried or timed out. On a local or well-connected setup the host candidates
17
+ * are ready almost immediately and waiting for completion adds seconds of dead
18
+ * air to call setup for no benefit. Offering early is safe: trickle ICE lets
19
+ * later candidates still arrive.
20
+ */
21
+ const ICE_GATHER_TIMEOUT_MS = 1_200;
22
+ const trimSlash = (url) => url.replace(/\/+$/, "");
23
+ /**
24
+ * A live media session. Owns exactly one `RTCPeerConnection` and the data
25
+ * channel on it, and can be torn down exactly once.
26
+ */
27
+ export class MediaSession {
28
+ options;
29
+ callbacks;
30
+ pc = null;
31
+ dataChannel = null;
32
+ keepaliveTimer = null;
33
+ closed = false;
34
+ /** The worker's handle for this peer connection; required to renegotiate. */
35
+ pcId;
36
+ /** The transceiver carrying the screen track, once one is attached. */
37
+ screenTransceiver = null;
38
+ session = null;
39
+ constructor(options, callbacks) {
40
+ this.options = options;
41
+ this.callbacks = callbacks;
42
+ }
43
+ /**
44
+ * Negotiate the media path for `session`, sending `localStream`'s audio.
45
+ *
46
+ * Resolves once the SDP answer has been applied. That is NOT the same as the
47
+ * call being connected — connection is signalled through `onConnected`, and
48
+ * the caller is responsible for bounding how long it waits for it.
49
+ */
50
+ async connect(session, localStream, wantVideo, screenStream) {
51
+ const pc = new RTCPeerConnection({ iceServers: this.options.iceServers });
52
+ this.pc = pc;
53
+ this.session = session;
54
+ // Audio only from the local devices, deliberately, even on a video call:
55
+ // the agent's avatar is rendered server-side and is not vision-based, so
56
+ // uploading the camera would cost the user bandwidth to feed something that
57
+ // never looks at it. The local camera is for the user's own self-view.
58
+ for (const track of localStream.getAudioTracks())
59
+ pc.addTrack(track, localStream);
60
+ // Reserve the camera slot whenever video is in play at all, so a screen
61
+ // track lands at index 2 rather than being read as a camera. Recvonly:
62
+ // this is where the avatar arrives, and nothing is ever sent on it.
63
+ if (wantVideo || screenStream) {
64
+ pc.addTransceiver("video", { direction: "recvonly" });
65
+ }
66
+ if (screenStream)
67
+ this.attachScreenTrack(pc, screenStream);
68
+ // The data channel MUST be created before createOffer. A channel opened
69
+ // afterwards is absent from the offer's m=application section, and the
70
+ // server only adopts a channel the browser opened — so every transcript
71
+ // frame would be silently dropped for the life of the call.
72
+ this.wireDataChannel(pc);
73
+ pc.ontrack = (event) => this.callbacks.onTrack(event);
74
+ pc.oniceconnectionstatechange = () => this.handleIceStateChange(pc);
75
+ // The camera slot above already asks for inbound video when it is wanted,
76
+ // so the offer options only have to cover audio.
77
+ const offer = await pc.createOffer({ offerToReceiveAudio: true });
78
+ await pc.setLocalDescription(offer);
79
+ await this.waitForIceGathering(pc);
80
+ // Teardown may have run while we were gathering; if the connection we are
81
+ // holding is no longer the current one, this attempt has been superseded
82
+ // and must not go on to exchange SDP for a call nobody is waiting for.
83
+ if (this.pc !== pc || this.closed)
84
+ return;
85
+ const local = pc.localDescription;
86
+ if (!local)
87
+ throw new Error("local SDP missing after ICE gathering");
88
+ const answer = await this.exchangeSdp(session, local.sdp);
89
+ if (this.pc !== pc || this.closed)
90
+ return;
91
+ // Kept so a later screen share renegotiates THIS connection instead of
92
+ // asking the worker to open a second call for a browser that has one.
93
+ this.pcId = answer.pc_id;
94
+ await pc.setRemoteDescription({ type: "answer", sdp: answer.sdp });
95
+ }
96
+ /**
97
+ * Attach a pre-captured display stream to a call that is already running.
98
+ *
99
+ * "Pre-captured" is the point: the caller owns `getDisplayMedia`, so the
100
+ * browser's picker has already been shown and consented to. Capturing here
101
+ * instead would raise a SECOND picker mid-call, which is the exact behaviour
102
+ * the vendor wrapper this SDK replaces had to be bypassed to avoid.
103
+ */
104
+ async addScreenShare(stream) {
105
+ const pc = this.pc;
106
+ if (!pc || this.closed)
107
+ throw new Error("no active call to share a screen with");
108
+ if (this.screenTransceiver)
109
+ throw new Error("a screen share is already attached");
110
+ if (!this.session)
111
+ throw new Error("no session to renegotiate against");
112
+ this.attachScreenTrack(pc, stream);
113
+ const offer = await pc.createOffer();
114
+ await pc.setLocalDescription(offer);
115
+ await this.waitForIceGathering(pc);
116
+ if (this.pc !== pc || this.closed)
117
+ return;
118
+ const local = pc.localDescription;
119
+ if (!local)
120
+ throw new Error("local SDP missing after ICE gathering");
121
+ const answer = await this.exchangeSdp(this.session, local.sdp, this.pcId);
122
+ if (this.pc !== pc || this.closed)
123
+ return;
124
+ await pc.setRemoteDescription({ type: "answer", sdp: answer.sdp });
125
+ }
126
+ /** Whether a screen track is currently attached. */
127
+ hasScreenShare() {
128
+ return this.screenTransceiver !== null;
129
+ }
130
+ attachScreenTrack(pc, stream) {
131
+ const [track] = stream.getVideoTracks();
132
+ if (!track)
133
+ throw new Error("the screen stream carries no video track");
134
+ this.screenTransceiver = pc.addTransceiver(track, { direction: "sendonly" });
135
+ }
136
+ /** Enable or disable the outgoing audio track. */
137
+ setMuted(stream, muted) {
138
+ // Toggling `enabled` keeps the transport open and sends silence, rather
139
+ // than stopping the track. A stopped track cannot be restarted without
140
+ // renegotiating, so muting would be a one-way door.
141
+ for (const track of stream.getAudioTracks())
142
+ track.enabled = !muted;
143
+ }
144
+ /** Tear down. Idempotent — safe to call from several paths at once. */
145
+ close() {
146
+ this.closed = true;
147
+ if (this.keepaliveTimer !== null) {
148
+ clearInterval(this.keepaliveTimer);
149
+ this.keepaliveTimer = null;
150
+ }
151
+ if (this.dataChannel) {
152
+ try {
153
+ this.dataChannel.close();
154
+ }
155
+ catch {
156
+ // Already closing — nothing to do.
157
+ }
158
+ this.dataChannel = null;
159
+ }
160
+ if (this.pc) {
161
+ try {
162
+ this.pc.close();
163
+ }
164
+ catch {
165
+ // Already closed — nothing to do.
166
+ }
167
+ this.pc = null;
168
+ }
169
+ this.screenTransceiver = null;
170
+ this.session = null;
171
+ this.pcId = undefined;
172
+ }
173
+ wireDataChannel(pc) {
174
+ const channel = pc.createDataChannel("chat");
175
+ this.dataChannel = channel;
176
+ channel.onmessage = (event) => {
177
+ if (typeof event.data === "string")
178
+ this.callbacks.onMessage(event.data);
179
+ };
180
+ channel.onopen = () => {
181
+ if (this.keepaliveTimer !== null)
182
+ clearInterval(this.keepaliveTimer);
183
+ this.keepaliveTimer = setInterval(() => {
184
+ if (channel.readyState !== "open")
185
+ return;
186
+ try {
187
+ channel.send(KEEPALIVE_FRAME);
188
+ }
189
+ catch {
190
+ // The channel is closing under us; the state machine will notice.
191
+ }
192
+ }, KEEPALIVE_INTERVAL_MS);
193
+ };
194
+ }
195
+ handleIceStateChange(pc) {
196
+ if (this.pc !== pc)
197
+ return; // superseded attempt
198
+ switch (pc.iceConnectionState) {
199
+ case "connected":
200
+ case "completed":
201
+ this.callbacks.onConnected();
202
+ break;
203
+ case "failed":
204
+ this.callbacks.onFailed("ICE connection failed");
205
+ break;
206
+ case "disconnected":
207
+ case "closed":
208
+ this.callbacks.onDisconnected();
209
+ break;
210
+ default:
211
+ break;
212
+ }
213
+ }
214
+ /**
215
+ * Resolve when gathering completes OR the budget elapses, whichever is first.
216
+ * Waiting for completion unconditionally is what makes call setup feel slow
217
+ * behind a STUN server that is unreachable.
218
+ */
219
+ async waitForIceGathering(pc) {
220
+ if (pc.iceGatheringState === "complete")
221
+ return;
222
+ await new Promise((resolve) => {
223
+ let settled = false;
224
+ const finish = () => {
225
+ if (settled)
226
+ return;
227
+ settled = true;
228
+ pc.removeEventListener("icegatheringstatechange", onStateChange);
229
+ clearTimeout(timer);
230
+ resolve();
231
+ };
232
+ const onStateChange = () => {
233
+ if (pc.iceGatheringState === "complete")
234
+ finish();
235
+ };
236
+ pc.addEventListener("icegatheringstatechange", onStateChange);
237
+ const timer = setTimeout(finish, ICE_GATHER_TIMEOUT_MS);
238
+ });
239
+ }
240
+ async exchangeSdp(session, sdp, pcId) {
241
+ const url = `${trimSlash(session.voiceWorkerUrl)}/v1/calls/web/sessions`;
242
+ // No Authorization header: the session token in the body IS the credential
243
+ // for this hop, and it is bound to this one call and short-lived.
244
+ const response = await this.options.fetchImpl(url, {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify({
248
+ sdp,
249
+ type: "offer",
250
+ callId: session.callId,
251
+ sessionToken: session.sessionToken,
252
+ ...(pcId ? { pc_id: pcId } : {}),
253
+ }),
254
+ });
255
+ if (!response.ok) {
256
+ const detail = await response.text().catch(() => "");
257
+ throw new Error(`${url} returned ${response.status}: ${detail.slice(0, 300)}`);
258
+ }
259
+ const json = await response.json().catch(() => null);
260
+ if (typeof json !== "object" ||
261
+ json === null ||
262
+ typeof json.sdp !== "string") {
263
+ throw new Error("Unexpected SDP answer shape from the voice worker");
264
+ }
265
+ return json;
266
+ }
267
+ }
268
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@loop-voice-agent/web",
3
+ "version": "0.1.0",
4
+ "description": "Browser SDK for the Loop Voice Agent platform — start a voice call from a publishable key and an agent id.",
5
+ "keywords": [
6
+ "voice",
7
+ "webrtc",
8
+ "voice-agent",
9
+ "sdk"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/adminloopmethods/voiceAgent.git",
15
+ "directory": "packages/web-sdk"
16
+ },
17
+ "type": "module",
18
+ "sideEffects": false,
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "LICENSE",
30
+ "README.md",
31
+ "dist"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "prepare": "npm run build",
40
+ "prepublishOnly": "npm run build",
41
+ "test": "vitest run",
42
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
43
+ "lint": "eslint . --max-warnings 0",
44
+ "lint:fix": "eslint . --fix",
45
+ "format": "prettier --write --ignore-path ../../.prettierignore .",
46
+ "format:check": "prettier --check --ignore-path ../../.prettierignore ."
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.0.0",
50
+ "typescript": "^5.6.0",
51
+ "vite": "7",
52
+ "vitest": "^4.1.6"
53
+ }
54
+ }