@nuancelabs/js-sdk 0.15.0 → 0.34.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 +202 -0
- package/NOTICE +4 -0
- package/index.d.ts +257 -348
- package/index.js +859 -577
- package/package.json +2 -2
- package/react/index.d.ts +3 -3
- package/react/index.js +6 -1
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ function isRecord(value) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
// src/core/sdk-version.ts
|
|
7
|
-
var SDK_HEADER = `js/${"0.
|
|
7
|
+
var SDK_HEADER = `js/${"0.34.0"}`;
|
|
8
8
|
|
|
9
9
|
// src/utils.ts
|
|
10
10
|
function normalizeHost(host) {
|
|
@@ -12,10 +12,13 @@ function normalizeHost(host) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// src/api/client.ts
|
|
15
|
+
var NETWORK_RETRY_DELAYS_MS = [300, 1200];
|
|
16
|
+
|
|
15
17
|
class ApiError extends Error {
|
|
16
18
|
status;
|
|
17
19
|
code;
|
|
18
20
|
retryAfterSeconds;
|
|
21
|
+
renewsAt;
|
|
19
22
|
body;
|
|
20
23
|
constructor(status, detail, body = null) {
|
|
21
24
|
super(detail);
|
|
@@ -24,6 +27,7 @@ class ApiError extends Error {
|
|
|
24
27
|
this.body = body;
|
|
25
28
|
this.code = isRecord(body) && typeof body.code === "string" ? body.code : null;
|
|
26
29
|
this.retryAfterSeconds = isRecord(body) && typeof body.retry_after_seconds === "number" ? body.retry_after_seconds : null;
|
|
30
|
+
this.renewsAt = isRecord(body) && typeof body.renews_at === "string" ? body.renews_at : null;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -31,18 +35,26 @@ class NuanceApiClient {
|
|
|
31
35
|
host;
|
|
32
36
|
headersProvider;
|
|
33
37
|
tokenProvider;
|
|
38
|
+
networkRetryDelaysMs;
|
|
34
39
|
tokens = new Map;
|
|
35
40
|
constructor(config) {
|
|
36
41
|
this.host = normalizeHost(config.host);
|
|
37
42
|
this.headersProvider = config.headers;
|
|
38
43
|
this.tokenProvider = config.token;
|
|
44
|
+
this.networkRetryDelaysMs = config.networkRetryDelaysMs ?? NETWORK_RETRY_DELAYS_MS;
|
|
39
45
|
}
|
|
40
46
|
async createConversation(request) {
|
|
41
47
|
const token = this.tokenProvider ? await this.tokenProvider() : undefined;
|
|
42
48
|
if (token !== undefined && (typeof token !== "string" || token === "")) {
|
|
43
49
|
throw new TypeError(`The token option must resolve to the session token string, but it resolved to ${token === "" ? "an empty string" : typeof token}. Return the "token" field of your endpoint's response.`);
|
|
44
50
|
}
|
|
45
|
-
const conversation = await this.request("/conversation", {
|
|
51
|
+
const conversation = await this.request("/conversation", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: request,
|
|
54
|
+
token,
|
|
55
|
+
page: pageHeaders(),
|
|
56
|
+
replaySafe: (headers) => !isApiKey(headers)
|
|
57
|
+
});
|
|
46
58
|
if (token !== undefined)
|
|
47
59
|
this.tokens.set(conversation.id, token);
|
|
48
60
|
return conversation;
|
|
@@ -69,18 +81,21 @@ class NuanceApiClient {
|
|
|
69
81
|
async request(path, options) {
|
|
70
82
|
const headers = new Headers(await this.resolveHeaders());
|
|
71
83
|
headers.set("X-Nuance-SDK", SDK_HEADER);
|
|
84
|
+
for (const [name, value] of Object.entries(options?.page ?? {})) {
|
|
85
|
+
headers.set(name, value);
|
|
86
|
+
}
|
|
72
87
|
if (options?.token !== undefined) {
|
|
73
88
|
headers.set("Authorization", `Bearer ${options.token}`);
|
|
74
89
|
}
|
|
75
90
|
if (options?.body !== undefined) {
|
|
76
91
|
headers.set("Content-Type", "application/json");
|
|
77
92
|
}
|
|
78
|
-
const response = await
|
|
93
|
+
const response = await this.send(`${this.host}${path}`, {
|
|
79
94
|
method: options?.method ?? "GET",
|
|
80
95
|
headers,
|
|
81
96
|
body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
82
97
|
signal: options?.signal
|
|
83
|
-
});
|
|
98
|
+
}, options?.replaySafe?.(headers) ?? true);
|
|
84
99
|
if (!response.ok) {
|
|
85
100
|
let detail = response.statusText;
|
|
86
101
|
let body = null;
|
|
@@ -99,10 +114,51 @@ class NuanceApiClient {
|
|
|
99
114
|
}
|
|
100
115
|
return response.json();
|
|
101
116
|
}
|
|
117
|
+
async send(url, init, replaySafe) {
|
|
118
|
+
for (let retry = 0;; retry++) {
|
|
119
|
+
try {
|
|
120
|
+
return await fetch(url, init);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const delayMs = this.networkRetryDelaysMs[retry];
|
|
123
|
+
if (!replaySafe || delayMs === undefined || !isNetworkFailure(error) || init.signal?.aborted) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
127
|
+
if (init.signal?.aborted)
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
102
132
|
async resolveHeaders() {
|
|
103
133
|
return this.headersProvider ? await this.headersProvider() : {};
|
|
104
134
|
}
|
|
105
135
|
}
|
|
136
|
+
function isNetworkFailure(error) {
|
|
137
|
+
return error instanceof TypeError;
|
|
138
|
+
}
|
|
139
|
+
function isApiKey(headers) {
|
|
140
|
+
return headers.get("Authorization")?.startsWith("Bearer nk_") ?? false;
|
|
141
|
+
}
|
|
142
|
+
function pageHeaders(location = globalThis.location, referrer = globalThis.document?.referrer) {
|
|
143
|
+
const headers = {};
|
|
144
|
+
const page = location && withoutQuery(location.href);
|
|
145
|
+
if (page)
|
|
146
|
+
headers["X-Nuance-Page"] = page;
|
|
147
|
+
const from = referrer && withoutQuery(referrer);
|
|
148
|
+
if (from)
|
|
149
|
+
headers["X-Nuance-Referrer"] = from;
|
|
150
|
+
return headers;
|
|
151
|
+
}
|
|
152
|
+
function withoutQuery(href) {
|
|
153
|
+
try {
|
|
154
|
+
const url = new URL(href);
|
|
155
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
156
|
+
return;
|
|
157
|
+
return `${url.origin}${url.pathname}`.slice(0, 512);
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
106
162
|
// src/core/active-session-error.ts
|
|
107
163
|
class ActiveSessionError extends Error {
|
|
108
164
|
constructor() {
|
|
@@ -110,7 +166,6 @@ class ActiveSessionError extends Error {
|
|
|
110
166
|
this.name = "ActiveSessionError";
|
|
111
167
|
}
|
|
112
168
|
}
|
|
113
|
-
|
|
114
169
|
// src/core/guest-limit-error.ts
|
|
115
170
|
class GuestLimitError extends Error {
|
|
116
171
|
reason;
|
|
@@ -120,7 +175,6 @@ class GuestLimitError extends Error {
|
|
|
120
175
|
this.reason = reason;
|
|
121
176
|
}
|
|
122
177
|
}
|
|
123
|
-
|
|
124
178
|
// src/core/legal-required-error.ts
|
|
125
179
|
class LegalRequiredError extends Error {
|
|
126
180
|
constructor() {
|
|
@@ -128,7 +182,6 @@ class LegalRequiredError extends Error {
|
|
|
128
182
|
this.name = "LegalRequiredError";
|
|
129
183
|
}
|
|
130
184
|
}
|
|
131
|
-
|
|
132
185
|
// src/core/maintenance-error.ts
|
|
133
186
|
class MaintenanceError extends Error {
|
|
134
187
|
constructor(message) {
|
|
@@ -136,7 +189,6 @@ class MaintenanceError extends Error {
|
|
|
136
189
|
this.name = "MaintenanceError";
|
|
137
190
|
}
|
|
138
191
|
}
|
|
139
|
-
|
|
140
192
|
// src/core/media-error.ts
|
|
141
193
|
function reasonFor(cause) {
|
|
142
194
|
const name = cause instanceof Error ? cause.name : "";
|
|
@@ -177,24 +229,146 @@ async function ensureMicrophoneAccess(deviceId) {
|
|
|
177
229
|
throw new MediaAccessError(error, "microphone");
|
|
178
230
|
}
|
|
179
231
|
}
|
|
180
|
-
|
|
181
232
|
// src/core/policy-curfew-error.ts
|
|
182
233
|
class PolicyCurfewError extends Error {
|
|
183
234
|
retryAfterSeconds;
|
|
184
235
|
constructor(retryAfterSeconds) {
|
|
185
|
-
super(
|
|
236
|
+
super(curfewMessage(retryAfterSeconds));
|
|
186
237
|
this.name = "PolicyCurfewError";
|
|
187
238
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
188
239
|
}
|
|
189
240
|
}
|
|
241
|
+
function curfewMessage(retryAfterSeconds) {
|
|
242
|
+
const paused = "New conversations are paused after repeated violations of our Acceptable Use Policy.";
|
|
243
|
+
if (retryAfterSeconds === null || !(retryAfterSeconds > 0)) {
|
|
244
|
+
return `${paused} Try again later.`;
|
|
245
|
+
}
|
|
246
|
+
const hours = Math.max(1, Math.ceil(retryAfterSeconds / 3600));
|
|
247
|
+
return `${paused} Try again in about ${hours} ${hours === 1 ? "hour" : "hours"}.`;
|
|
248
|
+
}
|
|
249
|
+
// src/core/types.ts
|
|
250
|
+
var ConversationState;
|
|
251
|
+
((ConversationState2) => {
|
|
252
|
+
ConversationState2["Idle"] = "idle";
|
|
253
|
+
ConversationState2["Queued"] = "queued";
|
|
254
|
+
ConversationState2["Connecting"] = "connecting";
|
|
255
|
+
ConversationState2["Connected"] = "connected";
|
|
256
|
+
ConversationState2["Reconnecting"] = "reconnecting";
|
|
257
|
+
ConversationState2["Disconnected"] = "disconnected";
|
|
258
|
+
ConversationState2["Failed"] = "failed";
|
|
259
|
+
})(ConversationState ||= {});
|
|
260
|
+
// src/core/camera.ts
|
|
261
|
+
function cameraFacing(track) {
|
|
262
|
+
const facing = track.getSettings().facingMode;
|
|
263
|
+
return facing === "user" || facing === "environment" ? facing : null;
|
|
264
|
+
}
|
|
265
|
+
function requireCameraFacing(track, facing) {
|
|
266
|
+
const requested = typeof facing === "string" ? facing : !Array.isArray(facing) ? facing?.exact : undefined;
|
|
267
|
+
if (typeof requested !== "string")
|
|
268
|
+
return;
|
|
269
|
+
if (!track || track.getSettings().facingMode !== requested)
|
|
270
|
+
throw new DOMException("Requested camera direction is unavailable", "OverconstrainedError");
|
|
271
|
+
}
|
|
272
|
+
var BUILT_IN = /facetime|built-?in|integrated|internal|macbook|user[- ]facing|front[- ]facing/i;
|
|
273
|
+
var PHONE = /iphone|android|continuity|desk view/i;
|
|
274
|
+
function classifyCamera(label, phone, facing) {
|
|
275
|
+
if (phone)
|
|
276
|
+
return facing === "environment" ? "phone_back" : "phone_front";
|
|
277
|
+
if (!label)
|
|
278
|
+
return;
|
|
279
|
+
if (PHONE.test(label))
|
|
280
|
+
return "phone_back";
|
|
281
|
+
return BUILT_IN.test(label) ? "built_in" : "external";
|
|
282
|
+
}
|
|
283
|
+
function onPhone() {
|
|
284
|
+
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
|
285
|
+
}
|
|
286
|
+
async function cameraSource(preferences) {
|
|
287
|
+
if (preferences.cameraEnabled === false)
|
|
288
|
+
return "off";
|
|
289
|
+
if (typeof navigator === "undefined")
|
|
290
|
+
return;
|
|
291
|
+
if (onPhone())
|
|
292
|
+
return classifyCamera("", true, preferences.cameraFacing);
|
|
293
|
+
try {
|
|
294
|
+
const cameras = (await navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === "videoinput");
|
|
295
|
+
const camera = cameras.find((device) => device.deviceId === preferences.cameraId) ?? cameras[0];
|
|
296
|
+
return camera ? classifyCamera(camera.label, false, null) : undefined;
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
190
301
|
|
|
191
|
-
// src/core/
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
302
|
+
// src/core/dispatch.ts
|
|
303
|
+
function deliver(handler, event) {
|
|
304
|
+
try {
|
|
305
|
+
handler(event);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
report(error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function report(error) {
|
|
311
|
+
if (typeof globalThis.reportError === "function") {
|
|
312
|
+
globalThis.reportError(error);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
console.error("Nuance SDK: an event handler threw", error);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/core/end-reason.ts
|
|
319
|
+
var FROM_V1 = {
|
|
320
|
+
client_ended: "close_requested",
|
|
321
|
+
input_end: "close_requested",
|
|
322
|
+
time_limit: "expired",
|
|
323
|
+
max_pause: "expired",
|
|
324
|
+
policy_violation: "content",
|
|
325
|
+
age_check: "age_check",
|
|
326
|
+
agent_ended: "end_call",
|
|
327
|
+
runtime_error: "error",
|
|
328
|
+
capacity_lost: "error",
|
|
329
|
+
job_ended: "error",
|
|
330
|
+
pacer_failed: "error",
|
|
331
|
+
output_not_ready: "error",
|
|
332
|
+
client_gone: "connection_lost",
|
|
333
|
+
client_stalled: "connection_lost",
|
|
334
|
+
closed: "connection_lost"
|
|
335
|
+
};
|
|
336
|
+
var RETIRED = {
|
|
337
|
+
remote_hangup: "end_call"
|
|
338
|
+
};
|
|
339
|
+
var KNOWN = new Set([
|
|
340
|
+
"close_requested",
|
|
341
|
+
"content",
|
|
342
|
+
"age_check",
|
|
343
|
+
"expired",
|
|
344
|
+
"end_call",
|
|
345
|
+
"error",
|
|
346
|
+
"connection_lost"
|
|
347
|
+
]);
|
|
348
|
+
function endReasonOf(reason) {
|
|
349
|
+
if (typeof reason !== "string" || reason === "")
|
|
350
|
+
return;
|
|
351
|
+
if (KNOWN.has(reason))
|
|
352
|
+
return reason;
|
|
353
|
+
return RETIRED[reason] ?? reason;
|
|
354
|
+
}
|
|
355
|
+
function v1EndReason(reason) {
|
|
356
|
+
if (typeof reason !== "string" || reason === "")
|
|
357
|
+
return;
|
|
358
|
+
if (KNOWN.has(reason))
|
|
359
|
+
return reason;
|
|
360
|
+
return FROM_V1[reason] ?? "error";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/core/resolvers.ts
|
|
364
|
+
function resolvers() {
|
|
365
|
+
let resolve;
|
|
366
|
+
let reject;
|
|
367
|
+
const promise = new Promise((settle, fail) => {
|
|
368
|
+
resolve = settle;
|
|
369
|
+
reject = fail;
|
|
370
|
+
});
|
|
371
|
+
return { promise, resolve, reject };
|
|
198
372
|
}
|
|
199
373
|
|
|
200
374
|
// src/core/ticker.ts
|
|
@@ -248,11 +422,172 @@ var pageTicker = backgroundTicker(globalThis);
|
|
|
248
422
|
// src/providers/livekit/conversation.ts
|
|
249
423
|
import {
|
|
250
424
|
ConnectionQuality,
|
|
251
|
-
ConnectionState as
|
|
425
|
+
ConnectionState as ConnectionState3,
|
|
426
|
+
ParticipantKind,
|
|
252
427
|
Room,
|
|
253
|
-
RoomEvent as
|
|
428
|
+
RoomEvent as RoomEvent4
|
|
254
429
|
} from "livekit-client";
|
|
255
430
|
|
|
431
|
+
// src/providers/livekit/attach.ts
|
|
432
|
+
function isAttach(participant) {
|
|
433
|
+
if (participant === undefined)
|
|
434
|
+
return false;
|
|
435
|
+
return participant.identity.startsWith("attach-") || participant.attributes?.["nuance.attach"] === "1";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/providers/livekit/audio-backlog.ts
|
|
439
|
+
import {
|
|
440
|
+
ConnectionState,
|
|
441
|
+
RoomEvent,
|
|
442
|
+
Track
|
|
443
|
+
} from "livekit-client";
|
|
444
|
+
var BACKLOG_MS = 300;
|
|
445
|
+
var AUDIO_BACKLOG_TIMING = {
|
|
446
|
+
sampleMs: 500,
|
|
447
|
+
stuckSamples: 2,
|
|
448
|
+
cooldownMs: 1e4,
|
|
449
|
+
maxCooldownMs: 120000
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
class AudioBacklogWatch {
|
|
453
|
+
read;
|
|
454
|
+
drop;
|
|
455
|
+
timing;
|
|
456
|
+
now;
|
|
457
|
+
timer = null;
|
|
458
|
+
previous = null;
|
|
459
|
+
stuck = 0;
|
|
460
|
+
droppedAt = null;
|
|
461
|
+
cooldown;
|
|
462
|
+
sampling = false;
|
|
463
|
+
constructor(read, drop, timing = AUDIO_BACKLOG_TIMING, now = () => performance.now()) {
|
|
464
|
+
this.read = read;
|
|
465
|
+
this.drop = drop;
|
|
466
|
+
this.timing = timing;
|
|
467
|
+
this.now = now;
|
|
468
|
+
this.cooldown = timing.cooldownMs;
|
|
469
|
+
}
|
|
470
|
+
start() {
|
|
471
|
+
if (this.timer !== null)
|
|
472
|
+
return;
|
|
473
|
+
this.timer = setInterval(() => {
|
|
474
|
+
this.sample();
|
|
475
|
+
}, this.timing.sampleMs);
|
|
476
|
+
}
|
|
477
|
+
stop() {
|
|
478
|
+
if (this.timer !== null) {
|
|
479
|
+
clearInterval(this.timer);
|
|
480
|
+
this.timer = null;
|
|
481
|
+
}
|
|
482
|
+
this.previous = null;
|
|
483
|
+
this.stuck = 0;
|
|
484
|
+
}
|
|
485
|
+
async sample() {
|
|
486
|
+
if (this.sampling || this.timer === null)
|
|
487
|
+
return;
|
|
488
|
+
this.sampling = true;
|
|
489
|
+
try {
|
|
490
|
+
const buffer = await this.read().catch(() => null);
|
|
491
|
+
if (this.timer === null)
|
|
492
|
+
return;
|
|
493
|
+
this.observe(buffer, this.now());
|
|
494
|
+
} finally {
|
|
495
|
+
this.sampling = false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
observe(buffer, now) {
|
|
499
|
+
const previous = this.previous;
|
|
500
|
+
this.previous = buffer;
|
|
501
|
+
if (buffer === null || previous === null || buffer.id !== previous.id) {
|
|
502
|
+
this.stuck = 0;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const emitted = buffer.emitted - previous.emitted;
|
|
506
|
+
if (emitted <= 0)
|
|
507
|
+
return;
|
|
508
|
+
const backlog = {
|
|
509
|
+
jitterBufferMs: (buffer.delay - previous.delay) / emitted * 1000,
|
|
510
|
+
targetMs: (buffer.target - previous.target) / emitted * 1000
|
|
511
|
+
};
|
|
512
|
+
const behind = backlog.jitterBufferMs - backlog.targetMs > BACKLOG_MS;
|
|
513
|
+
this.stuck = behind ? this.stuck + 1 : 0;
|
|
514
|
+
if (this.stuck < this.timing.stuckSamples)
|
|
515
|
+
return;
|
|
516
|
+
if (this.droppedAt !== null) {
|
|
517
|
+
if (now - this.droppedAt < this.cooldown)
|
|
518
|
+
return;
|
|
519
|
+
this.cooldown = Math.min(this.cooldown * 2, this.timing.maxCooldownMs);
|
|
520
|
+
}
|
|
521
|
+
this.stuck = 0;
|
|
522
|
+
this.droppedAt = now;
|
|
523
|
+
this.previous = null;
|
|
524
|
+
this.drop(backlog);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function avatarAudio(room) {
|
|
528
|
+
for (const participant of room.remoteParticipants.values()) {
|
|
529
|
+
if (isAttach(participant))
|
|
530
|
+
continue;
|
|
531
|
+
for (const publication of participant.trackPublications.values()) {
|
|
532
|
+
if (publication.kind === Track.Kind.Audio && publication.isSubscribed) {
|
|
533
|
+
return publication;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function readJitterBuffer(report2) {
|
|
540
|
+
let buffer = null;
|
|
541
|
+
report2?.forEach((stat) => {
|
|
542
|
+
if (stat.type !== "inbound-rtp" || stat.kind !== "audio")
|
|
543
|
+
return;
|
|
544
|
+
const {
|
|
545
|
+
id,
|
|
546
|
+
jitterBufferDelay,
|
|
547
|
+
jitterBufferTargetDelay,
|
|
548
|
+
jitterBufferEmittedCount
|
|
549
|
+
} = stat;
|
|
550
|
+
if (typeof id === "string" && typeof jitterBufferDelay === "number" && typeof jitterBufferTargetDelay === "number" && typeof jitterBufferEmittedCount === "number") {
|
|
551
|
+
buffer = {
|
|
552
|
+
id,
|
|
553
|
+
delay: jitterBufferDelay,
|
|
554
|
+
target: jitterBufferTargetDelay,
|
|
555
|
+
emitted: jitterBufferEmittedCount
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
return buffer;
|
|
560
|
+
}
|
|
561
|
+
async function avatarJitterBuffer(room) {
|
|
562
|
+
const track = avatarAudio(room)?.track;
|
|
563
|
+
if (!track)
|
|
564
|
+
return null;
|
|
565
|
+
return readJitterBuffer(await track.getRTCStatsReport());
|
|
566
|
+
}
|
|
567
|
+
var UNSUBSCRIBE_WAIT_MS = 2000;
|
|
568
|
+
async function resubscribeAvatarAudio(room) {
|
|
569
|
+
const publication = avatarAudio(room);
|
|
570
|
+
if (!publication)
|
|
571
|
+
return;
|
|
572
|
+
await new Promise((resolve) => {
|
|
573
|
+
const done = () => {
|
|
574
|
+
clearTimeout(timer);
|
|
575
|
+
room.off(RoomEvent.TrackUnsubscribed, unsubscribed);
|
|
576
|
+
resolve();
|
|
577
|
+
};
|
|
578
|
+
const unsubscribed = (_track, gone) => {
|
|
579
|
+
if (gone === publication)
|
|
580
|
+
done();
|
|
581
|
+
};
|
|
582
|
+
const timer = setTimeout(done, UNSUBSCRIBE_WAIT_MS);
|
|
583
|
+
room.on(RoomEvent.TrackUnsubscribed, unsubscribed);
|
|
584
|
+
publication.setSubscribed(false);
|
|
585
|
+
});
|
|
586
|
+
if (room.state === ConnectionState.Disconnected)
|
|
587
|
+
return;
|
|
588
|
+
publication.setSubscribed(true);
|
|
589
|
+
}
|
|
590
|
+
|
|
256
591
|
// src/providers/livekit/tracks.ts
|
|
257
592
|
class LiveKitVideoTrack {
|
|
258
593
|
track;
|
|
@@ -323,6 +658,9 @@ class LiveKitAvatarParticipant {
|
|
|
323
658
|
constructor(participant) {
|
|
324
659
|
this.participant = participant;
|
|
325
660
|
}
|
|
661
|
+
get identity() {
|
|
662
|
+
return this.participant?.identity ?? null;
|
|
663
|
+
}
|
|
326
664
|
updateParticipant(participant, keepReady = false) {
|
|
327
665
|
if (!keepReady && (participant === null || this.participant !== null && participant !== this.participant)) {
|
|
328
666
|
this._ready = false;
|
|
@@ -394,7 +732,7 @@ class LiveKitAvatarParticipant {
|
|
|
394
732
|
}
|
|
395
733
|
|
|
396
734
|
// src/providers/livekit/layer-fallback.ts
|
|
397
|
-
import { Track, VideoQuality } from "livekit-client";
|
|
735
|
+
import { Track as Track2, VideoQuality } from "livekit-client";
|
|
398
736
|
var LAYER_FALLBACK_TIMING = {
|
|
399
737
|
sampleMs: 1000,
|
|
400
738
|
slowFps: 12,
|
|
@@ -478,14 +816,16 @@ async function avatarVideoRate(room) {
|
|
|
478
816
|
if (typeof document !== "undefined" && document.hidden)
|
|
479
817
|
return null;
|
|
480
818
|
for (const participant of room.remoteParticipants.values()) {
|
|
481
|
-
|
|
819
|
+
if (isAttach(participant))
|
|
820
|
+
continue;
|
|
821
|
+
const publication = participant.getTrackPublication(Track2.Source.Camera);
|
|
482
822
|
const track = publication?.track;
|
|
483
823
|
if (!publication?.isSubscribed || !track)
|
|
484
824
|
continue;
|
|
485
|
-
const
|
|
825
|
+
const report2 = await track.getRTCStatsReport();
|
|
486
826
|
let width = null;
|
|
487
827
|
let framesDecoded = null;
|
|
488
|
-
|
|
828
|
+
report2?.forEach((stat) => {
|
|
489
829
|
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
490
830
|
width = typeof stat.frameWidth === "number" ? stat.frameWidth : null;
|
|
491
831
|
framesDecoded = typeof stat.framesDecoded === "number" ? stat.framesDecoded : null;
|
|
@@ -502,7 +842,9 @@ async function avatarVideoRate(room) {
|
|
|
502
842
|
}
|
|
503
843
|
function pinAvatarLayer(room, layer) {
|
|
504
844
|
for (const participant of room.remoteParticipants.values()) {
|
|
505
|
-
|
|
845
|
+
if (isAttach(participant))
|
|
846
|
+
continue;
|
|
847
|
+
const publication = participant.getTrackPublication(Track2.Source.Camera);
|
|
506
848
|
if (!publication?.isSubscribed)
|
|
507
849
|
continue;
|
|
508
850
|
publication.setVideoQuality(layer === "low" ? VideoQuality.LOW : VideoQuality.HIGH);
|
|
@@ -512,10 +854,10 @@ function pinAvatarLayer(room, layer) {
|
|
|
512
854
|
|
|
513
855
|
// src/providers/livekit/network-stats.ts
|
|
514
856
|
import {
|
|
515
|
-
ConnectionState,
|
|
857
|
+
ConnectionState as ConnectionState2,
|
|
516
858
|
ParticipantEvent,
|
|
517
|
-
RoomEvent,
|
|
518
|
-
Track as
|
|
859
|
+
RoomEvent as RoomEvent2,
|
|
860
|
+
Track as Track3
|
|
519
861
|
} from "livekit-client";
|
|
520
862
|
|
|
521
863
|
// src/providers/livekit/mic-silence.ts
|
|
@@ -582,9 +924,9 @@ function num(value) {
|
|
|
582
924
|
function str(value) {
|
|
583
925
|
return typeof value === "string" && value ? value : null;
|
|
584
926
|
}
|
|
585
|
-
function entries(
|
|
927
|
+
function entries(report2) {
|
|
586
928
|
const out = [];
|
|
587
|
-
|
|
929
|
+
report2.forEach((entry) => {
|
|
588
930
|
out.push(entry);
|
|
589
931
|
});
|
|
590
932
|
return out;
|
|
@@ -709,7 +1051,7 @@ function buildSample(reports, hidden) {
|
|
|
709
1051
|
reports.avatarVideo,
|
|
710
1052
|
reports.mic,
|
|
711
1053
|
reports.camera
|
|
712
|
-
].map((
|
|
1054
|
+
].map((report2) => report2 ? entries(report2) : null);
|
|
713
1055
|
const byId = new Map;
|
|
714
1056
|
for (const list of perReport) {
|
|
715
1057
|
if (list)
|
|
@@ -728,7 +1070,7 @@ function buildSample(reports, hidden) {
|
|
|
728
1070
|
}
|
|
729
1071
|
const inboundOf = (list) => list ? digestInbound(list.find((entry) => entry.type === "inbound-rtp"), byId) : null;
|
|
730
1072
|
const outboundOf = (list) => list ? digestOutbound(list.find((entry) => entry.type === "outbound-rtp" && entry.active !== false), byId) : null;
|
|
731
|
-
const
|
|
1073
|
+
const avatarAudio2 = inboundOf(perReport[0]);
|
|
732
1074
|
const avatarVideo = inboundOf(perReport[1]);
|
|
733
1075
|
const mic = outboundOf(perReport[2]);
|
|
734
1076
|
if (mic && reports.micTrack) {
|
|
@@ -736,17 +1078,18 @@ function buildSample(reports, hidden) {
|
|
|
736
1078
|
mic.track_ready_state = reports.micTrack.readyState;
|
|
737
1079
|
mic.track_enabled = reports.micTrack.enabled;
|
|
738
1080
|
}
|
|
739
|
-
const audioPlayout =
|
|
1081
|
+
const audioPlayout = avatarAudio2?.estimated_playout_timestamp_ms ?? null;
|
|
740
1082
|
const videoPlayout = avatarVideo?.estimated_playout_timestamp_ms ?? null;
|
|
741
1083
|
return {
|
|
742
1084
|
ts: Date.now() / 1000,
|
|
743
1085
|
hidden,
|
|
744
1086
|
...link,
|
|
745
1087
|
av_offset_ms: audioPlayout != null && videoPlayout != null ? audioPlayout - videoPlayout : null,
|
|
746
|
-
avatar_audio:
|
|
1088
|
+
avatar_audio: avatarAudio2,
|
|
747
1089
|
avatar_video: avatarVideo,
|
|
748
1090
|
mic,
|
|
749
|
-
camera: outboundOf(perReport[3])
|
|
1091
|
+
camera: outboundOf(perReport[3]),
|
|
1092
|
+
avatar_audio_drop: null
|
|
750
1093
|
};
|
|
751
1094
|
}
|
|
752
1095
|
function environment() {
|
|
@@ -775,6 +1118,7 @@ class NetworkStatsReporter {
|
|
|
775
1118
|
timer = null;
|
|
776
1119
|
seq = 0;
|
|
777
1120
|
pending = [];
|
|
1121
|
+
audioDrop = null;
|
|
778
1122
|
stopped = false;
|
|
779
1123
|
onVisibility = () => {
|
|
780
1124
|
if (document.visibilityState === "hidden")
|
|
@@ -787,7 +1131,7 @@ class NetworkStatsReporter {
|
|
|
787
1131
|
this.resetMic();
|
|
788
1132
|
};
|
|
789
1133
|
onMicMute = (publication) => {
|
|
790
|
-
if (publication.source ===
|
|
1134
|
+
if (publication.source === Track3.Source.Microphone)
|
|
791
1135
|
this.resetMic();
|
|
792
1136
|
};
|
|
793
1137
|
constructor(room, onMicSilent = () => {}) {
|
|
@@ -803,9 +1147,17 @@ class NetworkStatsReporter {
|
|
|
803
1147
|
}, SAMPLE_MS);
|
|
804
1148
|
document.addEventListener("visibilitychange", this.onVisibility);
|
|
805
1149
|
window.addEventListener("pagehide", this.onPageHide);
|
|
806
|
-
this.room.on(
|
|
1150
|
+
this.room.on(RoomEvent2.Reconnecting, this.onLinkChange).on(RoomEvent2.Reconnected, this.onLinkChange).on(RoomEvent2.Disconnected, this.onLinkChange);
|
|
807
1151
|
this.room.localParticipant.on(ParticipantEvent.TrackMuted, this.onMicMute).on(ParticipantEvent.TrackUnmuted, this.onMicMute);
|
|
808
1152
|
}
|
|
1153
|
+
noteAudioDrop(backlog) {
|
|
1154
|
+
const round = (ms) => Math.round(ms);
|
|
1155
|
+
this.audioDrop = {
|
|
1156
|
+
jitter_buffer_ms: round(backlog.jitterBufferMs),
|
|
1157
|
+
target_ms: round(backlog.targetMs),
|
|
1158
|
+
behind_ms: round(backlog.jitterBufferMs - backlog.targetMs)
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
809
1161
|
stop() {
|
|
810
1162
|
if (!this.teardown())
|
|
811
1163
|
return Promise.resolve();
|
|
@@ -825,7 +1177,7 @@ class NetworkStatsReporter {
|
|
|
825
1177
|
}
|
|
826
1178
|
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
827
1179
|
window.removeEventListener("pagehide", this.onPageHide);
|
|
828
|
-
this.room.off(
|
|
1180
|
+
this.room.off(RoomEvent2.Reconnecting, this.onLinkChange).off(RoomEvent2.Reconnected, this.onLinkChange).off(RoomEvent2.Disconnected, this.onLinkChange);
|
|
829
1181
|
this.room.localParticipant.off(ParticipantEvent.TrackMuted, this.onMicMute).off(ParticipantEvent.TrackUnmuted, this.onMicMute);
|
|
830
1182
|
this.resetMic();
|
|
831
1183
|
return true;
|
|
@@ -838,6 +1190,8 @@ class NetworkStatsReporter {
|
|
|
838
1190
|
}
|
|
839
1191
|
remoteTrack(source) {
|
|
840
1192
|
for (const participant of this.room.remoteParticipants.values()) {
|
|
1193
|
+
if (isAttach(participant))
|
|
1194
|
+
continue;
|
|
841
1195
|
const publication = participant.getTrackPublication(source);
|
|
842
1196
|
if (publication?.isSubscribed && hasStats(publication.track)) {
|
|
843
1197
|
return publication.track;
|
|
@@ -862,20 +1216,22 @@ class NetworkStatsReporter {
|
|
|
862
1216
|
if (this.stopped)
|
|
863
1217
|
return;
|
|
864
1218
|
try {
|
|
865
|
-
const [
|
|
866
|
-
this.report(this.remoteTrack(
|
|
867
|
-
this.report(this.remoteTrack(
|
|
868
|
-
this.report(this.localTrack(
|
|
869
|
-
this.report(this.localTrack(
|
|
1219
|
+
const [avatarAudio2, avatarVideo, mic, camera] = await Promise.all([
|
|
1220
|
+
this.report(this.remoteTrack(Track3.Source.Microphone)),
|
|
1221
|
+
this.report(this.remoteTrack(Track3.Source.Camera)),
|
|
1222
|
+
this.report(this.localTrack(Track3.Source.Microphone)),
|
|
1223
|
+
this.report(this.localTrack(Track3.Source.Camera))
|
|
870
1224
|
]);
|
|
871
1225
|
if (this.stopped)
|
|
872
1226
|
return;
|
|
873
|
-
const micPublication = this.room.localParticipant.getTrackPublication(
|
|
1227
|
+
const micPublication = this.room.localParticipant.getTrackPublication(Track3.Source.Microphone);
|
|
874
1228
|
const micTrack = micPublication?.track?.mediaStreamTrack ?? null;
|
|
875
|
-
const sample =
|
|
1229
|
+
const sample = avatarAudio2 || avatarVideo || mic || camera ? buildSample({ avatarAudio: avatarAudio2, avatarVideo, mic, camera, micTrack }, document.hidden) : null;
|
|
876
1230
|
this.watchMic(!!micPublication && !micPublication.isMuted && micTrack !== null, micTrack, mic ? sample?.mic?.packets_sent ?? 0 : null);
|
|
877
1231
|
if (!sample)
|
|
878
1232
|
return;
|
|
1233
|
+
sample.avatar_audio_drop = this.audioDrop;
|
|
1234
|
+
this.audioDrop = null;
|
|
879
1235
|
this.pending.push(sample);
|
|
880
1236
|
if (this.pending.length >= BATCH)
|
|
881
1237
|
this.flush();
|
|
@@ -885,7 +1241,7 @@ class NetworkStatsReporter {
|
|
|
885
1241
|
const was = this.micSilence.silent;
|
|
886
1242
|
const silent = this.micSilence.observe({
|
|
887
1243
|
at: Date.now(),
|
|
888
|
-
connected: this.room.state ===
|
|
1244
|
+
connected: this.room.state === ConnectionState2.Connected,
|
|
889
1245
|
live,
|
|
890
1246
|
packetsSent,
|
|
891
1247
|
readyState: track?.readyState ?? null,
|
|
@@ -911,8 +1267,90 @@ class NetworkStatsReporter {
|
|
|
911
1267
|
}
|
|
912
1268
|
}
|
|
913
1269
|
|
|
1270
|
+
// src/providers/livekit/parts.ts
|
|
1271
|
+
var PART_TYPE = "message.part";
|
|
1272
|
+
var PARTS_MAX = 64;
|
|
1273
|
+
var TEXT_MAX = 1024 * 1024;
|
|
1274
|
+
var DROPPED_KEPT = 16;
|
|
1275
|
+
|
|
1276
|
+
class Parts {
|
|
1277
|
+
onDrop;
|
|
1278
|
+
open = null;
|
|
1279
|
+
dropped = new Set;
|
|
1280
|
+
constructor(onDrop) {
|
|
1281
|
+
this.onDrop = onDrop;
|
|
1282
|
+
}
|
|
1283
|
+
take(packet) {
|
|
1284
|
+
if (packet === null) {
|
|
1285
|
+
this.cut("a packet that is no event arrived between its parts");
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
if (packet.type !== PART_TYPE) {
|
|
1289
|
+
this.cut("another event arrived between its parts");
|
|
1290
|
+
return packet;
|
|
1291
|
+
}
|
|
1292
|
+
const { id, index, count, text } = packet;
|
|
1293
|
+
if (typeof id !== "string" || typeof text !== "string" || typeof index !== "number" || typeof count !== "number" || !Number.isInteger(index) || !Number.isInteger(count) || count < 1 || count > PARTS_MAX || index < 0 || index >= count) {
|
|
1294
|
+
this.cut("a part broke the framing");
|
|
1295
|
+
if (typeof id === "string")
|
|
1296
|
+
this.report(id, "a part broke the framing");
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
if (index === 0) {
|
|
1300
|
+
this.cut("a new event began before its last part");
|
|
1301
|
+
this.open = { id, count, next: 0, text: "" };
|
|
1302
|
+
}
|
|
1303
|
+
const open = this.open;
|
|
1304
|
+
if (open === null) {
|
|
1305
|
+
this.report(id, "its first part never arrived");
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
if (open.id !== id || open.next !== index || open.count !== count) {
|
|
1309
|
+
this.cut("a part arrived out of turn");
|
|
1310
|
+
this.report(id, "a part arrived out of turn");
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
open.text += text;
|
|
1314
|
+
open.next += 1;
|
|
1315
|
+
if (open.text.length > TEXT_MAX) {
|
|
1316
|
+
this.cut("its parts carried more than 1 MiB");
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
if (open.next < open.count)
|
|
1320
|
+
return null;
|
|
1321
|
+
this.open = null;
|
|
1322
|
+
try {
|
|
1323
|
+
const event = JSON.parse(open.text);
|
|
1324
|
+
if (isRecord(event))
|
|
1325
|
+
return event;
|
|
1326
|
+
} catch {}
|
|
1327
|
+
this.report(open.id, "its parts joined to no JSON object");
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
clear() {
|
|
1331
|
+
this.open = null;
|
|
1332
|
+
this.dropped.clear();
|
|
1333
|
+
}
|
|
1334
|
+
cut(why) {
|
|
1335
|
+
if (this.open === null)
|
|
1336
|
+
return;
|
|
1337
|
+
const { id } = this.open;
|
|
1338
|
+
this.open = null;
|
|
1339
|
+
this.report(id, why);
|
|
1340
|
+
}
|
|
1341
|
+
report(id, why) {
|
|
1342
|
+
if (this.dropped.has(id))
|
|
1343
|
+
return;
|
|
1344
|
+
this.dropped.add(id);
|
|
1345
|
+
if (this.dropped.size > DROPPED_KEPT) {
|
|
1346
|
+
this.dropped.delete(this.dropped.values().next().value);
|
|
1347
|
+
}
|
|
1348
|
+
this.onDrop(id, why);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
914
1352
|
// src/providers/livekit/presentation.ts
|
|
915
|
-
import { Track as
|
|
1353
|
+
import { Track as Track4 } from "livekit-client";
|
|
916
1354
|
var PRESENTATION_TIMING = {
|
|
917
1355
|
sampleMs: 250,
|
|
918
1356
|
settleMs: 3000,
|
|
@@ -974,14 +1412,16 @@ class PresentationWatch {
|
|
|
974
1412
|
}
|
|
975
1413
|
async function avatarVideoSize(room) {
|
|
976
1414
|
for (const participant of room.remoteParticipants.values()) {
|
|
977
|
-
|
|
1415
|
+
if (isAttach(participant))
|
|
1416
|
+
continue;
|
|
1417
|
+
const publication = participant.getTrackPublication(Track4.Source.Camera);
|
|
978
1418
|
const track = publication?.track;
|
|
979
1419
|
if (!publication?.isSubscribed || !track)
|
|
980
1420
|
continue;
|
|
981
1421
|
const publishedWidth = publication.dimensions?.width ?? null;
|
|
982
|
-
const
|
|
1422
|
+
const report2 = await track.getRTCStatsReport();
|
|
983
1423
|
let width = null;
|
|
984
|
-
|
|
1424
|
+
report2?.forEach((stat) => {
|
|
985
1425
|
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
986
1426
|
width = typeof stat.frameWidth === "number" ? stat.frameWidth : null;
|
|
987
1427
|
}
|
|
@@ -992,7 +1432,7 @@ async function avatarVideoSize(room) {
|
|
|
992
1432
|
}
|
|
993
1433
|
|
|
994
1434
|
// src/providers/livekit/ready-fallback.ts
|
|
995
|
-
import { Track as
|
|
1435
|
+
import { Track as Track5 } from "livekit-client";
|
|
996
1436
|
var READY_FALLBACK_TIMING = {
|
|
997
1437
|
sampleMs: 500,
|
|
998
1438
|
delayMs: 5000,
|
|
@@ -1055,13 +1495,15 @@ class ReadyFallback {
|
|
|
1055
1495
|
}
|
|
1056
1496
|
async function avatarFramesDecoded(room) {
|
|
1057
1497
|
for (const participant of room.remoteParticipants.values()) {
|
|
1058
|
-
|
|
1498
|
+
if (isAttach(participant))
|
|
1499
|
+
continue;
|
|
1500
|
+
const publication = participant.getTrackPublication(Track5.Source.Camera);
|
|
1059
1501
|
const track = publication?.track;
|
|
1060
1502
|
if (!publication?.isSubscribed || !track)
|
|
1061
1503
|
continue;
|
|
1062
|
-
const
|
|
1504
|
+
const report2 = await track.getRTCStatsReport();
|
|
1063
1505
|
let frames = null;
|
|
1064
|
-
|
|
1506
|
+
report2?.forEach((stat) => {
|
|
1065
1507
|
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
1066
1508
|
frames = typeof stat.framesDecoded === "number" ? stat.framesDecoded : null;
|
|
1067
1509
|
}
|
|
@@ -1071,69 +1513,6 @@ async function avatarFramesDecoded(room) {
|
|
|
1071
1513
|
return null;
|
|
1072
1514
|
}
|
|
1073
1515
|
|
|
1074
|
-
// src/providers/livekit/sequence.ts
|
|
1075
|
-
class EventSequence {
|
|
1076
|
-
deliver;
|
|
1077
|
-
replay;
|
|
1078
|
-
last = 0;
|
|
1079
|
-
held = new Map;
|
|
1080
|
-
replaying = false;
|
|
1081
|
-
stopped = false;
|
|
1082
|
-
constructor(deliver, replay) {
|
|
1083
|
-
this.deliver = deliver;
|
|
1084
|
-
this.replay = replay;
|
|
1085
|
-
}
|
|
1086
|
-
repeats(seq) {
|
|
1087
|
-
return seq !== 0 && (seq <= this.last || this.held.has(seq));
|
|
1088
|
-
}
|
|
1089
|
-
accept(seq, item) {
|
|
1090
|
-
if (this.stopped || this.repeats(seq))
|
|
1091
|
-
return;
|
|
1092
|
-
if (seq === 0) {
|
|
1093
|
-
this.deliver(item);
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
if (seq !== this.last + 1) {
|
|
1097
|
-
this.held.set(seq, item);
|
|
1098
|
-
this.requestReplay();
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
this.last = seq;
|
|
1102
|
-
this.deliver(item);
|
|
1103
|
-
this.release(false);
|
|
1104
|
-
}
|
|
1105
|
-
catchUp() {
|
|
1106
|
-
if (!this.stopped)
|
|
1107
|
-
this.requestReplay();
|
|
1108
|
-
}
|
|
1109
|
-
stop() {
|
|
1110
|
-
this.stopped = true;
|
|
1111
|
-
this.held.clear();
|
|
1112
|
-
}
|
|
1113
|
-
requestReplay() {
|
|
1114
|
-
if (this.replaying)
|
|
1115
|
-
return;
|
|
1116
|
-
this.replaying = true;
|
|
1117
|
-
this.replay(this.last).catch(() => {
|
|
1118
|
-
return;
|
|
1119
|
-
}).finally(() => {
|
|
1120
|
-
this.replaying = false;
|
|
1121
|
-
this.release(true);
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
release(all) {
|
|
1125
|
-
const pending = [...this.held.keys()].sort((a, b) => a - b);
|
|
1126
|
-
for (const seq of pending) {
|
|
1127
|
-
if (this.stopped || !all && seq !== this.last + 1)
|
|
1128
|
-
return;
|
|
1129
|
-
const item = this.held.get(seq);
|
|
1130
|
-
this.held.delete(seq);
|
|
1131
|
-
this.last = seq;
|
|
1132
|
-
this.deliver(item);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
1516
|
// src/providers/livekit/signals.ts
|
|
1138
1517
|
function decode(payload) {
|
|
1139
1518
|
try {
|
|
@@ -1143,9 +1522,37 @@ function decode(payload) {
|
|
|
1143
1522
|
return null;
|
|
1144
1523
|
}
|
|
1145
1524
|
}
|
|
1525
|
+
function legacyReason(cause) {
|
|
1526
|
+
if (cause === "content")
|
|
1527
|
+
return "policy-violation";
|
|
1528
|
+
if (cause === "age_check")
|
|
1529
|
+
return "age-check";
|
|
1530
|
+
if (cause === "expired")
|
|
1531
|
+
return "time-limit";
|
|
1532
|
+
if (cause === "connection_lost")
|
|
1533
|
+
return "connection-lost";
|
|
1534
|
+
return "ended";
|
|
1535
|
+
}
|
|
1536
|
+
function closedEnd(decoded) {
|
|
1537
|
+
if (decoded.type !== "session.closed")
|
|
1538
|
+
return null;
|
|
1539
|
+
const cause = endReasonOf(decoded.reason);
|
|
1540
|
+
const ended = {
|
|
1541
|
+
reason: legacyReason(cause),
|
|
1542
|
+
byClient: cause === "close_requested"
|
|
1543
|
+
};
|
|
1544
|
+
if (cause !== undefined)
|
|
1545
|
+
ended.endReason = cause;
|
|
1546
|
+
if (typeof decoded.message === "string" && decoded.message !== "") {
|
|
1547
|
+
ended.message = decoded.message;
|
|
1548
|
+
}
|
|
1549
|
+
return ended;
|
|
1550
|
+
}
|
|
1146
1551
|
function endReason(reason) {
|
|
1147
1552
|
if (reason === "policy_violation")
|
|
1148
1553
|
return "policy-violation";
|
|
1554
|
+
if (reason === "age_check")
|
|
1555
|
+
return "age-check";
|
|
1149
1556
|
if (reason === "time_limit")
|
|
1150
1557
|
return "time-limit";
|
|
1151
1558
|
if (reason === "invalid_spec" || reason === "runtime_failed") {
|
|
@@ -1153,105 +1560,21 @@ function endReason(reason) {
|
|
|
1153
1560
|
}
|
|
1154
1561
|
return "ended";
|
|
1155
1562
|
}
|
|
1156
|
-
function parseSessionEvent(payload) {
|
|
1157
|
-
const decoded = decode(payload);
|
|
1158
|
-
if (!decoded)
|
|
1159
|
-
return null;
|
|
1160
|
-
const seq = Number.isSafeInteger(decoded.seq) && decoded.seq > 0 ? decoded.seq : 0;
|
|
1161
|
-
return { seq, event: protocolEvent(decoded, seq) };
|
|
1162
|
-
}
|
|
1163
|
-
function protocolEvent(decoded, seq) {
|
|
1164
|
-
if (decoded.type === "session.output_ready") {
|
|
1165
|
-
return { type: "output-ready" };
|
|
1166
|
-
}
|
|
1167
|
-
if (decoded.type === "command.result") {
|
|
1168
|
-
return {
|
|
1169
|
-
type: "command-result",
|
|
1170
|
-
id: typeof decoded.id === "string" ? decoded.id : null,
|
|
1171
|
-
result: {
|
|
1172
|
-
status: decoded.status === "applied" ? "applied" : "rejected",
|
|
1173
|
-
error: typeof decoded.error === "string" ? decoded.error : null
|
|
1174
|
-
}
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
if (decoded.type === "tool.call" && typeof decoded.call_id === "string" && typeof decoded.name === "string" && isRecord(decoded.arguments)) {
|
|
1178
|
-
return {
|
|
1179
|
-
type: "tool-call",
|
|
1180
|
-
call: {
|
|
1181
|
-
callId: decoded.call_id,
|
|
1182
|
-
name: decoded.name,
|
|
1183
|
-
label: typeof decoded.label === "string" ? decoded.label : "",
|
|
1184
|
-
arguments: decoded.arguments
|
|
1185
|
-
}
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
if (decoded.type === "session.status" && decoded.status === "ended") {
|
|
1189
|
-
return {
|
|
1190
|
-
type: "ended",
|
|
1191
|
-
reason: endReason(decoded.reason),
|
|
1192
|
-
byClient: decoded.reason === "client_ended"
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
if (decoded.type === "error" && decoded.fatal === true) {
|
|
1196
|
-
const ended = {
|
|
1197
|
-
type: "ended",
|
|
1198
|
-
reason: endReason(decoded.code),
|
|
1199
|
-
byClient: false
|
|
1200
|
-
};
|
|
1201
|
-
if (typeof decoded.message === "string" && decoded.message !== "") {
|
|
1202
|
-
ended.message = decoded.message;
|
|
1203
|
-
}
|
|
1204
|
-
return ended;
|
|
1205
|
-
}
|
|
1206
|
-
if (decoded.type === "error" && decoded.code === "bad_command") {
|
|
1207
|
-
return { type: "command-unreadable" };
|
|
1208
|
-
}
|
|
1209
|
-
const event = sessionEvent(decoded, seq);
|
|
1210
|
-
return event ? { type: "session-event", event } : { type: "other" };
|
|
1211
|
-
}
|
|
1212
|
-
var isString = (value) => typeof value === "string";
|
|
1213
|
-
function defaulted(value, absent) {
|
|
1214
|
-
if (value === undefined)
|
|
1215
|
-
return absent;
|
|
1216
|
-
return isString(value) ? value : null;
|
|
1217
|
-
}
|
|
1218
|
-
var READERS = {
|
|
1219
|
-
"user.transcript": (wire) => {
|
|
1220
|
-
const source = defaulted(wire.source, "asr");
|
|
1221
|
-
return isString(wire.text) && source !== null ? { text: wire.text, source } : null;
|
|
1222
|
-
},
|
|
1223
|
-
"user.speech.started": () => ({}),
|
|
1224
|
-
"user.speech.ended": () => ({}),
|
|
1225
|
-
"user.turn.ended": (wire) => isString(wire.text) ? { text: wire.text } : null,
|
|
1226
|
-
"agent.turn.started": (wire) => {
|
|
1227
|
-
const source = defaulted(wire.source, "reply");
|
|
1228
|
-
return isString(wire.turn_id) && isString(wire.opened_by) && source !== null ? { turnId: wire.turn_id, openedBy: wire.opened_by, source } : null;
|
|
1229
|
-
},
|
|
1230
|
-
"agent.speech.started": (wire) => isString(wire.turn_id) ? { turnId: wire.turn_id } : null,
|
|
1231
|
-
"agent.text": (wire) => {
|
|
1232
|
-
const turnId = wire.turn_id ?? null;
|
|
1233
|
-
return (turnId === null || isString(turnId)) && isString(wire.text) ? { turnId, text: wire.text } : null;
|
|
1234
|
-
},
|
|
1235
|
-
"agent.turn.ended": (wire) => isString(wire.turn_id) && isString(wire.reason) && isString(wire.spoken) ? { turnId: wire.turn_id, reason: wire.reason, spoken: wire.spoken } : null,
|
|
1236
|
-
floor: (wire) => isString(wire.state) ? { state: wire.state } : null
|
|
1237
|
-
};
|
|
1238
|
-
function sessionEvent(decoded, seq) {
|
|
1239
|
-
const type = decoded.type;
|
|
1240
|
-
if (!isString(type) || !Object.hasOwn(READERS, type))
|
|
1241
|
-
return null;
|
|
1242
|
-
const fields = READERS[type](decoded);
|
|
1243
|
-
if (fields === null)
|
|
1244
|
-
return null;
|
|
1245
|
-
const atMs = Number.isFinite(decoded.at_ms) ? decoded.at_ms : null;
|
|
1246
|
-
return { type, seq, atMs, ...fields };
|
|
1247
|
-
}
|
|
1248
1563
|
function parseRuntimeSignal(payload) {
|
|
1249
1564
|
const decoded = decode(payload);
|
|
1250
1565
|
if (decoded?.type === "output_ready") {
|
|
1251
1566
|
return { type: "output-ready" };
|
|
1252
1567
|
}
|
|
1253
1568
|
if (decoded?.type === "session_ended") {
|
|
1254
|
-
|
|
1569
|
+
const reason = endReason(decoded.reason);
|
|
1570
|
+
const signal = { type: "session-ended", reason };
|
|
1571
|
+
const cause = reason === "refused" ? undefined : v1EndReason(decoded.reason);
|
|
1572
|
+
if (cause !== undefined)
|
|
1573
|
+
signal.endReason = cause;
|
|
1574
|
+
if (typeof decoded.message === "string" && decoded.message !== "") {
|
|
1575
|
+
signal.message = decoded.message;
|
|
1576
|
+
}
|
|
1577
|
+
return signal;
|
|
1255
1578
|
}
|
|
1256
1579
|
if (decoded?.type === "link_poor") {
|
|
1257
1580
|
return { type: "link-poor" };
|
|
@@ -1271,6 +1594,10 @@ function clockSyncPong(payload, receivedAt, now) {
|
|
|
1271
1594
|
};
|
|
1272
1595
|
return new TextEncoder().encode(JSON.stringify(reply));
|
|
1273
1596
|
}
|
|
1597
|
+
function parseInterfaceEvent(payload) {
|
|
1598
|
+
const decoded = decode(payload);
|
|
1599
|
+
return Array.isArray(decoded) ? null : decoded;
|
|
1600
|
+
}
|
|
1274
1601
|
function parseDataEvent(payload) {
|
|
1275
1602
|
const decoded = decode(payload);
|
|
1276
1603
|
if (!decoded)
|
|
@@ -1293,38 +1620,14 @@ function parseDataEvent(payload) {
|
|
|
1293
1620
|
import {
|
|
1294
1621
|
AudioPresets,
|
|
1295
1622
|
ParticipantEvent as ParticipantEvent2,
|
|
1296
|
-
RoomEvent as
|
|
1297
|
-
Track as
|
|
1623
|
+
RoomEvent as RoomEvent3,
|
|
1624
|
+
Track as Track6
|
|
1298
1625
|
} from "livekit-client";
|
|
1299
|
-
|
|
1300
|
-
// src/core/camera.ts
|
|
1301
|
-
function cameraFacing(track) {
|
|
1302
|
-
const facing = track.getSettings().facingMode;
|
|
1303
|
-
return facing === "user" || facing === "environment" ? facing : null;
|
|
1304
|
-
}
|
|
1305
|
-
function requireCameraFacing(track, facing) {
|
|
1306
|
-
const requested = typeof facing === "string" ? facing : !Array.isArray(facing) ? facing?.exact : undefined;
|
|
1307
|
-
if (typeof requested !== "string")
|
|
1308
|
-
return;
|
|
1309
|
-
if (!track || track.getSettings().facingMode !== requested)
|
|
1310
|
-
throw new DOMException("Requested camera direction is unavailable", "OverconstrainedError");
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// src/providers/livekit/user-participant.ts
|
|
1314
1626
|
function microphonePublishOptions(resilient) {
|
|
1315
1627
|
if (!resilient)
|
|
1316
1628
|
return;
|
|
1317
1629
|
return { dtx: false, audioPreset: AudioPresets.speech };
|
|
1318
1630
|
}
|
|
1319
|
-
var SCREEN_CAPTURE = {
|
|
1320
|
-
audio: false,
|
|
1321
|
-
resolution: { width: 1920, height: 1080, frameRate: 2 },
|
|
1322
|
-
contentHint: "detail"
|
|
1323
|
-
};
|
|
1324
|
-
var SCREEN_PUBLISH = {
|
|
1325
|
-
screenShareEncoding: { maxBitrate: 1500000, maxFramerate: 2 },
|
|
1326
|
-
simulcast: false
|
|
1327
|
-
};
|
|
1328
1631
|
|
|
1329
1632
|
class LiveKitUserParticipant {
|
|
1330
1633
|
room;
|
|
@@ -1332,8 +1635,6 @@ class LiveKitUserParticipant {
|
|
|
1332
1635
|
onSpeakingChanged;
|
|
1333
1636
|
onMicrophoneChanged;
|
|
1334
1637
|
onCameraChanged;
|
|
1335
|
-
onScreenShareChanged;
|
|
1336
|
-
screenTrack;
|
|
1337
1638
|
inputs = {
|
|
1338
1639
|
audioinput: { changing: false, unavailable: false },
|
|
1339
1640
|
videoinput: { changing: false, unavailable: false }
|
|
@@ -1348,7 +1649,7 @@ class LiveKitUserParticipant {
|
|
|
1348
1649
|
lastAudioSource = null;
|
|
1349
1650
|
_isSpeaking = false;
|
|
1350
1651
|
speakingHandler = null;
|
|
1351
|
-
constructor(room, onTracksChanged, onSpeakingChanged, onMicrophoneChanged, onCameraChanged = () => {}, initialCameraFacing, resilient = false
|
|
1652
|
+
constructor(room, onTracksChanged, onSpeakingChanged, onMicrophoneChanged, onCameraChanged = () => {}, initialCameraFacing, resilient = false) {
|
|
1352
1653
|
this.room = room;
|
|
1353
1654
|
this.onTracksChanged = onTracksChanged;
|
|
1354
1655
|
this.onSpeakingChanged = onSpeakingChanged;
|
|
@@ -1356,13 +1657,11 @@ class LiveKitUserParticipant {
|
|
|
1356
1657
|
this.onCameraChanged = onCameraChanged;
|
|
1357
1658
|
this.initialCameraFacing = initialCameraFacing;
|
|
1358
1659
|
this.microphoneOptions = microphonePublishOptions(resilient);
|
|
1359
|
-
this.
|
|
1360
|
-
this.room.on(RoomEvent2.ActiveDeviceChanged, this.handleDeviceChanged);
|
|
1361
|
-
this.room.localParticipant.on(ParticipantEvent2.LocalTrackUnpublished, this.handleTrackUnpublished);
|
|
1660
|
+
this.room.on(RoomEvent3.ActiveDeviceChanged, this.handleDeviceChanged);
|
|
1362
1661
|
}
|
|
1363
1662
|
updateTracks() {
|
|
1364
1663
|
let changed = false;
|
|
1365
|
-
const videoPub = this.room.localParticipant.getTrackPublication(
|
|
1664
|
+
const videoPub = this.room.localParticipant.getTrackPublication(Track6.Source.Camera);
|
|
1366
1665
|
const videoSource = videoPub?.track ?? null;
|
|
1367
1666
|
if (videoSource !== this.lastVideoSource) {
|
|
1368
1667
|
this._videoTrack?.detach();
|
|
@@ -1370,7 +1669,7 @@ class LiveKitUserParticipant {
|
|
|
1370
1669
|
this.lastVideoSource = videoSource;
|
|
1371
1670
|
changed = true;
|
|
1372
1671
|
}
|
|
1373
|
-
const audioPub = this.room.localParticipant.getTrackPublication(
|
|
1672
|
+
const audioPub = this.room.localParticipant.getTrackPublication(Track6.Source.Microphone);
|
|
1374
1673
|
const audioSource = audioPub?.track ?? null;
|
|
1375
1674
|
if (audioSource !== this.lastAudioSource) {
|
|
1376
1675
|
this._audioTrack?.detach();
|
|
@@ -1452,7 +1751,7 @@ class LiveKitUserParticipant {
|
|
|
1452
1751
|
}
|
|
1453
1752
|
}
|
|
1454
1753
|
inputTrack(kind) {
|
|
1455
|
-
const publication = this.room.localParticipant.getTrackPublication(kind === "audioinput" ?
|
|
1754
|
+
const publication = this.room.localParticipant.getTrackPublication(kind === "audioinput" ? Track6.Source.Microphone : Track6.Source.Camera);
|
|
1456
1755
|
return kind === "audioinput" ? publication?.audioTrack : publication?.videoTrack;
|
|
1457
1756
|
}
|
|
1458
1757
|
async switchInput(kind, selection) {
|
|
@@ -1519,14 +1818,9 @@ class LiveKitUserParticipant {
|
|
|
1519
1818
|
this.notifyInput(kind);
|
|
1520
1819
|
}
|
|
1521
1820
|
};
|
|
1522
|
-
handleTrackUnpublished = (publication) => {
|
|
1523
|
-
if (!this.disposed && publication.source === Track5.Source.ScreenShare)
|
|
1524
|
-
this.onScreenShareChanged();
|
|
1525
|
-
};
|
|
1526
1821
|
cleanup() {
|
|
1527
1822
|
this.disposed = true;
|
|
1528
|
-
this.room.off(
|
|
1529
|
-
this.room.localParticipant.off(ParticipantEvent2.LocalTrackUnpublished, this.handleTrackUnpublished);
|
|
1823
|
+
this.room.off(RoomEvent3.ActiveDeviceChanged, this.handleDeviceChanged);
|
|
1530
1824
|
if (this.inputs.audioinput.changing)
|
|
1531
1825
|
this.inputTrack("audioinput")?.stop();
|
|
1532
1826
|
if (this.inputs.videoinput.changing)
|
|
@@ -1604,44 +1898,10 @@ class LiveKitUserParticipant {
|
|
|
1604
1898
|
get isVideoEnabled() {
|
|
1605
1899
|
return this.room.localParticipant.isCameraEnabled;
|
|
1606
1900
|
}
|
|
1607
|
-
async startScreenShare() {
|
|
1608
|
-
await this.setScreenShare(true);
|
|
1609
|
-
}
|
|
1610
|
-
async stopScreenShare() {
|
|
1611
|
-
const track = this.screenTrack;
|
|
1612
|
-
this.screenTrack = undefined;
|
|
1613
|
-
try {
|
|
1614
|
-
if (!this.disposed)
|
|
1615
|
-
await this.setScreenShare(false);
|
|
1616
|
-
} finally {
|
|
1617
|
-
track?.stop();
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
get isScreenSharing() {
|
|
1621
|
-
return this.room.localParticipant.isScreenShareEnabled;
|
|
1622
|
-
}
|
|
1623
|
-
async setScreenShare(enabled) {
|
|
1624
|
-
if (this.disposed)
|
|
1625
|
-
throw new Error("Participant is disconnected");
|
|
1626
|
-
try {
|
|
1627
|
-
const publication = await this.room.localParticipant.setScreenShareEnabled(enabled, SCREEN_CAPTURE, SCREEN_PUBLISH);
|
|
1628
|
-
if (this.disposed)
|
|
1629
|
-
publication?.track?.stop();
|
|
1630
|
-
else if (enabled)
|
|
1631
|
-
this.screenTrack = publication?.track;
|
|
1632
|
-
} catch (error) {
|
|
1633
|
-
throw new MediaAccessError(error);
|
|
1634
|
-
} finally {
|
|
1635
|
-
if (!this.disposed)
|
|
1636
|
-
this.onScreenShareChanged();
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
1901
|
releaseDevices() {
|
|
1640
1902
|
for (const pub of this.room.localParticipant.trackPublications.values()) {
|
|
1641
1903
|
pub.track?.stop();
|
|
1642
1904
|
}
|
|
1643
|
-
this.screenTrack?.stop();
|
|
1644
|
-
this.screenTrack = undefined;
|
|
1645
1905
|
}
|
|
1646
1906
|
startSpeakingDetection() {
|
|
1647
1907
|
this.stopSpeakingDetection();
|
|
@@ -1667,37 +1927,31 @@ class LiveKitUserParticipant {
|
|
|
1667
1927
|
|
|
1668
1928
|
// src/providers/livekit/conversation.ts
|
|
1669
1929
|
var DATA_EVENT_TOPIC = "nuance.data";
|
|
1670
|
-
var
|
|
1671
|
-
var PACKET_MAX_BYTES = 14000;
|
|
1930
|
+
var INTERFACE_EVENT_MAX_BYTES = 64000;
|
|
1672
1931
|
var RUNTIME_TOPIC = "runtime";
|
|
1673
1932
|
var CLOCK_SYNC_TOPIC = "clock-sync";
|
|
1674
1933
|
var STATS_FLUSH_TIMEOUT_MS = 300;
|
|
1675
|
-
var
|
|
1676
|
-
var
|
|
1934
|
+
var SESSION_CLOSE_TIMEOUT_MS = 300;
|
|
1935
|
+
var SESSION_TOPIC = "nuance.v1";
|
|
1936
|
+
var READY_ID = "client-ready";
|
|
1937
|
+
var INTERFACE_CLOSE_ID = "client-disconnect";
|
|
1677
1938
|
var REFUSAL_REASON_WAIT_MS = 500;
|
|
1678
|
-
var COMMAND_TIMEOUT_MS = 5000;
|
|
1679
|
-
var STREAM_TIMEOUT_MS = 30000;
|
|
1680
|
-
var BACKGROUND_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
1681
1939
|
var AVATAR_RETURN_TIMEOUT_MS = 8000;
|
|
1682
1940
|
var RUNTIME_RETURN_TIMEOUT_MS = 20000;
|
|
1683
1941
|
var RUNTIME_RECHECK_MS = 1000;
|
|
1684
|
-
function contextItem(item) {
|
|
1685
|
-
const name = item.name === undefined ? {} : { name: item.name };
|
|
1686
|
-
return item.type === "text" ? { type: "text", text: item.text, ...name } : { type: "image", media_type: item.mediaType, data: item.data, ...name };
|
|
1687
|
-
}
|
|
1688
1942
|
function isReconnecting(lkState) {
|
|
1689
|
-
return lkState ===
|
|
1943
|
+
return lkState === ConnectionState3.Reconnecting || lkState === ConnectionState3.SignalReconnecting;
|
|
1690
1944
|
}
|
|
1691
1945
|
function mapConnectionState(lkState, resilient = false) {
|
|
1692
1946
|
switch (lkState) {
|
|
1693
|
-
case
|
|
1947
|
+
case ConnectionState3.Connected:
|
|
1694
1948
|
return "connected";
|
|
1695
|
-
case
|
|
1949
|
+
case ConnectionState3.Connecting:
|
|
1696
1950
|
return "connecting";
|
|
1697
|
-
case
|
|
1698
|
-
case
|
|
1951
|
+
case ConnectionState3.Reconnecting:
|
|
1952
|
+
case ConnectionState3.SignalReconnecting:
|
|
1699
1953
|
return resilient ? "reconnecting" : "connecting";
|
|
1700
|
-
case
|
|
1954
|
+
case ConnectionState3.Disconnected:
|
|
1701
1955
|
return "disconnected";
|
|
1702
1956
|
}
|
|
1703
1957
|
}
|
|
@@ -1726,27 +1980,24 @@ class LiveKitConversation {
|
|
|
1726
1980
|
presentation = null;
|
|
1727
1981
|
readyFallback = null;
|
|
1728
1982
|
layerFallback = null;
|
|
1983
|
+
audioBacklog = null;
|
|
1729
1984
|
changeHandlers = new Set;
|
|
1730
1985
|
userQuality = "good";
|
|
1731
1986
|
avatarQuality = "good";
|
|
1732
1987
|
network = "good";
|
|
1733
1988
|
eventsRegistered = false;
|
|
1989
|
+
parts = new Parts((id, why) => {
|
|
1990
|
+
console.warn(`Nuance SDK: interface event ${id} was dropped: ${why}.`);
|
|
1991
|
+
});
|
|
1734
1992
|
sessionEnded = false;
|
|
1735
1993
|
refusal = null;
|
|
1736
|
-
speaksProtocol = false;
|
|
1737
1994
|
avatarSpeaking = false;
|
|
1738
|
-
sequence = this.newSequence();
|
|
1739
1995
|
answered = null;
|
|
1740
|
-
commands = new Map;
|
|
1741
|
-
commandCount = 0;
|
|
1742
|
-
readyOwed = false;
|
|
1743
1996
|
everConnected = false;
|
|
1744
1997
|
away = false;
|
|
1745
1998
|
awayTimer = null;
|
|
1746
1999
|
runtimeTimer = null;
|
|
1747
2000
|
presentationPaused = false;
|
|
1748
|
-
readySending = false;
|
|
1749
|
-
contextSent = null;
|
|
1750
2001
|
leaving = null;
|
|
1751
2002
|
connectGeneration = 0;
|
|
1752
2003
|
_id;
|
|
@@ -1754,10 +2005,12 @@ class LiveKitConversation {
|
|
|
1754
2005
|
cameraEnabled;
|
|
1755
2006
|
cameraFacing;
|
|
1756
2007
|
tokenProvider;
|
|
1757
|
-
opensOnReady;
|
|
1758
2008
|
presentsAtFullSize;
|
|
1759
|
-
|
|
2009
|
+
opensOnReady;
|
|
2010
|
+
readyOwed = false;
|
|
1760
2011
|
resilient;
|
|
2012
|
+
dropsAudioBacklog;
|
|
2013
|
+
interfaceTopic;
|
|
1761
2014
|
constructor(config) {
|
|
1762
2015
|
this.room = new Room({
|
|
1763
2016
|
audioCaptureDefaults: config.mediaDevices?.microphoneId ? { deviceId: { ideal: config.mediaDevices.microphoneId } } : undefined,
|
|
@@ -1777,10 +2030,11 @@ class LiveKitConversation {
|
|
|
1777
2030
|
this.cameraEnabled = config.cameraEnabled ?? true;
|
|
1778
2031
|
this.cameraFacing = config.mediaDevices?.cameraFacing;
|
|
1779
2032
|
this.tokenProvider = config.tokenProvider;
|
|
2033
|
+
this.presentsAtFullSize = config.presentsAtFullSize ?? false;
|
|
1780
2034
|
this.opensOnReady = config.opensOnReady ?? false;
|
|
1781
|
-
this.presentsAtFullSize = config.presentsAtFullSize ?? this.opensOnReady;
|
|
1782
|
-
this.initialContext = config.initialContext ?? [];
|
|
1783
2035
|
this.resilient = config.resilientReconnect ?? false;
|
|
2036
|
+
this.dropsAudioBacklog = config.dropAudioBacklog ?? false;
|
|
2037
|
+
this.interfaceTopic = config.interface;
|
|
1784
2038
|
}
|
|
1785
2039
|
get id() {
|
|
1786
2040
|
return this._id;
|
|
@@ -1791,11 +2045,7 @@ class LiveKitConversation {
|
|
|
1791
2045
|
}
|
|
1792
2046
|
this.setState("connecting");
|
|
1793
2047
|
this.sessionEnded = false;
|
|
1794
|
-
this.
|
|
1795
|
-
this.sequence.stop();
|
|
1796
|
-
this.sequence = this.newSequence();
|
|
1797
|
-
this.readyOwed = false;
|
|
1798
|
-
this.contextSent = null;
|
|
2048
|
+
this.parts.clear();
|
|
1799
2049
|
this.everConnected = false;
|
|
1800
2050
|
if (!this.eventsRegistered) {
|
|
1801
2051
|
this.registerRoomEvents();
|
|
@@ -1822,7 +2072,7 @@ class LiveKitConversation {
|
|
|
1822
2072
|
type: "speaking-changed",
|
|
1823
2073
|
participant: "user",
|
|
1824
2074
|
isSpeaking
|
|
1825
|
-
}), () => this.emit({ type: "microphone-changed" }), () => this.emit({ type: "camera-changed" }), this.cameraFacing, this.resilient
|
|
2075
|
+
}), () => this.emit({ type: "microphone-changed" }), () => this.emit({ type: "camera-changed" }), this.cameraFacing, this.resilient);
|
|
1826
2076
|
this._user.startSpeakingDetection();
|
|
1827
2077
|
try {
|
|
1828
2078
|
await this._user.unmuteAudio();
|
|
@@ -1845,6 +2095,7 @@ class LiveKitConversation {
|
|
|
1845
2095
|
this.checkForExistingAvatar();
|
|
1846
2096
|
this.stats = new NetworkStatsReporter(this.room, (silent) => this.emit({ type: "mic-silent-changed", silent }));
|
|
1847
2097
|
this.stats.start();
|
|
2098
|
+
this.startAudioBacklog();
|
|
1848
2099
|
} catch (error) {
|
|
1849
2100
|
if (!await aborted())
|
|
1850
2101
|
this.setState("failed");
|
|
@@ -1862,18 +2113,21 @@ class LiveKitConversation {
|
|
|
1862
2113
|
this.stopPresentation();
|
|
1863
2114
|
this.stopReadyFallback();
|
|
1864
2115
|
this.stopLayerFallback();
|
|
2116
|
+
this.stopAudioBacklog();
|
|
1865
2117
|
this.clearAway();
|
|
1866
2118
|
this.stopRefusal();
|
|
1867
2119
|
this._user?.cleanup();
|
|
1868
2120
|
this._user?.releaseDevices();
|
|
1869
2121
|
await this.flushStats();
|
|
1870
|
-
await this.
|
|
1871
|
-
this.sequence.stop();
|
|
2122
|
+
await this.sendInterfaceClose();
|
|
1872
2123
|
await this.room.disconnect();
|
|
1873
2124
|
this._avatar.updateParticipant(null);
|
|
1874
2125
|
this._user = null;
|
|
1875
2126
|
this.setState("disconnected");
|
|
1876
2127
|
}
|
|
2128
|
+
get interface() {
|
|
2129
|
+
return this.interfaceTopic;
|
|
2130
|
+
}
|
|
1877
2131
|
get state() {
|
|
1878
2132
|
return this._state;
|
|
1879
2133
|
}
|
|
@@ -1886,51 +2140,55 @@ class LiveKitConversation {
|
|
|
1886
2140
|
get avatar() {
|
|
1887
2141
|
return this._avatar;
|
|
1888
2142
|
}
|
|
1889
|
-
async
|
|
1890
|
-
if (
|
|
1891
|
-
throw new Error(
|
|
2143
|
+
async sendInterfaceEvent(event) {
|
|
2144
|
+
if (this.room.state !== ConnectionState3.Connected) {
|
|
2145
|
+
throw new Error("Not connected — call connect() first");
|
|
1892
2146
|
}
|
|
1893
|
-
|
|
2147
|
+
const text = JSON.stringify(event);
|
|
2148
|
+
const payload = new TextEncoder().encode(text);
|
|
2149
|
+
if (payload.byteLength > INTERFACE_EVENT_MAX_BYTES) {
|
|
2150
|
+
await this.room.localParticipant.sendText(text, {
|
|
2151
|
+
topic: this.interfaceTopic
|
|
2152
|
+
});
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
await this.room.localParticipant.publishData(payload, {
|
|
2156
|
+
reliable: true,
|
|
2157
|
+
topic: this.interfaceTopic
|
|
2158
|
+
});
|
|
1894
2159
|
}
|
|
1895
|
-
|
|
1896
|
-
const
|
|
1897
|
-
if (
|
|
1898
|
-
|
|
1899
|
-
return this.command({ type: "input.text", text: line, respond: true });
|
|
2160
|
+
onInterfacePacket(payload) {
|
|
2161
|
+
const event = this.parts.take(parseInterfaceEvent(payload));
|
|
2162
|
+
if (event !== null)
|
|
2163
|
+
this.deliverInterfaceEvent(event);
|
|
1900
2164
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
2165
|
+
deliverInterfaceEvent(event) {
|
|
2166
|
+
this.emit({ type: "interface-event", event });
|
|
2167
|
+
this.followAvatarSpeech(event);
|
|
2168
|
+
const ended = closedEnd(event);
|
|
2169
|
+
if (ended === null)
|
|
1903
2170
|
return;
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
data
|
|
1919
|
-
}))
|
|
1920
|
-
},
|
|
1921
|
-
is_error: options.isError ?? false,
|
|
1922
|
-
final: options.final ?? true
|
|
1923
|
-
});
|
|
2171
|
+
if (ended.byClient)
|
|
2172
|
+
this.answered?.();
|
|
2173
|
+
if (this.refusal !== null) {
|
|
2174
|
+
this.reportEnd("refused", ended.message);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
this.reportEnd(ended.reason, ended.message, ended.endReason);
|
|
2178
|
+
}
|
|
2179
|
+
followAvatarSpeech(event) {
|
|
2180
|
+
if (event.type === "session.output_transcript.delta") {
|
|
2181
|
+
this.setAvatarSpeaking(true);
|
|
2182
|
+
} else if (event.type === "session.output_audio.done") {
|
|
2183
|
+
this.setAvatarSpeaking(false);
|
|
2184
|
+
}
|
|
1924
2185
|
}
|
|
1925
2186
|
onChange(handler) {
|
|
1926
2187
|
return this.onEvent((event) => {
|
|
1927
|
-
if (event.type !== "data-event" && event.type !== "
|
|
2188
|
+
if (event.type !== "data-event" && event.type !== "interface-event")
|
|
1928
2189
|
handler(event);
|
|
1929
2190
|
});
|
|
1930
2191
|
}
|
|
1931
|
-
on(type, handler) {
|
|
1932
|
-
return this.onEvent(sessionEventsOf(type, handler));
|
|
1933
|
-
}
|
|
1934
2192
|
onEvent(handler) {
|
|
1935
2193
|
this.changeHandlers.add(handler);
|
|
1936
2194
|
return () => {
|
|
@@ -1944,41 +2202,11 @@ class LiveKitConversation {
|
|
|
1944
2202
|
}
|
|
1945
2203
|
});
|
|
1946
2204
|
}
|
|
1947
|
-
|
|
2205
|
+
onInterfaceEvent(handler) {
|
|
1948
2206
|
return this.onEvent((event) => {
|
|
1949
|
-
if (event.type === "
|
|
1950
|
-
handler(event.
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
async command(command) {
|
|
1954
|
-
if (this.room.state !== ConnectionState2.Connected) {
|
|
1955
|
-
throw new Error("Not connected — call connect() first");
|
|
1956
|
-
}
|
|
1957
|
-
if (!this.speaksProtocol) {
|
|
1958
|
-
throw new Error("The runtime takes no session commands");
|
|
1959
|
-
}
|
|
1960
|
-
const id = `client-command-${++this.commandCount}`;
|
|
1961
|
-
const answer = Promise.withResolvers();
|
|
1962
|
-
this.commands.set(id, { type: command.type, settle: answer.resolve });
|
|
1963
|
-
let timer;
|
|
1964
|
-
const text = JSON.stringify({ ...command, id });
|
|
1965
|
-
const payload = new TextEncoder().encode(text);
|
|
1966
|
-
const streamed = payload.byteLength > PACKET_MAX_BYTES;
|
|
1967
|
-
const sent = streamed ? this.room.localParticipant.sendText(text, { topic: SESSION_TOPIC }) : this.room.localParticipant.publishData(payload, {
|
|
1968
|
-
reliable: true,
|
|
1969
|
-
topic: SESSION_TOPIC
|
|
2207
|
+
if (event.type === "interface-event")
|
|
2208
|
+
handler(event.event);
|
|
1970
2209
|
});
|
|
1971
|
-
try {
|
|
1972
|
-
return await Promise.race([
|
|
1973
|
-
sent.then(() => answer.promise),
|
|
1974
|
-
new Promise((_, reject) => {
|
|
1975
|
-
timer = setTimeout(() => reject(new Error(`The runtime did not answer ${command.type}`)), streamed ? STREAM_TIMEOUT_MS : COMMAND_TIMEOUT_MS);
|
|
1976
|
-
})
|
|
1977
|
-
]);
|
|
1978
|
-
} finally {
|
|
1979
|
-
clearTimeout(timer);
|
|
1980
|
-
this.commands.delete(id);
|
|
1981
|
-
}
|
|
1982
2210
|
}
|
|
1983
2211
|
emit(event) {
|
|
1984
2212
|
for (const handler of this.changeHandlers) {
|
|
@@ -1996,8 +2224,13 @@ class LiveKitConversation {
|
|
|
1996
2224
|
this.network = worst;
|
|
1997
2225
|
this.emit({ type: "network-changed", quality: worst });
|
|
1998
2226
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2227
|
+
fromRuntime(participant) {
|
|
2228
|
+
if (participant === undefined)
|
|
2229
|
+
return false;
|
|
2230
|
+
return participant.kind === ParticipantKind.AGENT || participant.identity === this._avatar.identity;
|
|
2231
|
+
}
|
|
2232
|
+
isAvatarParticipant(participant) {
|
|
2233
|
+
return !isAttach(participant);
|
|
2001
2234
|
}
|
|
2002
2235
|
checkForExistingAvatar() {
|
|
2003
2236
|
for (const [, participant] of this.room.remoteParticipants) {
|
|
@@ -2013,27 +2246,26 @@ class LiveKitConversation {
|
|
|
2013
2246
|
}
|
|
2014
2247
|
}
|
|
2015
2248
|
registerRoomEvents() {
|
|
2016
|
-
this.room.on(
|
|
2249
|
+
this.room.on(RoomEvent4.ConnectionStateChanged, (lkState) => {
|
|
2017
2250
|
if (this._state === "idle")
|
|
2018
2251
|
return;
|
|
2019
|
-
if (lkState ===
|
|
2252
|
+
if (lkState === ConnectionState3.Disconnected)
|
|
2020
2253
|
return;
|
|
2021
2254
|
const mapped = mapConnectionState(lkState, this.resilient && this.everConnected);
|
|
2022
2255
|
if (mapped === "connected")
|
|
2023
2256
|
this.everConnected = true;
|
|
2024
|
-
if (lkState ===
|
|
2257
|
+
if (lkState === ConnectionState3.Reconnecting)
|
|
2025
2258
|
this.setAvatarSpeaking(false);
|
|
2026
2259
|
if (mapped !== this._state) {
|
|
2027
2260
|
this.setState(mapped);
|
|
2028
2261
|
}
|
|
2029
|
-
if (lkState ===
|
|
2030
|
-
this.
|
|
2031
|
-
if (this.away)
|
|
2032
|
-
this.awaitAvatarReturn();
|
|
2262
|
+
if (lkState === ConnectionState3.Connected && this.away) {
|
|
2263
|
+
this.awaitAvatarReturn();
|
|
2033
2264
|
}
|
|
2265
|
+
if (lkState === ConnectionState3.Connected)
|
|
2266
|
+
this.sendOwedReady();
|
|
2034
2267
|
});
|
|
2035
|
-
this.room.on(
|
|
2036
|
-
this.room.on(RoomEvent3.ParticipantConnected, (participant) => {
|
|
2268
|
+
this.room.on(RoomEvent4.ParticipantConnected, (participant) => {
|
|
2037
2269
|
if (this.isAvatarParticipant(participant)) {
|
|
2038
2270
|
const tracksChanged = this._avatar.updateParticipant(participant);
|
|
2039
2271
|
this.emit({ type: "avatar-connected" });
|
|
@@ -2042,10 +2274,9 @@ class LiveKitConversation {
|
|
|
2042
2274
|
}
|
|
2043
2275
|
if (this.away)
|
|
2044
2276
|
this.avatarBack();
|
|
2045
|
-
this.catchUp();
|
|
2046
2277
|
}
|
|
2047
2278
|
});
|
|
2048
|
-
this.room.on(
|
|
2279
|
+
this.room.on(RoomEvent4.ParticipantDisconnected, (participant) => {
|
|
2049
2280
|
if (!this.isAvatarParticipant(participant))
|
|
2050
2281
|
return;
|
|
2051
2282
|
if (!this.resilient) {
|
|
@@ -2053,7 +2284,7 @@ class LiveKitConversation {
|
|
|
2053
2284
|
return;
|
|
2054
2285
|
}
|
|
2055
2286
|
queueMicrotask(() => {
|
|
2056
|
-
if (this.room.state ===
|
|
2287
|
+
if (this.room.state === ConnectionState3.Disconnected)
|
|
2057
2288
|
return;
|
|
2058
2289
|
if (isReconnecting(this.room.state)) {
|
|
2059
2290
|
this.avatarAway();
|
|
@@ -2065,16 +2296,15 @@ class LiveKitConversation {
|
|
|
2065
2296
|
}
|
|
2066
2297
|
});
|
|
2067
2298
|
});
|
|
2068
|
-
this.room.on(
|
|
2299
|
+
this.room.on(RoomEvent4.Disconnected, () => {
|
|
2069
2300
|
this.stats?.abandon();
|
|
2070
2301
|
this.stats = null;
|
|
2071
|
-
this.sequence.stop();
|
|
2072
2302
|
this.setAvatarSpeaking(false);
|
|
2073
2303
|
this.stopPresentation();
|
|
2074
2304
|
this.stopReadyFallback();
|
|
2075
2305
|
this.stopLayerFallback();
|
|
2306
|
+
this.stopAudioBacklog();
|
|
2076
2307
|
this.clearAway();
|
|
2077
|
-
this.readyOwed = false;
|
|
2078
2308
|
this._user?.cleanup();
|
|
2079
2309
|
this._avatar.updateParticipant(null);
|
|
2080
2310
|
this._user = null;
|
|
@@ -2089,7 +2319,7 @@ class LiveKitConversation {
|
|
|
2089
2319
|
}
|
|
2090
2320
|
this.setState("disconnected");
|
|
2091
2321
|
});
|
|
2092
|
-
this.room.on(
|
|
2322
|
+
this.room.on(RoomEvent4.ConnectionQualityChanged, (quality, participant) => {
|
|
2093
2323
|
const mapped = mapConnectionQuality(quality);
|
|
2094
2324
|
if (participant === this.room.localParticipant) {
|
|
2095
2325
|
this.userQuality = mapped;
|
|
@@ -2100,7 +2330,15 @@ class LiveKitConversation {
|
|
|
2100
2330
|
}
|
|
2101
2331
|
this.updateNetwork();
|
|
2102
2332
|
});
|
|
2103
|
-
this.room.on(
|
|
2333
|
+
this.room.on(RoomEvent4.TrackPublished, (publication, participant) => {
|
|
2334
|
+
if (isAttach(participant))
|
|
2335
|
+
publication.setSubscribed(false);
|
|
2336
|
+
});
|
|
2337
|
+
this.room.on(RoomEvent4.TrackSubscribed, (_track, publication, participant) => {
|
|
2338
|
+
if (isAttach(participant)) {
|
|
2339
|
+
publication.setSubscribed(false);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2104
2342
|
if (this.isAvatarParticipant(participant)) {
|
|
2105
2343
|
if (this._avatar.updateTracks()) {
|
|
2106
2344
|
this.emit({ type: "tracks-changed" });
|
|
@@ -2108,18 +2346,16 @@ class LiveKitConversation {
|
|
|
2108
2346
|
this.armReadyFallback();
|
|
2109
2347
|
}
|
|
2110
2348
|
});
|
|
2111
|
-
this.room.on(
|
|
2349
|
+
this.room.on(RoomEvent4.TrackUnsubscribed, (_track, _publication, participant) => {
|
|
2112
2350
|
if (this.isAvatarParticipant(participant)) {
|
|
2113
2351
|
if (this._avatar.updateTracks()) {
|
|
2114
2352
|
this.emit({ type: "tracks-changed" });
|
|
2115
2353
|
}
|
|
2116
2354
|
}
|
|
2117
2355
|
});
|
|
2118
|
-
this.room.on(
|
|
2119
|
-
if (
|
|
2120
|
-
this.handleSessionEvent(payload);
|
|
2356
|
+
this.room.on(RoomEvent4.DataReceived, (payload, participant, _kind, topic) => {
|
|
2357
|
+
if (isAttach(participant))
|
|
2121
2358
|
return;
|
|
2122
|
-
}
|
|
2123
2359
|
if (topic === RUNTIME_TOPIC) {
|
|
2124
2360
|
this.handleRuntimeSignal(payload);
|
|
2125
2361
|
return;
|
|
@@ -2128,6 +2364,11 @@ class LiveKitConversation {
|
|
|
2128
2364
|
this.handleClockSyncPing(payload);
|
|
2129
2365
|
return;
|
|
2130
2366
|
}
|
|
2367
|
+
if (topic === this.interfaceTopic) {
|
|
2368
|
+
if (this.fromRuntime(participant))
|
|
2369
|
+
this.onInterfacePacket(payload);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2131
2372
|
if (topic !== DATA_EVENT_TOPIC)
|
|
2132
2373
|
return;
|
|
2133
2374
|
const event = parseDataEvent(payload);
|
|
@@ -2148,71 +2389,23 @@ class LiveKitConversation {
|
|
|
2148
2389
|
return;
|
|
2149
2390
|
});
|
|
2150
2391
|
}
|
|
2151
|
-
async
|
|
2152
|
-
if (this.sessionEnded ||
|
|
2392
|
+
async sendInterfaceClose() {
|
|
2393
|
+
if (this.sessionEnded || this.room.state !== ConnectionState3.Connected) {
|
|
2153
2394
|
return;
|
|
2154
2395
|
}
|
|
2155
2396
|
this.sessionEnded = true;
|
|
2156
|
-
const
|
|
2397
|
+
const closed = new Promise((resolve) => {
|
|
2157
2398
|
this.answered = resolve;
|
|
2158
2399
|
});
|
|
2159
|
-
const
|
|
2400
|
+
const close = new TextEncoder().encode(JSON.stringify({ type: "session.close", event_id: INTERFACE_CLOSE_ID }));
|
|
2160
2401
|
await Promise.race([
|
|
2161
|
-
this.room.localParticipant.publishData(
|
|
2162
|
-
new Promise((resolve) => setTimeout(resolve,
|
|
2402
|
+
this.room.localParticipant.publishData(close, { reliable: true, topic: this.interfaceTopic }).then(() => closed),
|
|
2403
|
+
new Promise((resolve) => setTimeout(resolve, SESSION_CLOSE_TIMEOUT_MS))
|
|
2163
2404
|
]).catch(() => {
|
|
2164
2405
|
return;
|
|
2165
2406
|
});
|
|
2166
2407
|
this.answered = null;
|
|
2167
2408
|
}
|
|
2168
|
-
handleSessionEvent(payload) {
|
|
2169
|
-
const packet = parseSessionEvent(payload);
|
|
2170
|
-
if (packet === null)
|
|
2171
|
-
return;
|
|
2172
|
-
this.speaksProtocol = true;
|
|
2173
|
-
this.sendContext();
|
|
2174
|
-
const { seq, event } = packet;
|
|
2175
|
-
if (this.sequence.repeats(seq))
|
|
2176
|
-
return;
|
|
2177
|
-
if (event.type === "command-result") {
|
|
2178
|
-
if (event.id === SESSION_END_ID)
|
|
2179
|
-
this.answered?.();
|
|
2180
|
-
else if (event.id !== null) {
|
|
2181
|
-
this.commands.get(event.id)?.settle(event.result);
|
|
2182
|
-
}
|
|
2183
|
-
} else if (event.type === "command-unreadable") {
|
|
2184
|
-
const [oldest] = this.commands;
|
|
2185
|
-
if (oldest) {
|
|
2186
|
-
const [id, { type, settle }] = oldest;
|
|
2187
|
-
this.commands.delete(id);
|
|
2188
|
-
settle({ status: "rejected", error: `unknown command ${type}` });
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
this.sequence.accept(seq, event);
|
|
2192
|
-
}
|
|
2193
|
-
deliver(event) {
|
|
2194
|
-
if (event.type === "output-ready") {
|
|
2195
|
-
this.markAvatarReady();
|
|
2196
|
-
} else if (event.type === "tool-call") {
|
|
2197
|
-
this.emit({ type: "tool-call", call: event.call });
|
|
2198
|
-
} else if (event.type === "ended") {
|
|
2199
|
-
this.answered?.();
|
|
2200
|
-
if (!event.byClient)
|
|
2201
|
-
this.reportEnd(event.reason, event.message);
|
|
2202
|
-
} else if (event.type === "session-event") {
|
|
2203
|
-
if (event.event.type === "floor") {
|
|
2204
|
-
this.setAvatarSpeaking(event.event.state === "speaking");
|
|
2205
|
-
}
|
|
2206
|
-
this.emit({ type: "session-event", event: event.event });
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
newSequence() {
|
|
2210
|
-
return new EventSequence((event) => this.deliver(event), (afterSeq) => this.command({ type: "session.replay", after_seq: afterSeq }));
|
|
2211
|
-
}
|
|
2212
|
-
catchUp() {
|
|
2213
|
-
if (this.speaksProtocol)
|
|
2214
|
-
this.sequence.catchUp();
|
|
2215
|
-
}
|
|
2216
2409
|
handleRuntimeSignal(payload) {
|
|
2217
2410
|
const signal = parseRuntimeSignal(payload);
|
|
2218
2411
|
if (signal?.type === "session-ended") {
|
|
@@ -2220,7 +2413,7 @@ class LiveKitConversation {
|
|
|
2220
2413
|
this.refusal ??= setTimeout(() => this.reportEnd("refused"), REFUSAL_REASON_WAIT_MS);
|
|
2221
2414
|
return;
|
|
2222
2415
|
}
|
|
2223
|
-
this.reportEnd(signal.reason);
|
|
2416
|
+
this.reportEnd(signal.reason, signal.message, signal.endReason);
|
|
2224
2417
|
} else if (signal?.type === "output-ready") {
|
|
2225
2418
|
this.markAvatarReady();
|
|
2226
2419
|
} else if (signal?.type === "link-poor") {
|
|
@@ -2233,8 +2426,6 @@ class LiveKitConversation {
|
|
|
2233
2426
|
}
|
|
2234
2427
|
this.readyFallback = new ReadyFallback(() => avatarFramesDecoded(this.room), () => this._avatar.isVideoPlaying, () => {
|
|
2235
2428
|
this.readyFallback = null;
|
|
2236
|
-
if (this.opensOnReady)
|
|
2237
|
-
this.speaksProtocol = true;
|
|
2238
2429
|
this.markAvatarReady();
|
|
2239
2430
|
});
|
|
2240
2431
|
this.readyFallback.start();
|
|
@@ -2260,10 +2451,6 @@ class LiveKitConversation {
|
|
|
2260
2451
|
this._avatar.markReady();
|
|
2261
2452
|
this.startLayerFallback();
|
|
2262
2453
|
this.emit({ type: "avatar-ready" });
|
|
2263
|
-
if (!this.opensOnReady) {
|
|
2264
|
-
this.emit({ type: "avatar-presented" });
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
2454
|
if (!this.presentsAtFullSize) {
|
|
2268
2455
|
this.markAvatarPresented();
|
|
2269
2456
|
return;
|
|
@@ -2277,42 +2464,19 @@ class LiveKitConversation {
|
|
|
2277
2464
|
}
|
|
2278
2465
|
markAvatarPresented() {
|
|
2279
2466
|
this.presentation = null;
|
|
2280
|
-
if (this.
|
|
2281
|
-
this.ready().catch(() => {
|
|
2282
|
-
return;
|
|
2283
|
-
});
|
|
2284
|
-
} else if (this.resilient && this.speaksProtocol && isReconnecting(this.room.state)) {
|
|
2467
|
+
if (this.opensOnReady) {
|
|
2285
2468
|
this.readyOwed = true;
|
|
2469
|
+
this.sendOwedReady();
|
|
2286
2470
|
}
|
|
2287
2471
|
this.emit({ type: "avatar-presented" });
|
|
2288
2472
|
}
|
|
2289
|
-
sendContext() {
|
|
2290
|
-
if (this.contextSent !== null || this.initialContext.length === 0)
|
|
2291
|
-
return;
|
|
2292
|
-
this.contextSent = this.command({
|
|
2293
|
-
type: "session.context",
|
|
2294
|
-
items: this.initialContext.map(contextItem)
|
|
2295
|
-
}).then(() => {
|
|
2296
|
-
return;
|
|
2297
|
-
}, () => {
|
|
2298
|
-
return;
|
|
2299
|
-
});
|
|
2300
|
-
}
|
|
2301
|
-
ready() {
|
|
2302
|
-
this.sendContext();
|
|
2303
|
-
const ready = () => this.command({ type: "session.ready" });
|
|
2304
|
-
return this.contextSent === null ? ready() : this.contextSent.then(ready);
|
|
2305
|
-
}
|
|
2306
2473
|
sendOwedReady() {
|
|
2307
|
-
if (!this.readyOwed ||
|
|
2308
|
-
return;
|
|
2309
|
-
this.readySending = true;
|
|
2310
|
-
this.ready().then(() => {
|
|
2311
|
-
this.readyOwed = false;
|
|
2312
|
-
}).catch(() => {
|
|
2474
|
+
if (!this.readyOwed || this.room.state !== ConnectionState3.Connected)
|
|
2313
2475
|
return;
|
|
2314
|
-
|
|
2315
|
-
|
|
2476
|
+
this.readyOwed = false;
|
|
2477
|
+
const ready = new TextEncoder().encode(JSON.stringify({ type: "session.ready", id: READY_ID }));
|
|
2478
|
+
this.room.localParticipant.publishData(ready, { reliable: true, topic: SESSION_TOPIC }).catch(() => {
|
|
2479
|
+
this.readyOwed = true;
|
|
2316
2480
|
});
|
|
2317
2481
|
}
|
|
2318
2482
|
avatarLeft() {
|
|
@@ -2360,7 +2524,7 @@ class LiveKitConversation {
|
|
|
2360
2524
|
this.awaitRuntimeReturn(RUNTIME_RECHECK_MS);
|
|
2361
2525
|
return;
|
|
2362
2526
|
}
|
|
2363
|
-
if (this.room.state ===
|
|
2527
|
+
if (this.room.state === ConnectionState3.Disconnected)
|
|
2364
2528
|
return;
|
|
2365
2529
|
if (this.avatarInRoom())
|
|
2366
2530
|
return;
|
|
@@ -2406,16 +2570,38 @@ class LiveKitConversation {
|
|
|
2406
2570
|
this.layerFallback?.stop();
|
|
2407
2571
|
this.layerFallback = null;
|
|
2408
2572
|
}
|
|
2573
|
+
startAudioBacklog() {
|
|
2574
|
+
if (!this.dropsAudioBacklog || this.audioBacklog !== null)
|
|
2575
|
+
return;
|
|
2576
|
+
this.audioBacklog = new AudioBacklogWatch(() => avatarJitterBuffer(this.room), (backlog) => {
|
|
2577
|
+
const behind = Math.round(backlog.jitterBufferMs - backlog.targetMs);
|
|
2578
|
+
console.info(`Nuance SDK: the avatar's audio was ${behind} ms behind (jitter buffer ${Math.round(backlog.jitterBufferMs)} ms, target ${Math.round(backlog.targetMs)} ms); dropped it to live.`);
|
|
2579
|
+
this.stats?.noteAudioDrop(backlog);
|
|
2580
|
+
resubscribeAvatarAudio(this.room).catch(() => {
|
|
2581
|
+
return;
|
|
2582
|
+
});
|
|
2583
|
+
});
|
|
2584
|
+
this.audioBacklog.start();
|
|
2585
|
+
}
|
|
2586
|
+
stopAudioBacklog() {
|
|
2587
|
+
this.audioBacklog?.stop();
|
|
2588
|
+
this.audioBacklog = null;
|
|
2589
|
+
}
|
|
2409
2590
|
stopPresentation() {
|
|
2410
2591
|
this.presentation?.stop();
|
|
2411
2592
|
this.presentation = null;
|
|
2412
2593
|
}
|
|
2413
|
-
reportEnd(reason, message) {
|
|
2594
|
+
reportEnd(reason, message, cause) {
|
|
2414
2595
|
this.stopRefusal();
|
|
2415
2596
|
if (this.sessionEnded)
|
|
2416
2597
|
return;
|
|
2417
2598
|
this.sessionEnded = true;
|
|
2418
|
-
this.emit(
|
|
2599
|
+
this.emit({
|
|
2600
|
+
type: "session-ended",
|
|
2601
|
+
reason,
|
|
2602
|
+
...cause !== undefined && { endReason: cause },
|
|
2603
|
+
...message !== undefined && { message }
|
|
2604
|
+
});
|
|
2419
2605
|
}
|
|
2420
2606
|
stopRefusal() {
|
|
2421
2607
|
if (this.refusal === null)
|
|
@@ -2436,16 +2622,16 @@ class LiveKitConversation {
|
|
|
2436
2622
|
|
|
2437
2623
|
// src/conversation.ts
|
|
2438
2624
|
var DEFAULT_HOST = "https://api.nuancelabs.ai";
|
|
2625
|
+
var LIVE_INTERFACE = "nuance_live";
|
|
2626
|
+
var LIVE_INTERFACES = new Set([LIVE_INTERFACE, "gpt_live"]);
|
|
2439
2627
|
var QUEUE_HEARTBEAT_INTERVAL_MS = 5000;
|
|
2440
2628
|
var HEARTBEAT_INTERVAL_MS = 5000;
|
|
2441
2629
|
var EVENTS_RETRY_MS = 5000;
|
|
2442
2630
|
var ADMISSION_FAILED_REQUESTS = 6;
|
|
2443
2631
|
var BUSY_RETRY_LIMIT_MS = 60000;
|
|
2444
2632
|
var BUSY_RETRY_DEFAULT_MS = 3000;
|
|
2633
|
+
var CAPACITY_RETRY_LIMIT_MS = 15000;
|
|
2445
2634
|
var LOST_WINDOW_MS = 30000;
|
|
2446
|
-
function createNuanceConversation(options = {}) {
|
|
2447
|
-
return new NuanceConversation(options);
|
|
2448
|
-
}
|
|
2449
2635
|
var TERMINAL_STATUSES = new Set([
|
|
2450
2636
|
"failed",
|
|
2451
2637
|
"expired",
|
|
@@ -2474,9 +2660,15 @@ function isMaintenanceBody(body) {
|
|
|
2474
2660
|
function isBusyBody(body) {
|
|
2475
2661
|
return isRecord(body) && body.code === "busy";
|
|
2476
2662
|
}
|
|
2663
|
+
function isCapacityBody(body) {
|
|
2664
|
+
return isRecord(body) && body.code === "capacity_unavailable";
|
|
2665
|
+
}
|
|
2477
2666
|
function isGuestLimitBody(body) {
|
|
2478
2667
|
return isRecord(body) && body.code === "guest_limit";
|
|
2479
2668
|
}
|
|
2669
|
+
function refusedByServer(error) {
|
|
2670
|
+
return error instanceof ApiError || error instanceof ActiveSessionError || error instanceof LegalRequiredError || error instanceof MaintenanceError || error instanceof GuestLimitError || error instanceof PolicyCurfewError;
|
|
2671
|
+
}
|
|
2480
2672
|
function isCurfewBody(body) {
|
|
2481
2673
|
return isRecord(body) && body.code === "policy_curfew";
|
|
2482
2674
|
}
|
|
@@ -2501,14 +2693,18 @@ class NuanceConversation {
|
|
|
2501
2693
|
ticker;
|
|
2502
2694
|
sessionOverrides;
|
|
2503
2695
|
personality;
|
|
2504
|
-
tools;
|
|
2505
|
-
initialContext;
|
|
2506
2696
|
variant;
|
|
2507
2697
|
maxSessionSeconds;
|
|
2508
2698
|
videoCapture;
|
|
2509
2699
|
startOn;
|
|
2700
|
+
greenScreen;
|
|
2701
|
+
alphaFrames;
|
|
2510
2702
|
mediaDevices;
|
|
2511
2703
|
resilientReconnect;
|
|
2704
|
+
dropAudioBacklog;
|
|
2705
|
+
interfaceName = LIVE_INTERFACE;
|
|
2706
|
+
stayConnectedOnEnd;
|
|
2707
|
+
joined = false;
|
|
2512
2708
|
delegate = null;
|
|
2513
2709
|
delegateUnsubscribe = null;
|
|
2514
2710
|
stopBeats = null;
|
|
@@ -2519,6 +2715,7 @@ class NuanceConversation {
|
|
|
2519
2715
|
reconnectingAt = null;
|
|
2520
2716
|
lostContact = false;
|
|
2521
2717
|
attempt = 0;
|
|
2718
|
+
toldPreparing = -1;
|
|
2522
2719
|
lastPosition = null;
|
|
2523
2720
|
lastEstimate = null;
|
|
2524
2721
|
capped = false;
|
|
@@ -2541,24 +2738,28 @@ class NuanceConversation {
|
|
|
2541
2738
|
this.ticker = deps.ticker ?? pageTicker;
|
|
2542
2739
|
this.sessionOverrides = options.sessionOverrides ?? {};
|
|
2543
2740
|
this.personality = options.personality;
|
|
2544
|
-
this.tools = options.tools;
|
|
2545
|
-
this.initialContext = options.initialContext ?? [];
|
|
2546
2741
|
this.variant = options.variant;
|
|
2547
2742
|
this.maxSessionSeconds = options.maxSessionSeconds;
|
|
2548
2743
|
this.videoCapture = options.videoCapture;
|
|
2549
2744
|
this.startOn = options.startOn ?? "first-frame";
|
|
2745
|
+
this.greenScreen = options.greenScreen ?? false;
|
|
2746
|
+
this.alphaFrames = options.alphaFrames ?? false;
|
|
2550
2747
|
this.mediaDevices = options.mediaDevices;
|
|
2551
2748
|
this.resilientReconnect = options.resilientReconnect ?? false;
|
|
2749
|
+
this.dropAudioBacklog = options.dropAudioBacklog ?? false;
|
|
2750
|
+
this.stayConnectedOnEnd = options.stayConnectedOnEnd ?? false;
|
|
2552
2751
|
}
|
|
2553
2752
|
async connect() {
|
|
2554
2753
|
if (this._state === "connecting" || this._state === "queued" || this._state === "connected" || this._state === "reconnecting") {
|
|
2555
2754
|
throw new Error(`Cannot connect while in "${this._state}" state`);
|
|
2556
2755
|
}
|
|
2557
2756
|
const attempt = ++this.attempt;
|
|
2757
|
+
let created = false;
|
|
2558
2758
|
this.setState("connecting");
|
|
2559
2759
|
this.clearLimit();
|
|
2560
2760
|
this.capped = false;
|
|
2561
2761
|
this.ended = false;
|
|
2762
|
+
this.joined = false;
|
|
2562
2763
|
this.reconnectingAt = null;
|
|
2563
2764
|
this.lostContact = false;
|
|
2564
2765
|
this.lastPosition = null;
|
|
@@ -2572,21 +2773,27 @@ class NuanceConversation {
|
|
|
2572
2773
|
try {
|
|
2573
2774
|
const request = {
|
|
2574
2775
|
session_overrides: this.sessionOverrides,
|
|
2575
|
-
|
|
2776
|
+
...this.startOn === "full-quality" ? { client_ready: true } : {}
|
|
2576
2777
|
};
|
|
2577
2778
|
const timezone = this.timezone();
|
|
2578
2779
|
if (timezone)
|
|
2579
2780
|
request.timezone = timezone;
|
|
2781
|
+
const camera = await cameraSource(mediaDevices);
|
|
2782
|
+
if (camera)
|
|
2783
|
+
request.camera = camera;
|
|
2580
2784
|
if (this.personality)
|
|
2581
2785
|
request.personality = this.personality;
|
|
2582
|
-
if (this.tools)
|
|
2583
|
-
request.tools = this.tools;
|
|
2584
2786
|
if (this.variant)
|
|
2585
2787
|
request.variant = this.variant;
|
|
2586
2788
|
if (this.maxSessionSeconds !== undefined) {
|
|
2587
2789
|
request.max_session_seconds = this.maxSessionSeconds;
|
|
2588
2790
|
}
|
|
2791
|
+
if (this.greenScreen)
|
|
2792
|
+
request.green_screen = true;
|
|
2793
|
+
if (this.alphaFrames)
|
|
2794
|
+
request.alpha_frames = true;
|
|
2589
2795
|
conversation = await this.createWhenNotBusy(request, attempt);
|
|
2796
|
+
created = true;
|
|
2590
2797
|
} catch (error) {
|
|
2591
2798
|
if (error instanceof ApiError && error.status === 409) {
|
|
2592
2799
|
throw new ActiveSessionError;
|
|
@@ -2622,7 +2829,7 @@ class NuanceConversation {
|
|
|
2622
2829
|
if (event.type === "session-ended" && (event.reason === "connection-lost" || event.reason === "refused")) {
|
|
2623
2830
|
const held = this.held;
|
|
2624
2831
|
this.held = null;
|
|
2625
|
-
this.endSession(event.reason, event.message);
|
|
2832
|
+
this.endSession(event.reason, event.message, event.endReason, false);
|
|
2626
2833
|
this.disconnect().catch(() => {});
|
|
2627
2834
|
if (held !== null) {
|
|
2628
2835
|
this.api.abandonConversation(held).catch(() => {});
|
|
@@ -2630,12 +2837,26 @@ class NuanceConversation {
|
|
|
2630
2837
|
return;
|
|
2631
2838
|
}
|
|
2632
2839
|
if (event.type === "session-ended") {
|
|
2633
|
-
this.endSession(event.reason, event.message);
|
|
2840
|
+
this.endSession(event.reason, event.message, event.endReason);
|
|
2634
2841
|
return;
|
|
2635
2842
|
}
|
|
2636
2843
|
this.emit(event);
|
|
2637
2844
|
});
|
|
2638
|
-
|
|
2845
|
+
if (this.ended && !this.stayConnectedOnEnd) {
|
|
2846
|
+
await this.disconnect();
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
try {
|
|
2850
|
+
await this.delegate.connect();
|
|
2851
|
+
} catch (error) {
|
|
2852
|
+
if (!this.ended || this.stayConnectedOnEnd)
|
|
2853
|
+
throw error;
|
|
2854
|
+
}
|
|
2855
|
+
this.joined = true;
|
|
2856
|
+
if (this.ended && !this.stayConnectedOnEnd) {
|
|
2857
|
+
await this.disconnect();
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2639
2860
|
if (grant === null) {
|
|
2640
2861
|
this.startHeartbeat(conversation.id, HEARTBEAT_INTERVAL_MS);
|
|
2641
2862
|
}
|
|
@@ -2643,6 +2864,19 @@ class NuanceConversation {
|
|
|
2643
2864
|
if (attempt !== this.attempt) {
|
|
2644
2865
|
throw error;
|
|
2645
2866
|
}
|
|
2867
|
+
if (!created && refusedByServer(error)) {
|
|
2868
|
+
const refused = {
|
|
2869
|
+
type: "session-ended",
|
|
2870
|
+
reason: "refused",
|
|
2871
|
+
message: error.message,
|
|
2872
|
+
error
|
|
2873
|
+
};
|
|
2874
|
+
for (const handler of this.changeHandlers) {
|
|
2875
|
+
try {
|
|
2876
|
+
handler(refused);
|
|
2877
|
+
} catch {}
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2646
2880
|
await this.disconnect().catch(() => {});
|
|
2647
2881
|
this.setState("failed");
|
|
2648
2882
|
throw error;
|
|
@@ -2650,6 +2884,7 @@ class NuanceConversation {
|
|
|
2650
2884
|
}
|
|
2651
2885
|
async disconnect() {
|
|
2652
2886
|
this.attempt += 1;
|
|
2887
|
+
this.joined = false;
|
|
2653
2888
|
this.clearLimit();
|
|
2654
2889
|
this.stopHeartbeat();
|
|
2655
2890
|
this.stopEvents?.();
|
|
@@ -2658,7 +2893,12 @@ class NuanceConversation {
|
|
|
2658
2893
|
this.admitted = null;
|
|
2659
2894
|
this.delegateUnsubscribe?.();
|
|
2660
2895
|
this.delegateUnsubscribe = null;
|
|
2661
|
-
|
|
2896
|
+
const trailing = this.ended ? this.delegate?.onInterfaceEvent((event) => this.emit({ type: "interface-event", event })) : undefined;
|
|
2897
|
+
try {
|
|
2898
|
+
await this.delegate?.disconnect();
|
|
2899
|
+
} finally {
|
|
2900
|
+
trailing?.();
|
|
2901
|
+
}
|
|
2662
2902
|
this.delegate = null;
|
|
2663
2903
|
await this.release();
|
|
2664
2904
|
this.setState("disconnected");
|
|
@@ -2669,6 +2909,9 @@ class NuanceConversation {
|
|
|
2669
2909
|
get id() {
|
|
2670
2910
|
return this._id;
|
|
2671
2911
|
}
|
|
2912
|
+
get interface() {
|
|
2913
|
+
return this.interfaceName;
|
|
2914
|
+
}
|
|
2672
2915
|
get user() {
|
|
2673
2916
|
if (!this.delegate) {
|
|
2674
2917
|
throw new Error("Not connected — call connect() first");
|
|
@@ -2681,45 +2924,18 @@ class NuanceConversation {
|
|
|
2681
2924
|
}
|
|
2682
2925
|
return this.delegate.avatar;
|
|
2683
2926
|
}
|
|
2684
|
-
async
|
|
2685
|
-
if (!this.delegate) {
|
|
2686
|
-
throw new Error("Not connected — call connect() first");
|
|
2687
|
-
}
|
|
2688
|
-
return this.delegate.setBackground(background);
|
|
2689
|
-
}
|
|
2690
|
-
async sendText(text) {
|
|
2691
|
-
if (!this.delegate) {
|
|
2692
|
-
throw new Error("Not connected — call connect() first");
|
|
2693
|
-
}
|
|
2694
|
-
return this.delegate.sendText(text);
|
|
2695
|
-
}
|
|
2696
|
-
async holdAgent() {
|
|
2697
|
-
if (!this.delegate) {
|
|
2698
|
-
throw new Error("Not connected — call connect() first");
|
|
2699
|
-
}
|
|
2700
|
-
return this.delegate.holdAgent();
|
|
2701
|
-
}
|
|
2702
|
-
async releaseAgent() {
|
|
2927
|
+
async sendInterfaceEvent(event) {
|
|
2703
2928
|
if (!this.delegate) {
|
|
2704
2929
|
throw new Error("Not connected — call connect() first");
|
|
2705
2930
|
}
|
|
2706
|
-
return this.delegate.
|
|
2707
|
-
}
|
|
2708
|
-
async sendToolResult(callId, output, options) {
|
|
2709
|
-
if (!this.delegate) {
|
|
2710
|
-
throw new Error("Not connected — call connect() first");
|
|
2711
|
-
}
|
|
2712
|
-
return this.delegate.sendToolResult(callId, output, options);
|
|
2931
|
+
return this.delegate.sendInterfaceEvent(event);
|
|
2713
2932
|
}
|
|
2714
2933
|
onChange(handler) {
|
|
2715
2934
|
return this.onEvent((event) => {
|
|
2716
|
-
if (event.type !== "data-event" && event.type !== "
|
|
2935
|
+
if (event.type !== "data-event" && event.type !== "interface-event")
|
|
2717
2936
|
handler(event);
|
|
2718
2937
|
});
|
|
2719
2938
|
}
|
|
2720
|
-
on(type, handler) {
|
|
2721
|
-
return this.onEvent(sessionEventsOf(type, handler));
|
|
2722
|
-
}
|
|
2723
2939
|
onEvent(handler) {
|
|
2724
2940
|
this.changeHandlers.add(handler);
|
|
2725
2941
|
return () => {
|
|
@@ -2733,10 +2949,10 @@ class NuanceConversation {
|
|
|
2733
2949
|
}
|
|
2734
2950
|
});
|
|
2735
2951
|
}
|
|
2736
|
-
|
|
2952
|
+
onInterfaceEvent(handler) {
|
|
2737
2953
|
return this.onEvent((event) => {
|
|
2738
|
-
if (event.type === "
|
|
2739
|
-
handler(event.
|
|
2954
|
+
if (event.type === "interface-event")
|
|
2955
|
+
handler(event.event);
|
|
2740
2956
|
});
|
|
2741
2957
|
}
|
|
2742
2958
|
async waitForSlot(conversationId, attempt) {
|
|
@@ -2745,7 +2961,7 @@ class NuanceConversation {
|
|
|
2745
2961
|
this.followLive(conversationId, admission.after, attempt);
|
|
2746
2962
|
return admission.grant;
|
|
2747
2963
|
}
|
|
2748
|
-
this.admitted =
|
|
2964
|
+
this.admitted = resolvers();
|
|
2749
2965
|
this.startHeartbeat(conversationId, QUEUE_HEARTBEAT_INTERVAL_MS);
|
|
2750
2966
|
await this.admitted.promise;
|
|
2751
2967
|
return null;
|
|
@@ -2798,6 +3014,8 @@ class NuanceConversation {
|
|
|
2798
3014
|
if (event.type === "queued" && event.position != null) {
|
|
2799
3015
|
this.reportPlace(event.position, event.estimated_wait_seconds ?? null, null, event.preparing ?? false);
|
|
2800
3016
|
}
|
|
3017
|
+
if (event.type === "preparing")
|
|
3018
|
+
this.reportPreparing();
|
|
2801
3019
|
}
|
|
2802
3020
|
}
|
|
2803
3021
|
} finally {
|
|
@@ -2838,7 +3056,7 @@ class NuanceConversation {
|
|
|
2838
3056
|
this.armLimit(event.seconds_remaining, receivedAt);
|
|
2839
3057
|
}
|
|
2840
3058
|
if (event.type === "ended") {
|
|
2841
|
-
this.endSession(this.endReason(event.reason));
|
|
3059
|
+
this.endSession(this.endReason(event.reason), undefined, v1EndReason(event.reason));
|
|
2842
3060
|
return;
|
|
2843
3061
|
}
|
|
2844
3062
|
}
|
|
@@ -2853,6 +3071,8 @@ class NuanceConversation {
|
|
|
2853
3071
|
return "time-limit";
|
|
2854
3072
|
if (reason === "policy_violation")
|
|
2855
3073
|
return "policy-violation";
|
|
3074
|
+
if (reason === "age_check")
|
|
3075
|
+
return "age-check";
|
|
2856
3076
|
if (reason === "runtime_silent" || this.cutOff())
|
|
2857
3077
|
return "connection-lost";
|
|
2858
3078
|
return "ended";
|
|
@@ -2873,6 +3093,11 @@ class NuanceConversation {
|
|
|
2873
3093
|
if (conversation.provider !== "livekit") {
|
|
2874
3094
|
throw new Error(`Unsupported provider ${conversation.provider}`);
|
|
2875
3095
|
}
|
|
3096
|
+
const named = conversation.interface ?? null;
|
|
3097
|
+
if (named === null || !LIVE_INTERFACES.has(named)) {
|
|
3098
|
+
throw new Error(`The session runs on ${named ?? "nuance.v1"}, which this SDK does not speak: it needs Nuance Live (interface "nuance_live")`);
|
|
3099
|
+
}
|
|
3100
|
+
this.interfaceName = named;
|
|
2876
3101
|
const connection = grant ?? await this.connectRoom(conversation.id);
|
|
2877
3102
|
if (attempt !== this.attempt) {
|
|
2878
3103
|
throw new DisconnectedError;
|
|
@@ -2883,10 +3108,11 @@ class NuanceConversation {
|
|
|
2883
3108
|
mediaDevices,
|
|
2884
3109
|
cameraEnabled: !this.sessionOverrides.disable_camera && mediaDevices.cameraEnabled !== false,
|
|
2885
3110
|
videoCapture: this.videoCapture,
|
|
2886
|
-
opensOnReady: conversation.provider_metadata?.client_ready === true,
|
|
2887
3111
|
presentsAtFullSize: this.startOn === "full-quality",
|
|
2888
|
-
|
|
2889
|
-
resilientReconnect: this.resilient
|
|
3112
|
+
opensOnReady: conversation.provider_metadata?.client_ready === true,
|
|
3113
|
+
resilientReconnect: this.resilient,
|
|
3114
|
+
dropAudioBacklog: typeof this.dropAudioBacklog === "function" ? this.dropAudioBacklog() : this.dropAudioBacklog,
|
|
3115
|
+
interface: this.interfaceName
|
|
2890
3116
|
});
|
|
2891
3117
|
}
|
|
2892
3118
|
async connectRoom(conversationId) {
|
|
@@ -2897,18 +3123,24 @@ class NuanceConversation {
|
|
|
2897
3123
|
return response.connection;
|
|
2898
3124
|
}
|
|
2899
3125
|
async createWhenNotBusy(request, attempt) {
|
|
2900
|
-
const
|
|
3126
|
+
const started = Date.now();
|
|
2901
3127
|
for (;; ) {
|
|
2902
3128
|
try {
|
|
2903
3129
|
return await this.api.createConversation(request);
|
|
2904
3130
|
} catch (error) {
|
|
2905
|
-
if (!(error instanceof ApiError) ||
|
|
3131
|
+
if (!(error instanceof ApiError) || attempt !== this.attempt) {
|
|
2906
3132
|
throw error;
|
|
2907
3133
|
}
|
|
2908
|
-
const
|
|
2909
|
-
|
|
3134
|
+
const busy = error.status === 503 && isBusyBody(error.body);
|
|
3135
|
+
const full = error.status === 429 && isCapacityBody(error.body);
|
|
3136
|
+
if (!busy && !full)
|
|
2910
3137
|
throw error;
|
|
2911
|
-
|
|
3138
|
+
const asked = error.body.retry_after_seconds;
|
|
3139
|
+
const waitMs = typeof asked === "number" ? asked * 1000 : BUSY_RETRY_DEFAULT_MS;
|
|
3140
|
+
const limitMs = busy ? BUSY_RETRY_LIMIT_MS : CAPACITY_RETRY_LIMIT_MS;
|
|
3141
|
+
if (Date.now() + waitMs > started + limitMs)
|
|
3142
|
+
throw error;
|
|
3143
|
+
if (busy && this._state !== "queued")
|
|
2912
3144
|
this.setState("queued");
|
|
2913
3145
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
2914
3146
|
if (attempt !== this.attempt)
|
|
@@ -2979,11 +3211,21 @@ class NuanceConversation {
|
|
|
2979
3211
|
this.admitted = null;
|
|
2980
3212
|
return;
|
|
2981
3213
|
}
|
|
3214
|
+
if (state.status === "queued" && state.position == null && state.preparing) {
|
|
3215
|
+
this.reportPreparing();
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
2982
3218
|
if (state.status !== "queued" || state.position == null) {
|
|
2983
3219
|
return;
|
|
2984
3220
|
}
|
|
2985
3221
|
this.reportPlace(state.position, state.estimated_wait_seconds ?? null, state.pool_slots ?? null, state.preparing ?? false);
|
|
2986
3222
|
}
|
|
3223
|
+
reportPreparing() {
|
|
3224
|
+
if (this.toldPreparing === this.attempt)
|
|
3225
|
+
return;
|
|
3226
|
+
this.toldPreparing = this.attempt;
|
|
3227
|
+
this.emit({ type: "avatar-preparing" });
|
|
3228
|
+
}
|
|
2987
3229
|
reportPlace(position, estimate, poolSlots, preparing) {
|
|
2988
3230
|
if (this._state !== "queued") {
|
|
2989
3231
|
this.setState("queued");
|
|
@@ -3029,14 +3271,28 @@ class NuanceConversation {
|
|
|
3029
3271
|
}
|
|
3030
3272
|
await this.api.abandonConversation(held).catch(() => {});
|
|
3031
3273
|
}
|
|
3032
|
-
endSession(reason, message) {
|
|
3274
|
+
endSession(reason, message, cause, leave = true) {
|
|
3033
3275
|
if (this.ended) {
|
|
3034
3276
|
return;
|
|
3035
3277
|
}
|
|
3036
3278
|
this.ended = true;
|
|
3037
3279
|
this.clearLimit();
|
|
3038
3280
|
this.stopHeartbeat();
|
|
3039
|
-
this.emit(
|
|
3281
|
+
this.emit({
|
|
3282
|
+
type: "session-ended",
|
|
3283
|
+
reason,
|
|
3284
|
+
...cause !== undefined && { endReason: cause },
|
|
3285
|
+
...message !== undefined && { message }
|
|
3286
|
+
});
|
|
3287
|
+
if (!leave || this.stayConnectedOnEnd)
|
|
3288
|
+
return;
|
|
3289
|
+
if (!this.joined) {
|
|
3290
|
+
this.delegateUnsubscribe?.();
|
|
3291
|
+
this.delegateUnsubscribe = null;
|
|
3292
|
+
this.delegate?.disconnect().catch(() => {});
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
this.disconnect().catch(() => {});
|
|
3040
3296
|
}
|
|
3041
3297
|
stopHeartbeat() {
|
|
3042
3298
|
this.stopBeats?.();
|
|
@@ -3044,7 +3300,7 @@ class NuanceConversation {
|
|
|
3044
3300
|
}
|
|
3045
3301
|
emit(event) {
|
|
3046
3302
|
for (const handler of this.changeHandlers) {
|
|
3047
|
-
handler
|
|
3303
|
+
deliver(handler, event);
|
|
3048
3304
|
}
|
|
3049
3305
|
}
|
|
3050
3306
|
atLimit() {
|
|
@@ -3070,17 +3326,43 @@ class NuanceConversation {
|
|
|
3070
3326
|
this.emit({ type: "state-changed", state });
|
|
3071
3327
|
}
|
|
3072
3328
|
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3329
|
+
|
|
3330
|
+
// src/create.ts
|
|
3331
|
+
var KNOWN2 = {
|
|
3332
|
+
host: true,
|
|
3333
|
+
headers: true,
|
|
3334
|
+
token: true,
|
|
3335
|
+
sessionOverrides: true,
|
|
3336
|
+
personality: true,
|
|
3337
|
+
variant: true,
|
|
3338
|
+
maxSessionSeconds: true,
|
|
3339
|
+
videoCapture: true,
|
|
3340
|
+
mediaDevices: true,
|
|
3341
|
+
startOn: true,
|
|
3342
|
+
greenScreen: true,
|
|
3343
|
+
alphaFrames: true,
|
|
3344
|
+
resilientReconnect: true,
|
|
3345
|
+
dropAudioBacklog: true,
|
|
3346
|
+
stayConnectedOnEnd: true
|
|
3347
|
+
};
|
|
3348
|
+
var MEDIA_DEVICES = new Set([
|
|
3349
|
+
"microphoneId",
|
|
3350
|
+
"cameraId",
|
|
3351
|
+
"cameraEnabled",
|
|
3352
|
+
"cameraFacing"
|
|
3353
|
+
]);
|
|
3354
|
+
function warnUnknown(options) {
|
|
3355
|
+
for (const name in options) {
|
|
3356
|
+
if (Object.hasOwn(KNOWN2, name))
|
|
3357
|
+
continue;
|
|
3358
|
+
const hint = MEDIA_DEVICES.has(name) ? ` It belongs under mediaDevices: { ${name} }.` : "";
|
|
3359
|
+
console.warn(`Nuance SDK: unknown option "${name}" is ignored.${hint}`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
function createNuanceConversation(options = {}) {
|
|
3363
|
+
warnUnknown(options);
|
|
3364
|
+
return new NuanceConversation(options);
|
|
3365
|
+
}
|
|
3084
3366
|
export {
|
|
3085
3367
|
createNuanceConversation,
|
|
3086
3368
|
PolicyCurfewError,
|