@nuancelabs/js-sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +34 -2
  2. package/index.d.ts +480 -0
  3. package/index.js +2490 -0
  4. package/package.json +23 -4
package/index.js ADDED
@@ -0,0 +1,2490 @@
1
+ // src/core/is-record.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+
6
+ // src/core/sdk-version.ts
7
+ var SDK_HEADER = `js/${"0.1.0"}`;
8
+
9
+ // src/utils.ts
10
+ function normalizeHost(host) {
11
+ return host.replace(/\/+$/, "");
12
+ }
13
+
14
+ // src/api/client.ts
15
+ class ApiError extends Error {
16
+ status;
17
+ body;
18
+ constructor(status, detail, body = null) {
19
+ super(detail);
20
+ this.name = "ApiError";
21
+ this.status = status;
22
+ this.body = body;
23
+ }
24
+ }
25
+
26
+ class NuanceApiClient {
27
+ host;
28
+ headersProvider;
29
+ tokenProvider;
30
+ tokens = new Map;
31
+ constructor(config) {
32
+ this.host = normalizeHost(config.host);
33
+ this.headersProvider = config.headers;
34
+ this.tokenProvider = config.token;
35
+ }
36
+ async createConversation(request) {
37
+ const token = this.tokenProvider ? await this.tokenProvider() : undefined;
38
+ const conversation = await this.request("/conversation", { method: "POST", body: request, token });
39
+ if (token !== undefined)
40
+ this.tokens.set(conversation.id, token);
41
+ return conversation;
42
+ }
43
+ async getConversation(conversationId) {
44
+ return this.request(`/conversation/${conversationId}`, { token: this.tokens.get(conversationId) });
45
+ }
46
+ async connectConversation(conversationId, request = {}) {
47
+ return this.request(`/conversation/${conversationId}/connect`, {
48
+ method: "POST",
49
+ body: request,
50
+ token: this.tokens.get(conversationId)
51
+ });
52
+ }
53
+ async heartbeatConversation(conversationId) {
54
+ return await this.request(`/conversation/${conversationId}/heartbeat`, { method: "POST", token: this.tokens.get(conversationId) });
55
+ }
56
+ async abandonConversation(conversationId) {
57
+ return await this.request(`/conversation/${conversationId}/abandon`, { method: "POST", token: this.tokens.get(conversationId) });
58
+ }
59
+ async request(path, options) {
60
+ const headers = new Headers(await this.resolveHeaders());
61
+ headers.set("X-Nuance-SDK", SDK_HEADER);
62
+ if (options?.token !== undefined) {
63
+ headers.set("Authorization", `Bearer ${options.token}`);
64
+ }
65
+ if (options?.body !== undefined) {
66
+ headers.set("Content-Type", "application/json");
67
+ }
68
+ const response = await fetch(`${this.host}${path}`, {
69
+ method: options?.method ?? "GET",
70
+ headers,
71
+ body: options?.body !== undefined ? JSON.stringify(options.body) : undefined
72
+ });
73
+ if (!response.ok) {
74
+ let detail = response.statusText;
75
+ let body = null;
76
+ try {
77
+ body = await response.json();
78
+ if (isRecord(body) && typeof body.detail === "string") {
79
+ detail = body.detail;
80
+ }
81
+ } catch {
82
+ body = null;
83
+ }
84
+ throw new ApiError(response.status, detail, body);
85
+ }
86
+ if (response.status === 204) {
87
+ return;
88
+ }
89
+ return response.json();
90
+ }
91
+ async resolveHeaders() {
92
+ return this.headersProvider ? await this.headersProvider() : {};
93
+ }
94
+ }
95
+
96
+ // src/core/active-session-error.ts
97
+ class ActiveSessionError extends Error {
98
+ constructor() {
99
+ super("an active conversation session already exists");
100
+ this.name = "ActiveSessionError";
101
+ }
102
+ }
103
+
104
+ // src/core/legal-required-error.ts
105
+ class LegalRequiredError extends Error {
106
+ constructor() {
107
+ super("the current terms of service must be accepted first");
108
+ this.name = "LegalRequiredError";
109
+ }
110
+ }
111
+
112
+ // src/core/maintenance-error.ts
113
+ class MaintenanceError extends Error {
114
+ constructor(message) {
115
+ super(message);
116
+ this.name = "MaintenanceError";
117
+ }
118
+ }
119
+
120
+ // src/core/media-error.ts
121
+ function reasonFor(cause) {
122
+ const name = cause instanceof Error ? cause.name : "";
123
+ switch (name) {
124
+ case "NotAllowedError":
125
+ case "SecurityError":
126
+ return "permission-denied";
127
+ case "NotFoundError":
128
+ case "OverconstrainedError":
129
+ return "device-not-found";
130
+ case "NotReadableError":
131
+ case "AbortError":
132
+ return "device-in-use";
133
+ default:
134
+ return "unknown";
135
+ }
136
+ }
137
+
138
+ class MediaAccessError extends Error {
139
+ device;
140
+ reason;
141
+ constructor(cause, device = "unknown") {
142
+ const reason = reasonFor(cause);
143
+ super(`media access failed: ${device} ${reason}`);
144
+ this.name = "MediaAccessError";
145
+ this.device = device;
146
+ this.reason = reason;
147
+ }
148
+ }
149
+ async function ensureMicrophoneAccess(deviceId) {
150
+ try {
151
+ const stream = await navigator.mediaDevices.getUserMedia({
152
+ audio: deviceId ? { deviceId: { ideal: deviceId } } : true
153
+ });
154
+ for (const track of stream.getTracks())
155
+ track.stop();
156
+ } catch (error) {
157
+ throw new MediaAccessError(error, "microphone");
158
+ }
159
+ }
160
+
161
+ // src/core/policy-curfew-error.ts
162
+ class PolicyCurfewError extends Error {
163
+ retryAfterSeconds;
164
+ constructor(retryAfterSeconds) {
165
+ super("new sessions are paused after repeated policy violations");
166
+ this.name = "PolicyCurfewError";
167
+ this.retryAfterSeconds = retryAfterSeconds;
168
+ }
169
+ }
170
+
171
+ // src/core/ticker.ts
172
+ var WORKER_SOURCE = "onmessage = (e) => { setInterval(() => postMessage(0), e.data); };";
173
+ function backgroundTicker(host) {
174
+ return (tick, intervalMs) => {
175
+ const stopTimer = workerInterval(host.Worker, tick, intervalMs) ?? pageInterval(tick, intervalMs);
176
+ const page = host.document;
177
+ const onVisibility = () => {
178
+ if (page?.visibilityState === "visible")
179
+ tick();
180
+ };
181
+ page?.addEventListener("visibilitychange", onVisibility);
182
+ return () => {
183
+ stopTimer();
184
+ page?.removeEventListener("visibilitychange", onVisibility);
185
+ };
186
+ };
187
+ }
188
+ function workerInterval(WorkerType, tick, intervalMs) {
189
+ if (WorkerType === undefined || typeof URL.createObjectURL !== "function") {
190
+ return null;
191
+ }
192
+ const url = URL.createObjectURL(new Blob([WORKER_SOURCE], { type: "text/javascript" }));
193
+ let worker;
194
+ try {
195
+ worker = new WorkerType(url);
196
+ } catch {
197
+ URL.revokeObjectURL(url);
198
+ return null;
199
+ }
200
+ let fallback = null;
201
+ worker.onmessage = () => tick();
202
+ worker.onerror = () => {
203
+ worker.terminate();
204
+ fallback ??= pageInterval(tick, intervalMs);
205
+ };
206
+ worker.postMessage(intervalMs);
207
+ return () => {
208
+ worker.terminate();
209
+ fallback?.();
210
+ URL.revokeObjectURL(url);
211
+ };
212
+ }
213
+ function pageInterval(tick, intervalMs) {
214
+ const id = setInterval(tick, intervalMs);
215
+ return () => clearInterval(id);
216
+ }
217
+ var pageTicker = backgroundTicker(globalThis);
218
+
219
+ // src/providers/livekit/conversation.ts
220
+ import {
221
+ ConnectionQuality,
222
+ ConnectionState as ConnectionState2,
223
+ Room,
224
+ RoomEvent as RoomEvent3
225
+ } from "livekit-client";
226
+
227
+ // src/providers/livekit/tracks.ts
228
+ class LiveKitVideoTrack {
229
+ track;
230
+ attachedElement = null;
231
+ constructor(track) {
232
+ this.track = track;
233
+ }
234
+ attach(element) {
235
+ this.track.attach(element);
236
+ this.attachedElement = element;
237
+ }
238
+ detach() {
239
+ if (this.attachedElement) {
240
+ this.track.detach(this.attachedElement);
241
+ this.attachedElement = null;
242
+ }
243
+ }
244
+ get isEnabled() {
245
+ return !this.track.isMuted;
246
+ }
247
+ get isPlaying() {
248
+ const element = this.attachedElement;
249
+ return element !== null && !element.paused && element.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA;
250
+ }
251
+ }
252
+
253
+ class LiveKitAudioTrack {
254
+ track;
255
+ get mediaStreamTrack() {
256
+ return this.track.mediaStreamTrack ?? null;
257
+ }
258
+ attachedElement = null;
259
+ muted = false;
260
+ constructor(track) {
261
+ this.track = track;
262
+ }
263
+ attach(element) {
264
+ this.track.attach(element);
265
+ element.muted = this.muted;
266
+ this.attachedElement = element;
267
+ }
268
+ detach() {
269
+ if (this.attachedElement) {
270
+ this.track.detach(this.attachedElement);
271
+ this.attachedElement = null;
272
+ }
273
+ }
274
+ setMuted(muted) {
275
+ this.muted = muted;
276
+ if (this.attachedElement) {
277
+ this.attachedElement.muted = muted;
278
+ }
279
+ }
280
+ get isEnabled() {
281
+ return !this.track.isMuted;
282
+ }
283
+ }
284
+
285
+ // src/providers/livekit/avatar-participant.ts
286
+ class LiveKitAvatarParticipant {
287
+ participant;
288
+ locallyMuted = false;
289
+ _ready = false;
290
+ _videoTrack = null;
291
+ _audioTrack = null;
292
+ lastVideoSource = null;
293
+ lastAudioSource = null;
294
+ constructor(participant) {
295
+ this.participant = participant;
296
+ }
297
+ updateParticipant(participant, keepReady = false) {
298
+ if (!keepReady && (participant === null || this.participant !== null && participant !== this.participant)) {
299
+ this._ready = false;
300
+ }
301
+ this.participant = participant;
302
+ return this.updateTracks();
303
+ }
304
+ markReady() {
305
+ this._ready = true;
306
+ this.applyMute();
307
+ }
308
+ applyMute() {
309
+ this._audioTrack?.setMuted(this.locallyMuted || !this._ready);
310
+ }
311
+ updateTracks() {
312
+ let changed = false;
313
+ const videoSource = this.findTrack("video");
314
+ if (videoSource !== this.lastVideoSource) {
315
+ this._videoTrack?.detach();
316
+ this._videoTrack = videoSource ? new LiveKitVideoTrack(videoSource) : null;
317
+ this.lastVideoSource = videoSource;
318
+ changed = true;
319
+ }
320
+ const audioSource = this.findTrack("audio");
321
+ if (audioSource !== this.lastAudioSource) {
322
+ this._audioTrack?.detach();
323
+ this._audioTrack = audioSource ? new LiveKitAudioTrack(audioSource) : null;
324
+ this.applyMute();
325
+ this.lastAudioSource = audioSource;
326
+ changed = true;
327
+ }
328
+ return changed;
329
+ }
330
+ get videoTrack() {
331
+ return this._videoTrack;
332
+ }
333
+ get audioTrack() {
334
+ return this._audioTrack;
335
+ }
336
+ async muteAudio() {
337
+ this.locallyMuted = true;
338
+ this.applyMute();
339
+ }
340
+ async unmuteAudio() {
341
+ this.locallyMuted = false;
342
+ this.applyMute();
343
+ }
344
+ get isMuted() {
345
+ return this.locallyMuted;
346
+ }
347
+ get isVideoPlaying() {
348
+ return this._videoTrack?.isPlaying ?? false;
349
+ }
350
+ get isVideoEnabled() {
351
+ return this._videoTrack !== null;
352
+ }
353
+ get isReady() {
354
+ return this._ready;
355
+ }
356
+ findTrack(kind) {
357
+ if (!this.participant)
358
+ return null;
359
+ for (const pub of this.participant.trackPublications.values()) {
360
+ if (pub.track?.kind === kind)
361
+ return pub.track;
362
+ }
363
+ return null;
364
+ }
365
+ }
366
+
367
+ // src/providers/livekit/layer-fallback.ts
368
+ import { Track, VideoQuality } from "livekit-client";
369
+ var LAYER_FALLBACK_TIMING = {
370
+ sampleMs: 1000,
371
+ slowFps: 12,
372
+ stuckSamples: 3,
373
+ holdMs: 30000,
374
+ maxHoldMs: 120000
375
+ };
376
+
377
+ class LayerFallback {
378
+ read;
379
+ pin;
380
+ timing;
381
+ timer = null;
382
+ previous = null;
383
+ slow = 0;
384
+ pinnedUntil = null;
385
+ hold;
386
+ sampling = false;
387
+ constructor(read, pin, timing = LAYER_FALLBACK_TIMING) {
388
+ this.read = read;
389
+ this.pin = pin;
390
+ this.timing = timing;
391
+ this.hold = timing.holdMs;
392
+ }
393
+ start() {
394
+ if (this.timer !== null)
395
+ return;
396
+ this.timer = setInterval(() => {
397
+ this.sample();
398
+ }, this.timing.sampleMs);
399
+ }
400
+ stop() {
401
+ if (this.timer !== null) {
402
+ clearInterval(this.timer);
403
+ this.timer = null;
404
+ }
405
+ this.previous = null;
406
+ this.slow = 0;
407
+ }
408
+ async sample() {
409
+ if (this.sampling || this.timer === null)
410
+ return;
411
+ this.sampling = true;
412
+ try {
413
+ const now = Date.now();
414
+ if (this.pinnedUntil !== null && now >= this.pinnedUntil) {
415
+ this.pinnedUntil = null;
416
+ this.pin("any");
417
+ this.previous = null;
418
+ }
419
+ const rate = await this.read().catch(() => null);
420
+ if (this.timer === null)
421
+ return;
422
+ this.observe(rate, now);
423
+ } finally {
424
+ this.sampling = false;
425
+ }
426
+ }
427
+ observe(rate, now) {
428
+ const previous = this.previous;
429
+ this.previous = rate;
430
+ if (this.pinnedUntil !== null || rate === null || previous === null)
431
+ return;
432
+ if (rate.framesDecoded === null || previous.framesDecoded === null)
433
+ return;
434
+ const seconds = (rate.at - previous.at) / 1000;
435
+ if (seconds <= 0)
436
+ return;
437
+ const fps = (rate.framesDecoded - previous.framesDecoded) / seconds;
438
+ const full = rate.width !== null && rate.publishedWidth !== null && rate.width >= rate.publishedWidth;
439
+ this.slow = full && fps < this.timing.slowFps ? this.slow + 1 : 0;
440
+ if (this.slow < this.timing.stuckSamples)
441
+ return;
442
+ this.slow = 0;
443
+ this.pinnedUntil = now + this.hold;
444
+ this.hold = Math.min(this.hold * 2, this.timing.maxHoldMs);
445
+ this.pin("low");
446
+ }
447
+ }
448
+ async function avatarVideoRate(room) {
449
+ if (typeof document !== "undefined" && document.hidden)
450
+ return null;
451
+ for (const participant of room.remoteParticipants.values()) {
452
+ const publication = participant.getTrackPublication(Track.Source.Camera);
453
+ const track = publication?.track;
454
+ if (!publication?.isSubscribed || !track)
455
+ continue;
456
+ const report = await track.getRTCStatsReport();
457
+ let width = null;
458
+ let framesDecoded = null;
459
+ report?.forEach((stat) => {
460
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
461
+ width = typeof stat.frameWidth === "number" ? stat.frameWidth : null;
462
+ framesDecoded = typeof stat.framesDecoded === "number" ? stat.framesDecoded : null;
463
+ }
464
+ });
465
+ return {
466
+ width,
467
+ publishedWidth: publication.dimensions?.width ?? null,
468
+ framesDecoded,
469
+ at: Date.now()
470
+ };
471
+ }
472
+ return null;
473
+ }
474
+ function pinAvatarLayer(room, layer) {
475
+ for (const participant of room.remoteParticipants.values()) {
476
+ const publication = participant.getTrackPublication(Track.Source.Camera);
477
+ if (!publication?.isSubscribed)
478
+ continue;
479
+ publication.setVideoQuality(layer === "low" ? VideoQuality.LOW : VideoQuality.HIGH);
480
+ return;
481
+ }
482
+ }
483
+
484
+ // src/providers/livekit/network-stats.ts
485
+ import {
486
+ ConnectionState,
487
+ ParticipantEvent,
488
+ RoomEvent,
489
+ Track as Track2
490
+ } from "livekit-client";
491
+
492
+ // src/providers/livekit/mic-silence.ts
493
+ var MIC_SILENT_AFTER_MS = 4000;
494
+
495
+ class MicSilence {
496
+ silentAfterMs;
497
+ packets = null;
498
+ countedAt = null;
499
+ stillMs = 0;
500
+ stalled = false;
501
+ downSince = null;
502
+ _silent = false;
503
+ constructor(silentAfterMs = MIC_SILENT_AFTER_MS) {
504
+ this.silentAfterMs = silentAfterMs;
505
+ }
506
+ get silent() {
507
+ return this._silent;
508
+ }
509
+ observe(reading) {
510
+ if (!reading.connected || !reading.live) {
511
+ this.reset();
512
+ return this._silent;
513
+ }
514
+ const { at } = reading;
515
+ const packets = reading.packetsSent;
516
+ if (packets === null) {
517
+ this.countedAt = null;
518
+ } else {
519
+ if (packets !== this.packets)
520
+ this.stillMs = 0;
521
+ else if (this.countedAt !== null)
522
+ this.stillMs += at - this.countedAt;
523
+ this.packets = packets;
524
+ this.countedAt = at;
525
+ this.stalled = this.stillMs >= this.silentAfterMs;
526
+ }
527
+ const down = reading.readyState === "ended" || reading.trackMuted;
528
+ if (!down)
529
+ this.downSince = null;
530
+ else
531
+ this.downSince ??= at;
532
+ const trackDown = this.downSince !== null && at - this.downSince >= this.silentAfterMs;
533
+ this._silent = this.stalled || trackDown;
534
+ return this._silent;
535
+ }
536
+ reset() {
537
+ this.packets = null;
538
+ this.countedAt = null;
539
+ this.stillMs = 0;
540
+ this.stalled = false;
541
+ this.downSince = null;
542
+ this._silent = false;
543
+ }
544
+ }
545
+
546
+ // src/providers/livekit/network-stats.ts
547
+ var STATS_TOPIC = "webrtc-stats";
548
+ var SAMPLE_MS = 1000;
549
+ var BATCH = 5;
550
+ function num(value) {
551
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
552
+ }
553
+ function str(value) {
554
+ return typeof value === "string" && value ? value : null;
555
+ }
556
+ function entries(report) {
557
+ const out = [];
558
+ report.forEach((entry) => {
559
+ out.push(entry);
560
+ });
561
+ return out;
562
+ }
563
+ function codecOf(entry, byId) {
564
+ const codec = entry.codecId ? byId.get(entry.codecId) : undefined;
565
+ const mime = codec ? str(codec.mimeType) : null;
566
+ return mime ? mime.replace(/^(audio|video)\//, "") : null;
567
+ }
568
+ function digestInbound(entry, byId) {
569
+ if (!entry)
570
+ return null;
571
+ const digest = {
572
+ packets_received: num(entry.packetsReceived),
573
+ packets_lost: num(entry.packetsLost),
574
+ bytes_received: num(entry.bytesReceived),
575
+ jitter_s: num(entry.jitter),
576
+ nack_count: num(entry.nackCount),
577
+ jitter_buffer_delay_s: num(entry.jitterBufferDelay),
578
+ jitter_buffer_target_delay_s: num(entry.jitterBufferTargetDelay),
579
+ jitter_buffer_emitted_count: num(entry.jitterBufferEmittedCount),
580
+ estimated_playout_timestamp_ms: num(entry.estimatedPlayoutTimestamp),
581
+ codec: codecOf(entry, byId)
582
+ };
583
+ if (entry.kind === "audio") {
584
+ digest.total_samples_received = num(entry.totalSamplesReceived);
585
+ digest.concealed_samples = num(entry.concealedSamples);
586
+ digest.silent_concealed_samples = num(entry.silentConcealedSamples);
587
+ digest.concealment_events = num(entry.concealmentEvents);
588
+ digest.inserted_samples_for_deceleration = num(entry.insertedSamplesForDeceleration);
589
+ digest.removed_samples_for_acceleration = num(entry.removedSamplesForAcceleration);
590
+ digest.audio_level = num(entry.audioLevel);
591
+ digest.total_samples_duration_s = num(entry.totalSamplesDuration);
592
+ } else {
593
+ digest.pli_count = num(entry.pliCount);
594
+ digest.fir_count = num(entry.firCount);
595
+ digest.frames_received = num(entry.framesReceived);
596
+ digest.frames_decoded = num(entry.framesDecoded);
597
+ digest.frames_dropped = num(entry.framesDropped);
598
+ digest.frames_per_second = num(entry.framesPerSecond);
599
+ digest.frame_width = num(entry.frameWidth);
600
+ digest.frame_height = num(entry.frameHeight);
601
+ digest.freeze_count = num(entry.freezeCount);
602
+ digest.total_freeze_duration_s = num(entry.totalFreezesDuration);
603
+ digest.pause_count = num(entry.pauseCount);
604
+ digest.total_pause_duration_s = num(entry.totalPausesDuration);
605
+ digest.total_inter_frame_delay_s = num(entry.totalInterFrameDelay);
606
+ digest.total_decode_time_s = num(entry.totalDecodeTime);
607
+ digest.decoder = str(entry.decoderImplementation);
608
+ }
609
+ return digest;
610
+ }
611
+ function digestOutbound(entry, byId) {
612
+ if (!entry)
613
+ return null;
614
+ const remote = entry.remoteId ? byId.get(entry.remoteId) : undefined;
615
+ const digest = {
616
+ packets_sent: num(entry.packetsSent),
617
+ bytes_sent: num(entry.bytesSent),
618
+ retransmitted_packets_sent: num(entry.retransmittedPacketsSent),
619
+ nack_count: num(entry.nackCount),
620
+ target_bitrate: num(entry.targetBitrate),
621
+ remote_rtt_s: remote ? num(remote.roundTripTime) : null,
622
+ remote_fraction_lost: remote ? num(remote.fractionLost) : null,
623
+ remote_jitter_s: remote ? num(remote.jitter) : null,
624
+ remote_packets_lost: remote ? num(remote.packetsLost) : null,
625
+ codec: codecOf(entry, byId)
626
+ };
627
+ if (entry.kind === "video") {
628
+ digest.pli_count = num(entry.pliCount);
629
+ digest.fir_count = num(entry.firCount);
630
+ digest.frames_sent = num(entry.framesSent);
631
+ digest.frames_encoded = num(entry.framesEncoded);
632
+ digest.frames_per_second = num(entry.framesPerSecond);
633
+ digest.frame_width = num(entry.frameWidth);
634
+ digest.frame_height = num(entry.frameHeight);
635
+ digest.total_encode_time_s = num(entry.totalEncodeTime);
636
+ digest.quality_limitation_reason = str(entry.qualityLimitationReason);
637
+ const durations = entry.qualityLimitationDurations;
638
+ digest.quality_limitation_durations_s = durations && typeof durations === "object" ? durations : null;
639
+ digest.encoder = str(entry.encoderImplementation);
640
+ }
641
+ return digest;
642
+ }
643
+ var NO_LINK = {
644
+ rtt_s: null,
645
+ candidate_type: null,
646
+ protocol: null,
647
+ avail_out_bps: null,
648
+ avail_in_bps: null
649
+ };
650
+ function digestLink(all, byId) {
651
+ let pair;
652
+ for (const transport of all) {
653
+ if (transport.type !== "transport")
654
+ continue;
655
+ const id = str(transport.selectedCandidatePairId);
656
+ if (id) {
657
+ pair = byId.get(id);
658
+ if (pair)
659
+ break;
660
+ }
661
+ }
662
+ if (!pair) {
663
+ pair = all.find((entry) => entry.type === "candidate-pair" && entry.nominated === true && entry.state === "succeeded");
664
+ }
665
+ if (!pair)
666
+ return NO_LINK;
667
+ const localId = str(pair.localCandidateId);
668
+ const local = localId ? byId.get(localId) : undefined;
669
+ return {
670
+ rtt_s: num(pair.currentRoundTripTime),
671
+ candidate_type: local ? str(local.candidateType) : null,
672
+ protocol: local ? str(local.protocol) : null,
673
+ avail_out_bps: num(pair.availableOutgoingBitrate),
674
+ avail_in_bps: num(pair.availableIncomingBitrate)
675
+ };
676
+ }
677
+ function buildSample(reports, hidden) {
678
+ const perReport = [
679
+ reports.avatarAudio,
680
+ reports.avatarVideo,
681
+ reports.mic,
682
+ reports.camera
683
+ ].map((report) => report ? entries(report) : null);
684
+ const byId = new Map;
685
+ for (const list of perReport) {
686
+ if (list)
687
+ for (const entry of list)
688
+ byId.set(entry.id, entry);
689
+ }
690
+ let link = NO_LINK;
691
+ for (const list of perReport) {
692
+ if (!list)
693
+ continue;
694
+ const candidate = digestLink(list, byId);
695
+ if (candidate.rtt_s != null || candidate.candidate_type) {
696
+ link = candidate;
697
+ break;
698
+ }
699
+ }
700
+ const inboundOf = (list) => list ? digestInbound(list.find((entry) => entry.type === "inbound-rtp"), byId) : null;
701
+ const outboundOf = (list) => list ? digestOutbound(list.find((entry) => entry.type === "outbound-rtp" && entry.active !== false), byId) : null;
702
+ const avatarAudio = inboundOf(perReport[0]);
703
+ const avatarVideo = inboundOf(perReport[1]);
704
+ const mic = outboundOf(perReport[2]);
705
+ if (mic && reports.micTrack) {
706
+ mic.track_muted = reports.micTrack.muted;
707
+ mic.track_ready_state = reports.micTrack.readyState;
708
+ mic.track_enabled = reports.micTrack.enabled;
709
+ }
710
+ const audioPlayout = avatarAudio?.estimated_playout_timestamp_ms ?? null;
711
+ const videoPlayout = avatarVideo?.estimated_playout_timestamp_ms ?? null;
712
+ return {
713
+ ts: Date.now() / 1000,
714
+ hidden,
715
+ ...link,
716
+ av_offset_ms: audioPlayout != null && videoPlayout != null ? audioPlayout - videoPlayout : null,
717
+ avatar_audio: avatarAudio,
718
+ avatar_video: avatarVideo,
719
+ mic,
720
+ camera: outboundOf(perReport[3])
721
+ };
722
+ }
723
+ function environment() {
724
+ if (typeof navigator === "undefined")
725
+ return null;
726
+ const connection = navigator.connection;
727
+ return {
728
+ user_agent: str(navigator.userAgent),
729
+ platform: str(navigator.platform),
730
+ hardware_concurrency: num(navigator.hardwareConcurrency),
731
+ connection_type: connection ? str(connection.effectiveType) ?? str(connection.type) : null,
732
+ connection_downlink_mbps: connection ? num(connection.downlink) : null,
733
+ connection_rtt_ms: connection ? num(connection.rtt) : null,
734
+ viewport: typeof window !== "undefined" && window.innerWidth ? `${window.innerWidth}x${window.innerHeight}` : null
735
+ };
736
+ }
737
+ function hasStats(track) {
738
+ return typeof track === "object" && track !== null && typeof track.getRTCStatsReport === "function";
739
+ }
740
+
741
+ class NetworkStatsReporter {
742
+ room;
743
+ env;
744
+ onMicSilent;
745
+ micSilence = new MicSilence;
746
+ timer = null;
747
+ seq = 0;
748
+ pending = [];
749
+ stopped = false;
750
+ onVisibility = () => {
751
+ if (document.visibilityState === "hidden")
752
+ this.flush();
753
+ };
754
+ onPageHide = () => {
755
+ this.flush();
756
+ };
757
+ onLinkChange = () => {
758
+ this.resetMic();
759
+ };
760
+ onMicMute = (publication) => {
761
+ if (publication.source === Track2.Source.Microphone)
762
+ this.resetMic();
763
+ };
764
+ constructor(room, onMicSilent = () => {}) {
765
+ this.room = room;
766
+ this.env = environment();
767
+ this.onMicSilent = onMicSilent;
768
+ }
769
+ start() {
770
+ if (this.timer !== null || this.stopped)
771
+ return;
772
+ this.timer = setInterval(() => {
773
+ this.sample();
774
+ }, SAMPLE_MS);
775
+ document.addEventListener("visibilitychange", this.onVisibility);
776
+ window.addEventListener("pagehide", this.onPageHide);
777
+ this.room.on(RoomEvent.Reconnecting, this.onLinkChange).on(RoomEvent.Reconnected, this.onLinkChange).on(RoomEvent.Disconnected, this.onLinkChange);
778
+ this.room.localParticipant.on(ParticipantEvent.TrackMuted, this.onMicMute).on(ParticipantEvent.TrackUnmuted, this.onMicMute);
779
+ }
780
+ stop() {
781
+ if (!this.teardown())
782
+ return Promise.resolve();
783
+ return this.flush();
784
+ }
785
+ abandon() {
786
+ this.teardown();
787
+ this.pending = [];
788
+ }
789
+ teardown() {
790
+ if (this.stopped)
791
+ return false;
792
+ this.stopped = true;
793
+ if (this.timer !== null) {
794
+ clearInterval(this.timer);
795
+ this.timer = null;
796
+ }
797
+ document.removeEventListener("visibilitychange", this.onVisibility);
798
+ window.removeEventListener("pagehide", this.onPageHide);
799
+ this.room.off(RoomEvent.Reconnecting, this.onLinkChange).off(RoomEvent.Reconnected, this.onLinkChange).off(RoomEvent.Disconnected, this.onLinkChange);
800
+ this.room.localParticipant.off(ParticipantEvent.TrackMuted, this.onMicMute).off(ParticipantEvent.TrackUnmuted, this.onMicMute);
801
+ this.resetMic();
802
+ return true;
803
+ }
804
+ resetMic() {
805
+ const was = this.micSilence.silent;
806
+ this.micSilence.reset();
807
+ if (was)
808
+ this.onMicSilent(false);
809
+ }
810
+ remoteTrack(source) {
811
+ for (const participant of this.room.remoteParticipants.values()) {
812
+ const publication = participant.getTrackPublication(source);
813
+ if (publication?.isSubscribed && hasStats(publication.track)) {
814
+ return publication.track;
815
+ }
816
+ }
817
+ return null;
818
+ }
819
+ localTrack(source) {
820
+ const publication = this.room.localParticipant.getTrackPublication(source);
821
+ return publication && !publication.isMuted && hasStats(publication.track) ? publication.track : null;
822
+ }
823
+ async report(track) {
824
+ if (!track)
825
+ return null;
826
+ try {
827
+ return await track.getRTCStatsReport() ?? null;
828
+ } catch {
829
+ return null;
830
+ }
831
+ }
832
+ async sample() {
833
+ if (this.stopped)
834
+ return;
835
+ try {
836
+ const [avatarAudio, avatarVideo, mic, camera] = await Promise.all([
837
+ this.report(this.remoteTrack(Track2.Source.Microphone)),
838
+ this.report(this.remoteTrack(Track2.Source.Camera)),
839
+ this.report(this.localTrack(Track2.Source.Microphone)),
840
+ this.report(this.localTrack(Track2.Source.Camera))
841
+ ]);
842
+ if (this.stopped)
843
+ return;
844
+ const micPublication = this.room.localParticipant.getTrackPublication(Track2.Source.Microphone);
845
+ const micTrack = micPublication?.track?.mediaStreamTrack ?? null;
846
+ const sample = avatarAudio || avatarVideo || mic || camera ? buildSample({ avatarAudio, avatarVideo, mic, camera, micTrack }, document.hidden) : null;
847
+ this.watchMic(!!micPublication && !micPublication.isMuted && micTrack !== null, micTrack, mic ? sample?.mic?.packets_sent ?? 0 : null);
848
+ if (!sample)
849
+ return;
850
+ this.pending.push(sample);
851
+ if (this.pending.length >= BATCH)
852
+ this.flush();
853
+ } catch {}
854
+ }
855
+ watchMic(live, track, packetsSent) {
856
+ const was = this.micSilence.silent;
857
+ const silent = this.micSilence.observe({
858
+ at: Date.now(),
859
+ connected: this.room.state === ConnectionState.Connected,
860
+ live,
861
+ packetsSent,
862
+ readyState: track?.readyState ?? null,
863
+ trackMuted: track?.muted ?? false
864
+ });
865
+ if (silent !== was)
866
+ this.onMicSilent(silent);
867
+ }
868
+ async flush() {
869
+ if (!this.pending.length)
870
+ return;
871
+ const packet = {
872
+ type: "webrtc_stats",
873
+ seq: this.seq++,
874
+ ts: Date.now() / 1000,
875
+ env: this.env,
876
+ samples: this.pending
877
+ };
878
+ this.pending = [];
879
+ try {
880
+ await this.room.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(packet)), { reliable: true, topic: STATS_TOPIC });
881
+ } catch {}
882
+ }
883
+ }
884
+
885
+ // src/providers/livekit/presentation.ts
886
+ import { Track as Track3 } from "livekit-client";
887
+ var PRESENTATION_TIMING = {
888
+ sampleMs: 250,
889
+ settleMs: 3000,
890
+ steadySamples: 4,
891
+ maxWaitMs: 12000
892
+ };
893
+
894
+ class PresentationWatch {
895
+ read;
896
+ onPresented;
897
+ timing;
898
+ timer = null;
899
+ startedAt = 0;
900
+ steady = 0;
901
+ sampling = false;
902
+ constructor(read, onPresented, timing = PRESENTATION_TIMING) {
903
+ this.read = read;
904
+ this.onPresented = onPresented;
905
+ this.timing = timing;
906
+ }
907
+ start() {
908
+ if (this.timer !== null)
909
+ return;
910
+ this.startedAt = Date.now();
911
+ this.timer = setInterval(() => {
912
+ this.sample();
913
+ }, this.timing.sampleMs);
914
+ }
915
+ stop() {
916
+ if (this.timer !== null) {
917
+ clearInterval(this.timer);
918
+ this.timer = null;
919
+ }
920
+ }
921
+ async sample() {
922
+ if (this.sampling || this.timer === null)
923
+ return;
924
+ this.sampling = true;
925
+ try {
926
+ const size = await this.read().catch(() => ({ width: null, publishedWidth: null }));
927
+ if (this.timer === null)
928
+ return;
929
+ const full = size.width !== null && size.publishedWidth !== null && size.width >= size.publishedWidth;
930
+ this.steady = full ? this.steady + 1 : 0;
931
+ const elapsed = Date.now() - this.startedAt;
932
+ if (elapsed >= this.timing.settleMs && this.steady >= this.timing.steadySamples) {
933
+ this.finish("full-size");
934
+ } else if (elapsed >= this.timing.maxWaitMs) {
935
+ this.finish("timed-out");
936
+ }
937
+ } finally {
938
+ this.sampling = false;
939
+ }
940
+ }
941
+ finish(by) {
942
+ this.stop();
943
+ this.onPresented(by);
944
+ }
945
+ }
946
+ async function avatarVideoSize(room) {
947
+ for (const participant of room.remoteParticipants.values()) {
948
+ const publication = participant.getTrackPublication(Track3.Source.Camera);
949
+ const track = publication?.track;
950
+ if (!publication?.isSubscribed || !track)
951
+ continue;
952
+ const publishedWidth = publication.dimensions?.width ?? null;
953
+ const report = await track.getRTCStatsReport();
954
+ let width = null;
955
+ report?.forEach((stat) => {
956
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
957
+ width = typeof stat.frameWidth === "number" ? stat.frameWidth : null;
958
+ }
959
+ });
960
+ return { width, publishedWidth };
961
+ }
962
+ return { width: null, publishedWidth: null };
963
+ }
964
+
965
+ // src/providers/livekit/ready-fallback.ts
966
+ import { Track as Track4 } from "livekit-client";
967
+ var READY_FALLBACK_TIMING = {
968
+ sampleMs: 500,
969
+ delayMs: 5000,
970
+ maxWaitMs: 12000
971
+ };
972
+
973
+ class ReadyFallback {
974
+ read;
975
+ elementPlaying;
976
+ onFallback;
977
+ timing;
978
+ timer = null;
979
+ startedAt = 0;
980
+ lastFrames = null;
981
+ sampling = false;
982
+ constructor(read, elementPlaying, onFallback, timing = READY_FALLBACK_TIMING) {
983
+ this.read = read;
984
+ this.elementPlaying = elementPlaying;
985
+ this.onFallback = onFallback;
986
+ this.timing = timing;
987
+ }
988
+ start() {
989
+ if (this.timer !== null)
990
+ return;
991
+ this.startedAt = Date.now();
992
+ this.lastFrames = null;
993
+ this.timer = setInterval(() => {
994
+ this.sample();
995
+ }, this.timing.sampleMs);
996
+ }
997
+ get running() {
998
+ return this.timer !== null;
999
+ }
1000
+ stop() {
1001
+ if (this.timer !== null) {
1002
+ clearInterval(this.timer);
1003
+ this.timer = null;
1004
+ }
1005
+ }
1006
+ async sample() {
1007
+ if (this.sampling || this.timer === null)
1008
+ return;
1009
+ this.sampling = true;
1010
+ try {
1011
+ const frames = await this.read().catch(() => null);
1012
+ if (this.timer === null)
1013
+ return;
1014
+ const playing = frames !== null && this.lastFrames !== null && frames > this.lastFrames;
1015
+ this.lastFrames = frames;
1016
+ const elapsed = Date.now() - this.startedAt;
1017
+ const unmeasured = frames === null && elapsed >= this.timing.maxWaitMs && this.elementPlaying();
1018
+ if (playing && elapsed >= this.timing.delayMs || unmeasured) {
1019
+ this.stop();
1020
+ this.onFallback();
1021
+ }
1022
+ } finally {
1023
+ this.sampling = false;
1024
+ }
1025
+ }
1026
+ }
1027
+ async function avatarFramesDecoded(room) {
1028
+ for (const participant of room.remoteParticipants.values()) {
1029
+ const publication = participant.getTrackPublication(Track4.Source.Camera);
1030
+ const track = publication?.track;
1031
+ if (!publication?.isSubscribed || !track)
1032
+ continue;
1033
+ const report = await track.getRTCStatsReport();
1034
+ let frames = null;
1035
+ report?.forEach((stat) => {
1036
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
1037
+ frames = typeof stat.framesDecoded === "number" ? stat.framesDecoded : null;
1038
+ }
1039
+ });
1040
+ return frames;
1041
+ }
1042
+ return null;
1043
+ }
1044
+
1045
+ // src/providers/livekit/signals.ts
1046
+ function decode(payload) {
1047
+ try {
1048
+ const decoded = JSON.parse(new TextDecoder().decode(payload));
1049
+ return isRecord(decoded) ? decoded : null;
1050
+ } catch {
1051
+ return null;
1052
+ }
1053
+ }
1054
+ function endReason(reason) {
1055
+ if (reason === "policy_violation")
1056
+ return "policy-violation";
1057
+ if (reason === "time_limit")
1058
+ return "time-limit";
1059
+ return "ended";
1060
+ }
1061
+ function parseSessionEvent(payload) {
1062
+ const decoded = decode(payload);
1063
+ if (!decoded)
1064
+ return null;
1065
+ if (decoded.type === "session.output_ready") {
1066
+ return { type: "output-ready" };
1067
+ }
1068
+ if (decoded.type === "command.result") {
1069
+ return {
1070
+ type: "command-result",
1071
+ id: typeof decoded.id === "string" ? decoded.id : null,
1072
+ result: {
1073
+ status: decoded.status === "applied" ? "applied" : "rejected",
1074
+ error: typeof decoded.error === "string" ? decoded.error : null
1075
+ }
1076
+ };
1077
+ }
1078
+ if (decoded.type === "session.status" && decoded.status === "ended") {
1079
+ return {
1080
+ type: "ended",
1081
+ reason: endReason(decoded.reason),
1082
+ byClient: decoded.reason === "client_ended"
1083
+ };
1084
+ }
1085
+ if (decoded.type === "error" && decoded.fatal === true) {
1086
+ return { type: "ended", reason: endReason(decoded.code), byClient: false };
1087
+ }
1088
+ if (decoded.type === "error" && decoded.code === "bad_command") {
1089
+ return { type: "command-unreadable" };
1090
+ }
1091
+ return { type: "other" };
1092
+ }
1093
+ function parseRuntimeSignal(payload) {
1094
+ const decoded = decode(payload);
1095
+ if (decoded?.type === "output_ready") {
1096
+ return { type: "output-ready" };
1097
+ }
1098
+ if (decoded?.type === "session_ended") {
1099
+ return { type: "session-ended", reason: endReason(decoded.reason) };
1100
+ }
1101
+ if (decoded?.type === "link_poor") {
1102
+ return { type: "link-poor" };
1103
+ }
1104
+ return null;
1105
+ }
1106
+ function clockSyncPong(payload, receivedAt, now) {
1107
+ const decoded = decode(payload);
1108
+ if (decoded?.type !== "clock_sync_ping")
1109
+ return null;
1110
+ const reply = {
1111
+ type: "clock_sync_pong",
1112
+ seq: decoded.seq,
1113
+ t0: decoded.t0,
1114
+ t1: receivedAt,
1115
+ t2: now()
1116
+ };
1117
+ return new TextEncoder().encode(JSON.stringify(reply));
1118
+ }
1119
+ function parseDataEvent(payload) {
1120
+ const decoded = decode(payload);
1121
+ if (!decoded)
1122
+ return null;
1123
+ return {
1124
+ type: "data.event",
1125
+ version: 1,
1126
+ category: typeof decoded.category === "string" ? decoded.category : "",
1127
+ source: typeof decoded.source === "string" ? decoded.source : "",
1128
+ kind: typeof decoded.kind === "string" ? decoded.kind : typeof decoded.type === "string" ? decoded.type : "",
1129
+ session_id: typeof decoded.session_id === "string" ? decoded.session_id : null,
1130
+ request_id: decoded.request_id != null ? decoded.request_id : null,
1131
+ timestamp_s: typeof decoded.timestamp_s === "number" ? decoded.timestamp_s : null,
1132
+ severity: "info",
1133
+ payload: decoded
1134
+ };
1135
+ }
1136
+
1137
+ // src/providers/livekit/user-participant.ts
1138
+ import {
1139
+ AudioPresets,
1140
+ ParticipantEvent as ParticipantEvent2,
1141
+ RoomEvent as RoomEvent2,
1142
+ Track as Track5
1143
+ } from "livekit-client";
1144
+
1145
+ // src/core/camera.ts
1146
+ function cameraFacing(track) {
1147
+ const facing = track.getSettings().facingMode;
1148
+ return facing === "user" || facing === "environment" ? facing : null;
1149
+ }
1150
+ function requireCameraFacing(track, facing) {
1151
+ const requested = typeof facing === "string" ? facing : !Array.isArray(facing) ? facing?.exact : undefined;
1152
+ if (typeof requested !== "string")
1153
+ return;
1154
+ if (!track || track.getSettings().facingMode !== requested)
1155
+ throw new DOMException("Requested camera direction is unavailable", "OverconstrainedError");
1156
+ }
1157
+
1158
+ // src/providers/livekit/user-participant.ts
1159
+ function microphonePublishOptions(resilient) {
1160
+ if (!resilient)
1161
+ return;
1162
+ return { dtx: false, audioPreset: AudioPresets.speech };
1163
+ }
1164
+
1165
+ class LiveKitUserParticipant {
1166
+ room;
1167
+ onTracksChanged;
1168
+ onSpeakingChanged;
1169
+ onMicrophoneChanged;
1170
+ onCameraChanged;
1171
+ inputs = {
1172
+ audioinput: { changing: false, unavailable: false },
1173
+ videoinput: { changing: false, unavailable: false }
1174
+ };
1175
+ disposed = false;
1176
+ selectedCameraFacing = null;
1177
+ initialCameraFacing;
1178
+ microphoneOptions;
1179
+ _videoTrack = null;
1180
+ _audioTrack = null;
1181
+ lastVideoSource = null;
1182
+ lastAudioSource = null;
1183
+ _isSpeaking = false;
1184
+ speakingHandler = null;
1185
+ constructor(room, onTracksChanged, onSpeakingChanged, onMicrophoneChanged, onCameraChanged = () => {}, initialCameraFacing, resilient = false) {
1186
+ this.room = room;
1187
+ this.onTracksChanged = onTracksChanged;
1188
+ this.onSpeakingChanged = onSpeakingChanged;
1189
+ this.onMicrophoneChanged = onMicrophoneChanged;
1190
+ this.onCameraChanged = onCameraChanged;
1191
+ this.initialCameraFacing = initialCameraFacing;
1192
+ this.microphoneOptions = microphonePublishOptions(resilient);
1193
+ this.room.on(RoomEvent2.ActiveDeviceChanged, this.handleDeviceChanged);
1194
+ }
1195
+ updateTracks() {
1196
+ let changed = false;
1197
+ const videoPub = this.room.localParticipant.getTrackPublication(Track5.Source.Camera);
1198
+ const videoSource = videoPub?.track ?? null;
1199
+ if (videoSource !== this.lastVideoSource) {
1200
+ this._videoTrack?.detach();
1201
+ this._videoTrack = videoSource ? new LiveKitVideoTrack(videoSource) : null;
1202
+ this.lastVideoSource = videoSource;
1203
+ changed = true;
1204
+ }
1205
+ const audioPub = this.room.localParticipant.getTrackPublication(Track5.Source.Microphone);
1206
+ const audioSource = audioPub?.track ?? null;
1207
+ if (audioSource !== this.lastAudioSource) {
1208
+ this._audioTrack?.detach();
1209
+ this._audioTrack = audioSource ? new LiveKitAudioTrack(audioSource) : null;
1210
+ this.lastAudioSource = audioSource;
1211
+ changed = true;
1212
+ }
1213
+ return changed;
1214
+ }
1215
+ get videoTrack() {
1216
+ return this._videoTrack;
1217
+ }
1218
+ get audioTrack() {
1219
+ return this._audioTrack;
1220
+ }
1221
+ get microphoneId() {
1222
+ return this.inputId("audioinput");
1223
+ }
1224
+ get cameraId() {
1225
+ return this.inputId("videoinput");
1226
+ }
1227
+ get cameraFacing() {
1228
+ if (this.disposed || this.inputs.videoinput.unavailable)
1229
+ return null;
1230
+ const track = this.inputTrack("videoinput")?.mediaStreamTrack;
1231
+ return this.isVideoEnabled && track?.readyState === "live" ? cameraFacing(track) : this.initialCameraFacing ?? this.selectedCameraFacing;
1232
+ }
1233
+ inputId(kind) {
1234
+ if (this.disposed || this.inputs[kind].unavailable)
1235
+ return null;
1236
+ const enabled = kind === "audioinput" ? !this.isMuted : this.isVideoEnabled;
1237
+ if (!enabled)
1238
+ return this.room.getActiveDevice(kind) ?? null;
1239
+ const track = this.inputTrack(kind)?.mediaStreamTrack;
1240
+ return track?.readyState === "live" ? track.getSettings().deviceId ?? this.room.getActiveDevice(kind) ?? null : null;
1241
+ }
1242
+ async setMicrophone(deviceId) {
1243
+ await this.setInputDevice("audioinput", { deviceId });
1244
+ }
1245
+ async setCamera(deviceId) {
1246
+ await this.setInputDevice("videoinput", { deviceId });
1247
+ }
1248
+ async setCameraFacing(facing) {
1249
+ await this.setInputDevice("videoinput", { facing });
1250
+ }
1251
+ async setInputDevice(kind, selection) {
1252
+ this.beginInputChange(kind);
1253
+ const state = this.inputs[kind];
1254
+ const previous = this.inputId(kind);
1255
+ try {
1256
+ await this.switchInput(kind, selection);
1257
+ if (kind === "videoinput")
1258
+ this.initialCameraFacing = undefined;
1259
+ state.unavailable = false;
1260
+ } catch (error) {
1261
+ let recovered = false;
1262
+ for (const fallback of new Set([previous, "default"])) {
1263
+ if (this.disposed || !fallback)
1264
+ continue;
1265
+ try {
1266
+ await this.switchInput(kind, { deviceId: fallback });
1267
+ state.unavailable = false;
1268
+ recovered = true;
1269
+ break;
1270
+ } catch {}
1271
+ }
1272
+ if (!recovered && !this.disposed) {
1273
+ state.unavailable = true;
1274
+ await this.applyEnabled(kind, false);
1275
+ }
1276
+ throw new MediaAccessError(error, kind === "audioinput" ? "microphone" : "camera");
1277
+ } finally {
1278
+ state.changing = false;
1279
+ if (!this.disposed) {
1280
+ if (this.updateTracks())
1281
+ this.onTracksChanged();
1282
+ this.notifyInput(kind);
1283
+ }
1284
+ }
1285
+ }
1286
+ inputTrack(kind) {
1287
+ const publication = this.room.localParticipant.getTrackPublication(kind === "audioinput" ? Track5.Source.Microphone : Track5.Source.Camera);
1288
+ return kind === "audioinput" ? publication?.audioTrack : publication?.videoTrack;
1289
+ }
1290
+ async switchInput(kind, selection) {
1291
+ const track = this.inputTrack(kind);
1292
+ if (this.disposed || !track && kind === "audioinput")
1293
+ throw new Error("Input is unavailable");
1294
+ try {
1295
+ let deviceId = "deviceId" in selection ? selection.deviceId : "";
1296
+ let facing = null;
1297
+ if ("facing" in selection || !track || track.isMuted) {
1298
+ if ("facing" in selection)
1299
+ track?.mediaStreamTrack.stop();
1300
+ const constraints = "facing" in selection ? { facingMode: { exact: selection.facing } } : {
1301
+ deviceId: deviceId === "default" ? "default" : { exact: deviceId }
1302
+ };
1303
+ const probe = await navigator.mediaDevices.getUserMedia({
1304
+ audio: kind === "audioinput" ? constraints : false,
1305
+ video: kind === "videoinput" ? constraints : false
1306
+ });
1307
+ try {
1308
+ if (kind === "videoinput") {
1309
+ const capture = probe.getVideoTracks()[0];
1310
+ if ("facing" in selection) {
1311
+ requireCameraFacing(capture, selection.facing);
1312
+ deviceId = capture?.getSettings().deviceId ?? "";
1313
+ if (!deviceId)
1314
+ throw new DOMException("Camera device ID is unavailable", "NotFoundError");
1315
+ }
1316
+ if (capture)
1317
+ facing = cameraFacing(capture);
1318
+ }
1319
+ } finally {
1320
+ for (const capture of probe.getTracks())
1321
+ capture.stop();
1322
+ }
1323
+ if (this.disposed)
1324
+ throw new Error("Participant is disconnected");
1325
+ }
1326
+ const changed = await this.room.switchActiveDevice(kind, deviceId, deviceId !== "default");
1327
+ if (this.disposed)
1328
+ throw new Error("Participant is disconnected");
1329
+ if (!changed && deviceId !== "default")
1330
+ throw new Error("Input did not change");
1331
+ if (kind === "videoinput") {
1332
+ const current = this.inputTrack(kind)?.mediaStreamTrack;
1333
+ this.selectedCameraFacing = facing ?? (current?.readyState === "live" ? cameraFacing(current) : null);
1334
+ }
1335
+ } finally {
1336
+ if (this.disposed) {
1337
+ track?.stop();
1338
+ this.inputTrack(kind)?.stop();
1339
+ }
1340
+ }
1341
+ }
1342
+ handleDeviceChanged = (kind) => {
1343
+ if (kind === "audiooutput" || this.disposed)
1344
+ return;
1345
+ if (!this.inputs[kind].changing) {
1346
+ this.inputs[kind].unavailable = false;
1347
+ if (kind === "videoinput") {
1348
+ const track = this.inputTrack(kind)?.mediaStreamTrack;
1349
+ this.selectedCameraFacing = track?.readyState === "live" ? cameraFacing(track) : null;
1350
+ }
1351
+ this.notifyInput(kind);
1352
+ }
1353
+ };
1354
+ cleanup() {
1355
+ this.disposed = true;
1356
+ this.room.off(RoomEvent2.ActiveDeviceChanged, this.handleDeviceChanged);
1357
+ if (this.inputs.audioinput.changing)
1358
+ this.inputTrack("audioinput")?.stop();
1359
+ if (this.inputs.videoinput.changing)
1360
+ this.inputTrack("videoinput")?.stop();
1361
+ this.stopSpeakingDetection();
1362
+ }
1363
+ get isSpeaking() {
1364
+ return this._isSpeaking;
1365
+ }
1366
+ async enableVideo() {
1367
+ await this.setInputEnabled("videoinput", true);
1368
+ }
1369
+ async disableVideo() {
1370
+ await this.setInputEnabled("videoinput", false);
1371
+ }
1372
+ async muteAudio() {
1373
+ await this.setInputEnabled("audioinput", false);
1374
+ }
1375
+ async unmuteAudio() {
1376
+ await this.setInputEnabled("audioinput", true);
1377
+ }
1378
+ beginInputChange(kind) {
1379
+ if (this.disposed)
1380
+ throw new Error("Participant is disconnected");
1381
+ if (this.inputs[kind].changing)
1382
+ throw new Error("Input change is in progress");
1383
+ this.inputs[kind].changing = true;
1384
+ }
1385
+ async setInputEnabled(kind, enabled) {
1386
+ this.beginInputChange(kind);
1387
+ const track = this.inputTrack(kind);
1388
+ try {
1389
+ if (kind === "videoinput" && !enabled)
1390
+ this.selectedCameraFacing = this.cameraFacing;
1391
+ if (kind === "videoinput" && enabled && this.initialCameraFacing) {
1392
+ await this.switchInput(kind, { facing: this.initialCameraFacing });
1393
+ this.initialCameraFacing = undefined;
1394
+ }
1395
+ await this.applyEnabled(kind, enabled);
1396
+ if (this.disposed)
1397
+ throw new Error("Participant is disconnected");
1398
+ if (enabled)
1399
+ this.inputs[kind].unavailable = false;
1400
+ if (kind === "videoinput" && enabled) {
1401
+ const current = this.inputTrack(kind)?.mediaStreamTrack;
1402
+ if (current?.readyState === "live")
1403
+ this.selectedCameraFacing = cameraFacing(current);
1404
+ }
1405
+ if (this.updateTracks())
1406
+ this.onTracksChanged();
1407
+ this.notifyInput(kind);
1408
+ } finally {
1409
+ this.inputs[kind].changing = false;
1410
+ if (this.disposed) {
1411
+ track?.stop();
1412
+ this.inputTrack(kind)?.stop();
1413
+ }
1414
+ }
1415
+ }
1416
+ async applyEnabled(kind, enabled) {
1417
+ if (kind === "audioinput")
1418
+ await this.room.localParticipant.setMicrophoneEnabled(enabled, undefined, this.microphoneOptions);
1419
+ else
1420
+ await this.room.localParticipant.setCameraEnabled(enabled);
1421
+ }
1422
+ notifyInput(kind) {
1423
+ if (kind === "audioinput")
1424
+ this.onMicrophoneChanged();
1425
+ else
1426
+ this.onCameraChanged();
1427
+ }
1428
+ get isMuted() {
1429
+ return !this.room.localParticipant.isMicrophoneEnabled;
1430
+ }
1431
+ get isVideoEnabled() {
1432
+ return this.room.localParticipant.isCameraEnabled;
1433
+ }
1434
+ releaseDevices() {
1435
+ for (const pub of this.room.localParticipant.trackPublications.values()) {
1436
+ pub.track?.stop();
1437
+ }
1438
+ }
1439
+ startSpeakingDetection() {
1440
+ this.stopSpeakingDetection();
1441
+ this.speakingHandler = (speaking) => {
1442
+ if (speaking !== this._isSpeaking) {
1443
+ this._isSpeaking = speaking;
1444
+ this.onSpeakingChanged(speaking);
1445
+ }
1446
+ };
1447
+ this.room.localParticipant.on(ParticipantEvent2.IsSpeakingChanged, this.speakingHandler);
1448
+ }
1449
+ stopSpeakingDetection() {
1450
+ if (this.speakingHandler) {
1451
+ this.room.localParticipant.off(ParticipantEvent2.IsSpeakingChanged, this.speakingHandler);
1452
+ this.speakingHandler = null;
1453
+ }
1454
+ if (this._isSpeaking) {
1455
+ this._isSpeaking = false;
1456
+ this.onSpeakingChanged(false);
1457
+ }
1458
+ }
1459
+ }
1460
+
1461
+ // src/providers/livekit/conversation.ts
1462
+ var DATA_EVENT_TOPIC = "nuance.data";
1463
+ var SESSION_TOPIC = "nuance.v1";
1464
+ var RUNTIME_TOPIC = "runtime";
1465
+ var CLOCK_SYNC_TOPIC = "clock-sync";
1466
+ var STATS_FLUSH_TIMEOUT_MS = 300;
1467
+ var SESSION_END_TIMEOUT_MS = 300;
1468
+ var SESSION_END_ID = "client-session-end";
1469
+ var COMMAND_TIMEOUT_MS = 5000;
1470
+ var BACKGROUND_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1471
+ var AVATAR_RETURN_TIMEOUT_MS = 8000;
1472
+ function isReconnecting(lkState) {
1473
+ return lkState === ConnectionState2.Reconnecting || lkState === ConnectionState2.SignalReconnecting;
1474
+ }
1475
+ function mapConnectionState(lkState, resilient = false) {
1476
+ switch (lkState) {
1477
+ case ConnectionState2.Connected:
1478
+ return "connected";
1479
+ case ConnectionState2.Connecting:
1480
+ return "connecting";
1481
+ case ConnectionState2.Reconnecting:
1482
+ case ConnectionState2.SignalReconnecting:
1483
+ return resilient ? "reconnecting" : "connecting";
1484
+ case ConnectionState2.Disconnected:
1485
+ return "disconnected";
1486
+ }
1487
+ }
1488
+ function mapConnectionQuality(quality) {
1489
+ switch (quality) {
1490
+ case ConnectionQuality.Poor:
1491
+ return "poor";
1492
+ case ConnectionQuality.Lost:
1493
+ return "lost";
1494
+ default:
1495
+ return "good";
1496
+ }
1497
+ }
1498
+ var QUALITY_RANK = {
1499
+ good: 0,
1500
+ poor: 1,
1501
+ lost: 2
1502
+ };
1503
+
1504
+ class LiveKitConversation {
1505
+ room;
1506
+ _state = "idle";
1507
+ _user = null;
1508
+ _avatar = new LiveKitAvatarParticipant(null);
1509
+ stats = null;
1510
+ presentation = null;
1511
+ readyFallback = null;
1512
+ layerFallback = null;
1513
+ changeHandlers = new Set;
1514
+ userQuality = "good";
1515
+ avatarQuality = "good";
1516
+ network = "good";
1517
+ eventsRegistered = false;
1518
+ sessionEnded = false;
1519
+ speaksProtocol = false;
1520
+ answered = null;
1521
+ commands = new Map;
1522
+ commandCount = 0;
1523
+ readyOwed = false;
1524
+ everConnected = false;
1525
+ away = false;
1526
+ awayTimer = null;
1527
+ presentationPaused = false;
1528
+ readySending = false;
1529
+ leaving = null;
1530
+ connectGeneration = 0;
1531
+ _id;
1532
+ serverUrl;
1533
+ cameraEnabled;
1534
+ cameraFacing;
1535
+ tokenProvider;
1536
+ opensOnReady;
1537
+ resilient;
1538
+ constructor(config) {
1539
+ this.room = new Room({
1540
+ audioCaptureDefaults: config.mediaDevices?.microphoneId ? { deviceId: { ideal: config.mediaDevices.microphoneId } } : undefined,
1541
+ videoCaptureDefaults: {
1542
+ ...config.videoCapture ? {
1543
+ resolution: {
1544
+ width: config.videoCapture.width,
1545
+ height: config.videoCapture.height,
1546
+ aspectRatio: config.videoCapture.aspectRatio ?? config.videoCapture.width / config.videoCapture.height
1547
+ }
1548
+ } : {},
1549
+ ...config.mediaDevices?.cameraId && !config.mediaDevices.cameraFacing ? { deviceId: { ideal: config.mediaDevices.cameraId } } : {}
1550
+ }
1551
+ });
1552
+ this._id = config.conversationId;
1553
+ this.serverUrl = config.serverUrl;
1554
+ this.cameraEnabled = config.cameraEnabled ?? true;
1555
+ this.cameraFacing = config.mediaDevices?.cameraFacing;
1556
+ this.tokenProvider = config.tokenProvider;
1557
+ this.opensOnReady = config.opensOnReady ?? false;
1558
+ this.resilient = config.resilientReconnect ?? false;
1559
+ }
1560
+ get id() {
1561
+ return this._id;
1562
+ }
1563
+ async connect() {
1564
+ if (this._state === "connecting" || this._state === "connected" || this._state === "reconnecting") {
1565
+ throw new Error(`Cannot connect while in "${this._state}" state`);
1566
+ }
1567
+ this.setState("connecting");
1568
+ this.sessionEnded = false;
1569
+ this.speaksProtocol = false;
1570
+ this.readyOwed = false;
1571
+ this.everConnected = false;
1572
+ if (!this.eventsRegistered) {
1573
+ this.registerRoomEvents();
1574
+ this.eventsRegistered = true;
1575
+ }
1576
+ const generation = this.connectGeneration;
1577
+ const aborted = async () => {
1578
+ if (generation === this.connectGeneration)
1579
+ return false;
1580
+ this._user?.cleanup();
1581
+ this._user?.releaseDevices();
1582
+ await this.room.disconnect();
1583
+ this._user = null;
1584
+ return true;
1585
+ };
1586
+ try {
1587
+ const token = await this.tokenProvider();
1588
+ if (await aborted())
1589
+ return;
1590
+ await this.room.connect(this.serverUrl, token);
1591
+ if (await aborted())
1592
+ return;
1593
+ this._user = new LiveKitUserParticipant(this.room, () => this.emit({ type: "tracks-changed" }), (isSpeaking) => this.emit({
1594
+ type: "speaking-changed",
1595
+ participant: "user",
1596
+ isSpeaking
1597
+ }), () => this.emit({ type: "microphone-changed" }), () => this.emit({ type: "camera-changed" }), this.cameraFacing, this.resilient);
1598
+ this._user.startSpeakingDetection();
1599
+ try {
1600
+ await this._user.unmuteAudio();
1601
+ } catch (error) {
1602
+ if (await aborted())
1603
+ return;
1604
+ throw new MediaAccessError(error, "microphone");
1605
+ }
1606
+ if (await aborted())
1607
+ return;
1608
+ if (this.cameraEnabled) {
1609
+ try {
1610
+ await this._user.enableVideo();
1611
+ } catch {}
1612
+ }
1613
+ if (await aborted())
1614
+ return;
1615
+ this.everConnected = true;
1616
+ this.setState("connected");
1617
+ this.checkForExistingAvatar();
1618
+ this.stats = new NetworkStatsReporter(this.room, (silent) => this.emit({ type: "mic-silent-changed", silent }));
1619
+ this.stats.start();
1620
+ } catch (error) {
1621
+ if (!await aborted())
1622
+ this.setState("failed");
1623
+ throw error;
1624
+ }
1625
+ }
1626
+ disconnect() {
1627
+ this.leaving ??= this.leave().finally(() => {
1628
+ this.leaving = null;
1629
+ });
1630
+ return this.leaving;
1631
+ }
1632
+ async leave() {
1633
+ this.connectGeneration++;
1634
+ this.stopPresentation();
1635
+ this.stopReadyFallback();
1636
+ this.stopLayerFallback();
1637
+ this.clearAway();
1638
+ this._user?.cleanup();
1639
+ this._user?.releaseDevices();
1640
+ await this.flushStats();
1641
+ await this.sendSessionEnd();
1642
+ await this.room.disconnect();
1643
+ this._avatar.updateParticipant(null);
1644
+ this._user = null;
1645
+ this.setState("disconnected");
1646
+ }
1647
+ get state() {
1648
+ return this._state;
1649
+ }
1650
+ get user() {
1651
+ if (!this._user) {
1652
+ throw new Error("Not connected — call connect() first");
1653
+ }
1654
+ return this._user;
1655
+ }
1656
+ get avatar() {
1657
+ return this._avatar;
1658
+ }
1659
+ async setBackground(background) {
1660
+ if (!BACKGROUND_ID.test(background)) {
1661
+ throw new Error(`Not a background id: ${JSON.stringify(background)}`);
1662
+ }
1663
+ return this.command({ type: "scene.background", background });
1664
+ }
1665
+ onChange(handler) {
1666
+ return this.onEvent((event) => {
1667
+ if (event.type !== "data-event")
1668
+ handler(event);
1669
+ });
1670
+ }
1671
+ onEvent(handler) {
1672
+ this.changeHandlers.add(handler);
1673
+ return () => {
1674
+ this.changeHandlers.delete(handler);
1675
+ };
1676
+ }
1677
+ onDataEvent(handler) {
1678
+ return this.onEvent((event) => {
1679
+ if (event.type === "data-event") {
1680
+ handler(event.event);
1681
+ }
1682
+ });
1683
+ }
1684
+ async command(command) {
1685
+ if (this.room.state !== ConnectionState2.Connected) {
1686
+ throw new Error("Not connected — call connect() first");
1687
+ }
1688
+ if (!this.speaksProtocol) {
1689
+ throw new Error("The runtime takes no session commands");
1690
+ }
1691
+ const id = `client-command-${++this.commandCount}`;
1692
+ const answer = Promise.withResolvers();
1693
+ this.commands.set(id, { type: command.type, settle: answer.resolve });
1694
+ let timer;
1695
+ try {
1696
+ return await Promise.race([
1697
+ this.room.localParticipant.publishData(new TextEncoder().encode(JSON.stringify({ ...command, id })), { reliable: true, topic: SESSION_TOPIC }).then(() => answer.promise),
1698
+ new Promise((_, reject) => {
1699
+ timer = setTimeout(() => reject(new Error(`The runtime did not answer ${command.type}`)), COMMAND_TIMEOUT_MS);
1700
+ })
1701
+ ]);
1702
+ } finally {
1703
+ clearTimeout(timer);
1704
+ this.commands.delete(id);
1705
+ }
1706
+ }
1707
+ emit(event) {
1708
+ for (const handler of this.changeHandlers) {
1709
+ handler(event);
1710
+ }
1711
+ }
1712
+ setState(state) {
1713
+ this._state = state;
1714
+ this.emit({ type: "state-changed", state });
1715
+ }
1716
+ updateNetwork() {
1717
+ const worst = QUALITY_RANK[this.avatarQuality] > QUALITY_RANK[this.userQuality] ? this.avatarQuality : this.userQuality;
1718
+ if (worst === this.network)
1719
+ return;
1720
+ this.network = worst;
1721
+ this.emit({ type: "network-changed", quality: worst });
1722
+ }
1723
+ isAvatarParticipant(_participant) {
1724
+ return true;
1725
+ }
1726
+ checkForExistingAvatar() {
1727
+ for (const [, participant] of this.room.remoteParticipants) {
1728
+ if (this.isAvatarParticipant(participant)) {
1729
+ const tracksChanged = this._avatar.updateParticipant(participant);
1730
+ this.emit({ type: "avatar-connected" });
1731
+ if (tracksChanged) {
1732
+ this.emit({ type: "tracks-changed" });
1733
+ }
1734
+ this.armReadyFallback();
1735
+ return;
1736
+ }
1737
+ }
1738
+ }
1739
+ registerRoomEvents() {
1740
+ this.room.on(RoomEvent3.ConnectionStateChanged, (lkState) => {
1741
+ if (this._state === "idle")
1742
+ return;
1743
+ if (lkState === ConnectionState2.Disconnected)
1744
+ return;
1745
+ const mapped = mapConnectionState(lkState, this.resilient && this.everConnected);
1746
+ if (mapped === "connected")
1747
+ this.everConnected = true;
1748
+ if (mapped !== this._state) {
1749
+ this.setState(mapped);
1750
+ }
1751
+ if (lkState === ConnectionState2.Connected) {
1752
+ this.sendOwedReady();
1753
+ if (this.away)
1754
+ this.awaitAvatarReturn();
1755
+ }
1756
+ });
1757
+ this.room.on(RoomEvent3.ParticipantConnected, (participant) => {
1758
+ if (this.isAvatarParticipant(participant)) {
1759
+ const tracksChanged = this._avatar.updateParticipant(participant);
1760
+ this.emit({ type: "avatar-connected" });
1761
+ if (tracksChanged) {
1762
+ this.emit({ type: "tracks-changed" });
1763
+ }
1764
+ if (this.away)
1765
+ this.avatarBack();
1766
+ }
1767
+ });
1768
+ this.room.on(RoomEvent3.ParticipantDisconnected, (participant) => {
1769
+ if (!this.isAvatarParticipant(participant))
1770
+ return;
1771
+ if (!this.resilient) {
1772
+ this.avatarLeft();
1773
+ return;
1774
+ }
1775
+ queueMicrotask(() => {
1776
+ if (this.room.state === ConnectionState2.Disconnected)
1777
+ return;
1778
+ if (isReconnecting(this.room.state))
1779
+ this.avatarAway();
1780
+ else
1781
+ this.avatarLeft();
1782
+ });
1783
+ });
1784
+ this.room.on(RoomEvent3.Disconnected, () => {
1785
+ this.stats?.abandon();
1786
+ this.stats = null;
1787
+ this.stopPresentation();
1788
+ this.stopReadyFallback();
1789
+ this.stopLayerFallback();
1790
+ this.clearAway();
1791
+ this.readyOwed = false;
1792
+ this._user?.cleanup();
1793
+ this._avatar.updateParticipant(null);
1794
+ this._user = null;
1795
+ this.userQuality = "good";
1796
+ this.avatarQuality = "good";
1797
+ this.updateNetwork();
1798
+ if (this.resilient) {
1799
+ this.emit({ type: "avatar-disconnected" });
1800
+ if (this._state === "reconnecting" && this.leaving === null) {
1801
+ this.reportEnd("connection-lost");
1802
+ }
1803
+ }
1804
+ this.setState("disconnected");
1805
+ });
1806
+ this.room.on(RoomEvent3.ConnectionQualityChanged, (quality, participant) => {
1807
+ const mapped = mapConnectionQuality(quality);
1808
+ if (participant === this.room.localParticipant) {
1809
+ this.userQuality = mapped;
1810
+ } else if (this.isAvatarParticipant(participant)) {
1811
+ this.avatarQuality = mapped;
1812
+ } else {
1813
+ return;
1814
+ }
1815
+ this.updateNetwork();
1816
+ });
1817
+ this.room.on(RoomEvent3.TrackSubscribed, (_track, _publication, participant) => {
1818
+ if (this.isAvatarParticipant(participant)) {
1819
+ if (this._avatar.updateTracks()) {
1820
+ this.emit({ type: "tracks-changed" });
1821
+ }
1822
+ this.armReadyFallback();
1823
+ }
1824
+ });
1825
+ this.room.on(RoomEvent3.TrackUnsubscribed, (_track, _publication, participant) => {
1826
+ if (this.isAvatarParticipant(participant)) {
1827
+ if (this._avatar.updateTracks()) {
1828
+ this.emit({ type: "tracks-changed" });
1829
+ }
1830
+ }
1831
+ });
1832
+ this.room.on(RoomEvent3.DataReceived, (payload, _participant, _kind, topic) => {
1833
+ if (topic === SESSION_TOPIC) {
1834
+ this.handleSessionEvent(payload);
1835
+ return;
1836
+ }
1837
+ if (topic === RUNTIME_TOPIC) {
1838
+ this.handleRuntimeSignal(payload);
1839
+ return;
1840
+ }
1841
+ if (topic === CLOCK_SYNC_TOPIC) {
1842
+ this.handleClockSyncPing(payload);
1843
+ return;
1844
+ }
1845
+ if (topic !== DATA_EVENT_TOPIC)
1846
+ return;
1847
+ const event = parseDataEvent(payload);
1848
+ if (event === null)
1849
+ return;
1850
+ this.emit({ type: "data-event", event });
1851
+ });
1852
+ }
1853
+ async flushStats() {
1854
+ const stats = this.stats;
1855
+ this.stats = null;
1856
+ if (!stats)
1857
+ return;
1858
+ await Promise.race([
1859
+ stats.stop(),
1860
+ new Promise((resolve) => setTimeout(resolve, STATS_FLUSH_TIMEOUT_MS))
1861
+ ]).catch(() => {
1862
+ return;
1863
+ });
1864
+ }
1865
+ async sendSessionEnd() {
1866
+ if (this.sessionEnded || !this.speaksProtocol || this.room.state !== ConnectionState2.Connected) {
1867
+ return;
1868
+ }
1869
+ this.sessionEnded = true;
1870
+ const answered = new Promise((resolve) => {
1871
+ this.answered = resolve;
1872
+ });
1873
+ const command = new TextEncoder().encode(JSON.stringify({ type: "session.end", id: SESSION_END_ID }));
1874
+ await Promise.race([
1875
+ this.room.localParticipant.publishData(command, { reliable: true, topic: SESSION_TOPIC }).then(() => answered),
1876
+ new Promise((resolve) => setTimeout(resolve, SESSION_END_TIMEOUT_MS))
1877
+ ]).catch(() => {
1878
+ return;
1879
+ });
1880
+ this.answered = null;
1881
+ }
1882
+ handleSessionEvent(payload) {
1883
+ const event = parseSessionEvent(payload);
1884
+ if (event === null)
1885
+ return;
1886
+ this.speaksProtocol = true;
1887
+ if (event.type === "output-ready") {
1888
+ this.markAvatarReady();
1889
+ } else if (event.type === "command-result") {
1890
+ if (event.id === SESSION_END_ID)
1891
+ this.answered?.();
1892
+ else if (event.id !== null) {
1893
+ this.commands.get(event.id)?.settle(event.result);
1894
+ }
1895
+ } else if (event.type === "command-unreadable") {
1896
+ const [oldest] = this.commands;
1897
+ if (oldest) {
1898
+ const [id, { type, settle }] = oldest;
1899
+ this.commands.delete(id);
1900
+ settle({ status: "rejected", error: `unknown command ${type}` });
1901
+ }
1902
+ } else if (event.type === "ended") {
1903
+ this.answered?.();
1904
+ if (!event.byClient)
1905
+ this.reportEnd(event.reason);
1906
+ }
1907
+ }
1908
+ handleRuntimeSignal(payload) {
1909
+ const signal = parseRuntimeSignal(payload);
1910
+ if (signal?.type === "session-ended") {
1911
+ this.reportEnd(signal.reason);
1912
+ } else if (signal?.type === "output-ready") {
1913
+ this.markAvatarReady();
1914
+ } else if (signal?.type === "link-poor") {
1915
+ this.emit({ type: "link-poor" });
1916
+ }
1917
+ }
1918
+ armReadyFallback() {
1919
+ if (!this.resilient || this._avatar.isReady || this._avatar.videoTrack === null || this.readyFallback?.running) {
1920
+ return;
1921
+ }
1922
+ this.readyFallback = new ReadyFallback(() => avatarFramesDecoded(this.room), () => this._avatar.isVideoPlaying, () => {
1923
+ this.readyFallback = null;
1924
+ if (this.opensOnReady)
1925
+ this.speaksProtocol = true;
1926
+ this.markAvatarReady();
1927
+ });
1928
+ this.readyFallback.start();
1929
+ }
1930
+ stopReadyFallback() {
1931
+ this.readyFallback?.stop();
1932
+ this.readyFallback = null;
1933
+ }
1934
+ markAvatarReady() {
1935
+ this.stopReadyFallback();
1936
+ if (this._avatar.isReady)
1937
+ return;
1938
+ this._avatar.markReady();
1939
+ this.startLayerFallback();
1940
+ this.emit({ type: "avatar-ready" });
1941
+ if (!this.opensOnReady) {
1942
+ this.emit({ type: "avatar-presented" });
1943
+ return;
1944
+ }
1945
+ this.startPresentation();
1946
+ }
1947
+ startPresentation() {
1948
+ this.presentation?.stop();
1949
+ this.presentation = new PresentationWatch(() => avatarVideoSize(this.room), () => this.markAvatarPresented());
1950
+ this.presentation.start();
1951
+ }
1952
+ markAvatarPresented() {
1953
+ this.presentation = null;
1954
+ if (this.speaksProtocol && this.room.state === ConnectionState2.Connected) {
1955
+ this.command({ type: "session.ready" }).catch(() => {
1956
+ return;
1957
+ });
1958
+ } else if (this.resilient && this.speaksProtocol && isReconnecting(this.room.state)) {
1959
+ this.readyOwed = true;
1960
+ }
1961
+ this.emit({ type: "avatar-presented" });
1962
+ }
1963
+ sendOwedReady() {
1964
+ if (!this.readyOwed || !this.speaksProtocol || this.readySending)
1965
+ return;
1966
+ this.readySending = true;
1967
+ this.command({ type: "session.ready" }).then(() => {
1968
+ this.readyOwed = false;
1969
+ }).catch(() => {
1970
+ return;
1971
+ }).finally(() => {
1972
+ this.readySending = false;
1973
+ });
1974
+ }
1975
+ avatarLeft() {
1976
+ this.stopPresentation();
1977
+ this.stopReadyFallback();
1978
+ this.stopLayerFallback();
1979
+ this._avatar.updateParticipant(null);
1980
+ this.avatarQuality = "good";
1981
+ this.updateNetwork();
1982
+ this.emit({ type: "avatar-disconnected" });
1983
+ }
1984
+ avatarAway() {
1985
+ this.stopReadyFallback();
1986
+ this.stopLayerFallback();
1987
+ this.away = true;
1988
+ if (this.presentation !== null) {
1989
+ this.stopPresentation();
1990
+ this.presentationPaused = true;
1991
+ }
1992
+ if (this._avatar.updateParticipant(null, true)) {
1993
+ this.emit({ type: "tracks-changed" });
1994
+ }
1995
+ this.avatarQuality = "good";
1996
+ this.updateNetwork();
1997
+ }
1998
+ awaitAvatarReturn() {
1999
+ if (this.awayTimer !== null)
2000
+ return;
2001
+ this.awayTimer = setTimeout(() => {
2002
+ this.awayTimer = null;
2003
+ if (!this.away)
2004
+ return;
2005
+ this.clearAway();
2006
+ this.avatarLeft();
2007
+ }, AVATAR_RETURN_TIMEOUT_MS);
2008
+ }
2009
+ avatarBack() {
2010
+ const resume = this.presentationPaused && this._avatar.isReady;
2011
+ this.clearAway();
2012
+ if (resume)
2013
+ this.startPresentation();
2014
+ if (this._avatar.isReady)
2015
+ this.startLayerFallback();
2016
+ }
2017
+ clearAway() {
2018
+ this.away = false;
2019
+ this.presentationPaused = false;
2020
+ if (this.awayTimer !== null) {
2021
+ clearTimeout(this.awayTimer);
2022
+ this.awayTimer = null;
2023
+ }
2024
+ }
2025
+ startLayerFallback() {
2026
+ if (this.layerFallback !== null)
2027
+ return;
2028
+ this.layerFallback = new LayerFallback(() => avatarVideoRate(this.room), (layer) => pinAvatarLayer(this.room, layer));
2029
+ this.layerFallback.start();
2030
+ }
2031
+ stopLayerFallback() {
2032
+ this.layerFallback?.stop();
2033
+ this.layerFallback = null;
2034
+ }
2035
+ stopPresentation() {
2036
+ this.presentation?.stop();
2037
+ this.presentation = null;
2038
+ }
2039
+ reportEnd(reason) {
2040
+ if (this.sessionEnded)
2041
+ return;
2042
+ this.sessionEnded = true;
2043
+ this.emit({ type: "session-ended", reason });
2044
+ }
2045
+ handleClockSyncPing(payload) {
2046
+ const reply = clockSyncPong(payload, Date.now() / 1000, () => Date.now() / 1000);
2047
+ if (!reply)
2048
+ return;
2049
+ this.room.localParticipant.publishData(reply, {
2050
+ reliable: true,
2051
+ topic: CLOCK_SYNC_TOPIC
2052
+ });
2053
+ }
2054
+ }
2055
+
2056
+ // src/conversation.ts
2057
+ var DEFAULT_HOST = "https://api.nuancelabs.ai";
2058
+ var QUEUE_HEARTBEAT_INTERVAL_MS = 5000;
2059
+ var HEARTBEAT_INTERVAL_MS = 5000;
2060
+ var ADMISSION_FAILED_BEATS = 6;
2061
+ var LOST_WINDOW_MS = 30000;
2062
+ function createNuanceConversation(options = {}) {
2063
+ return new NuanceConversation(options);
2064
+ }
2065
+ var TERMINAL_STATUSES = new Set([
2066
+ "failed",
2067
+ "expired",
2068
+ "ended"
2069
+ ]);
2070
+
2071
+ class DisconnectedError extends Error {
2072
+ constructor() {
2073
+ super("Disconnected while connecting");
2074
+ this.name = "DisconnectedError";
2075
+ }
2076
+ }
2077
+
2078
+ class TerminalStatusError extends Error {
2079
+ constructor(status) {
2080
+ super(`Conversation reached terminal status "${status}"`);
2081
+ this.name = "TerminalStatusError";
2082
+ }
2083
+ }
2084
+ function isLegalRequiredBody(body) {
2085
+ return isRecord(body) && body.code === "legal_required";
2086
+ }
2087
+ function isMaintenanceBody(body) {
2088
+ return isRecord(body) && body.code === "maintenance";
2089
+ }
2090
+ function isCurfewBody(body) {
2091
+ return isRecord(body) && body.code === "policy_curfew";
2092
+ }
2093
+ function browserTimezone() {
2094
+ try {
2095
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
2096
+ } catch {
2097
+ return;
2098
+ }
2099
+ }
2100
+ var liveKitProvider = (connection, config) => new LiveKitConversation({
2101
+ ...config,
2102
+ serverUrl: connection.server_url,
2103
+ tokenProvider: async () => connection.participant_token
2104
+ });
2105
+
2106
+ class NuanceConversation {
2107
+ api;
2108
+ provider;
2109
+ ensureMicrophone;
2110
+ timezone;
2111
+ ticker;
2112
+ sessionOverrides;
2113
+ personality;
2114
+ variant;
2115
+ videoCapture;
2116
+ mediaDevices;
2117
+ resilientReconnect;
2118
+ delegate = null;
2119
+ delegateUnsubscribe = null;
2120
+ stopBeats = null;
2121
+ admitted = null;
2122
+ failedBeats = 0;
2123
+ resilient = false;
2124
+ reconnectingAt = null;
2125
+ lostContact = false;
2126
+ attempt = 0;
2127
+ lastPosition = null;
2128
+ lastEstimate = null;
2129
+ capped = false;
2130
+ ended = false;
2131
+ limitTimer = null;
2132
+ limitAt = null;
2133
+ changeHandlers = new Set;
2134
+ _state = "idle";
2135
+ _id = null;
2136
+ held = null;
2137
+ constructor(options, deps = {}) {
2138
+ this.api = deps.api ?? new NuanceApiClient({
2139
+ host: options.host ?? DEFAULT_HOST,
2140
+ headers: options.headers,
2141
+ token: options.token
2142
+ });
2143
+ this.provider = deps.provider ?? liveKitProvider;
2144
+ this.ensureMicrophone = deps.ensureMicrophone ?? ensureMicrophoneAccess;
2145
+ this.timezone = deps.timezone ?? browserTimezone;
2146
+ this.ticker = deps.ticker ?? pageTicker;
2147
+ this.sessionOverrides = options.sessionOverrides ?? {};
2148
+ this.personality = options.personality;
2149
+ this.variant = options.variant;
2150
+ this.videoCapture = options.videoCapture;
2151
+ this.mediaDevices = options.mediaDevices;
2152
+ this.resilientReconnect = options.resilientReconnect ?? false;
2153
+ }
2154
+ async connect() {
2155
+ if (this._state === "connecting" || this._state === "queued" || this._state === "connected" || this._state === "reconnecting") {
2156
+ throw new Error(`Cannot connect while in "${this._state}" state`);
2157
+ }
2158
+ const attempt = ++this.attempt;
2159
+ this.setState("connecting");
2160
+ this.clearLimit();
2161
+ this.capped = false;
2162
+ this.ended = false;
2163
+ this.reconnectingAt = null;
2164
+ this.lostContact = false;
2165
+ this.lastPosition = null;
2166
+ this.lastEstimate = null;
2167
+ try {
2168
+ const mediaDevices = {
2169
+ ...typeof this.mediaDevices === "function" ? this.mediaDevices() : this.mediaDevices
2170
+ };
2171
+ await this.ensureMicrophone(mediaDevices.microphoneId);
2172
+ let conversation;
2173
+ try {
2174
+ const request = {
2175
+ session_overrides: this.sessionOverrides,
2176
+ client_ready: true
2177
+ };
2178
+ const timezone = this.timezone();
2179
+ if (timezone)
2180
+ request.timezone = timezone;
2181
+ if (this.personality)
2182
+ request.personality = this.personality;
2183
+ if (this.variant)
2184
+ request.variant = this.variant;
2185
+ conversation = await this.api.createConversation(request);
2186
+ } catch (error) {
2187
+ if (error instanceof ApiError && error.status === 409) {
2188
+ throw new ActiveSessionError;
2189
+ }
2190
+ if (error instanceof ApiError && error.status === 403 && isLegalRequiredBody(error.body)) {
2191
+ throw new LegalRequiredError;
2192
+ }
2193
+ if (error instanceof ApiError && error.status === 503 && isMaintenanceBody(error.body)) {
2194
+ throw new MaintenanceError(typeof error.body.detail === "string" ? error.body.detail : "The service is under maintenance");
2195
+ }
2196
+ if (error instanceof ApiError && error.status === 403 && isCurfewBody(error.body)) {
2197
+ throw new PolicyCurfewError(typeof error.body.retry_after_seconds === "number" ? error.body.retry_after_seconds : null);
2198
+ }
2199
+ throw error;
2200
+ }
2201
+ if (attempt !== this.attempt) {
2202
+ await this.api.abandonConversation(conversation.id).catch(() => {});
2203
+ throw new Error("Disconnected while creating the session");
2204
+ }
2205
+ this._id = conversation.id;
2206
+ this.held = conversation.id;
2207
+ this.emit({ type: "session-created", id: conversation.id });
2208
+ this.admitted = Promise.withResolvers();
2209
+ this.startHeartbeat(conversation.id, QUEUE_HEARTBEAT_INTERVAL_MS);
2210
+ await this.admitted.promise;
2211
+ this.delegate = await this.createDelegate(conversation, mediaDevices, attempt);
2212
+ this.delegateUnsubscribe = this.delegate.onEvent((event) => {
2213
+ if (event.type === "state-changed") {
2214
+ this.setState(event.state);
2215
+ return;
2216
+ }
2217
+ if (event.type === "session-ended" && event.reason === "connection-lost") {
2218
+ const held = this.held;
2219
+ this.held = null;
2220
+ this.endSession(event.reason);
2221
+ this.disconnect().catch(() => {});
2222
+ if (held !== null) {
2223
+ this.api.abandonConversation(held).catch(() => {});
2224
+ }
2225
+ return;
2226
+ }
2227
+ if (event.type === "session-ended") {
2228
+ this.endSession(event.reason);
2229
+ return;
2230
+ }
2231
+ this.emit(event);
2232
+ });
2233
+ await this.delegate.connect();
2234
+ this.startHeartbeat(conversation.id, HEARTBEAT_INTERVAL_MS);
2235
+ } catch (error) {
2236
+ if (attempt !== this.attempt) {
2237
+ throw error;
2238
+ }
2239
+ await this.disconnect().catch(() => {});
2240
+ this.setState("failed");
2241
+ throw error;
2242
+ }
2243
+ }
2244
+ async disconnect() {
2245
+ this.attempt += 1;
2246
+ this.clearLimit();
2247
+ this.stopHeartbeat();
2248
+ this.admitted?.reject(new Error("Disconnected while waiting for a slot"));
2249
+ this.admitted = null;
2250
+ this.delegateUnsubscribe?.();
2251
+ this.delegateUnsubscribe = null;
2252
+ await this.delegate?.disconnect();
2253
+ this.delegate = null;
2254
+ await this.release();
2255
+ this.setState("disconnected");
2256
+ }
2257
+ get state() {
2258
+ return this._state;
2259
+ }
2260
+ get id() {
2261
+ return this._id;
2262
+ }
2263
+ get user() {
2264
+ if (!this.delegate) {
2265
+ throw new Error("Not connected — call connect() first");
2266
+ }
2267
+ return this.delegate.user;
2268
+ }
2269
+ get avatar() {
2270
+ if (!this.delegate) {
2271
+ throw new Error("Not connected — call connect() first");
2272
+ }
2273
+ return this.delegate.avatar;
2274
+ }
2275
+ async setBackground(background) {
2276
+ if (!this.delegate) {
2277
+ throw new Error("Not connected — call connect() first");
2278
+ }
2279
+ return this.delegate.setBackground(background);
2280
+ }
2281
+ onChange(handler) {
2282
+ return this.onEvent((event) => {
2283
+ if (event.type !== "data-event")
2284
+ handler(event);
2285
+ });
2286
+ }
2287
+ onEvent(handler) {
2288
+ this.changeHandlers.add(handler);
2289
+ return () => {
2290
+ this.changeHandlers.delete(handler);
2291
+ };
2292
+ }
2293
+ onDataEvent(handler) {
2294
+ return this.onEvent((event) => {
2295
+ if (event.type === "data-event") {
2296
+ handler(event.event);
2297
+ }
2298
+ });
2299
+ }
2300
+ async createDelegate(conversation, mediaDevices, attempt) {
2301
+ if (conversation.provider !== "livekit") {
2302
+ throw new Error(`Unsupported provider ${conversation.provider}`);
2303
+ }
2304
+ const response = await this.api.connectConversation(conversation.id);
2305
+ if (attempt !== this.attempt) {
2306
+ throw new DisconnectedError;
2307
+ }
2308
+ if (response.connection.kind !== "livekit") {
2309
+ throw new Error(`Expected LiveKit connection, received ${response.connection.kind}`);
2310
+ }
2311
+ this.resilient = typeof this.resilientReconnect === "function" ? this.resilientReconnect() : this.resilientReconnect;
2312
+ return this.provider(response.connection, {
2313
+ conversationId: conversation.id,
2314
+ mediaDevices,
2315
+ cameraEnabled: !this.sessionOverrides.disable_camera && mediaDevices.cameraEnabled !== false,
2316
+ videoCapture: this.videoCapture,
2317
+ opensOnReady: conversation.provider_metadata?.client_ready === true,
2318
+ resilientReconnect: this.resilient
2319
+ });
2320
+ }
2321
+ startHeartbeat(conversationId, intervalMs) {
2322
+ this.stopHeartbeat();
2323
+ this.failedBeats = 0;
2324
+ const attempt = this.attempt;
2325
+ let currentMs = intervalMs;
2326
+ const pace = (state) => {
2327
+ const askedMs = state.next_beat_seconds != null ? state.next_beat_seconds * 1000 : intervalMs;
2328
+ if (askedMs === currentMs || attempt !== this.attempt)
2329
+ return;
2330
+ currentMs = askedMs;
2331
+ this.stopBeats?.();
2332
+ this.stopBeats = this.ticker(beat, askedMs);
2333
+ };
2334
+ const beat = () => {
2335
+ const sentAt = Date.now();
2336
+ this.api.heartbeatConversation(conversationId).then((state) => {
2337
+ if (attempt !== this.attempt)
2338
+ return;
2339
+ this.lostContact = this.failedBeats > 0;
2340
+ this.failedBeats = 0;
2341
+ this.applyQueueState(state, sentAt);
2342
+ pace(state);
2343
+ }).catch((error) => {
2344
+ if (attempt !== this.attempt)
2345
+ return;
2346
+ if (!(error instanceof TerminalStatusError)) {
2347
+ this.failedBeats += 1;
2348
+ if (this.admitted && this.failedBeats >= ADMISSION_FAILED_BEATS) {
2349
+ this.admitted.reject(new Error("Lost contact with the server while waiting", {
2350
+ cause: error
2351
+ }));
2352
+ this.admitted = null;
2353
+ }
2354
+ return;
2355
+ }
2356
+ if (this.admitted) {
2357
+ this.admitted.reject(error);
2358
+ this.admitted = null;
2359
+ return;
2360
+ }
2361
+ this.endSession(this.atLimit() ? "time-limit" : this.cutOff() ? "connection-lost" : this.capped ? "time-limit" : "ended");
2362
+ });
2363
+ };
2364
+ beat();
2365
+ this.stopBeats = this.ticker(beat, intervalMs);
2366
+ }
2367
+ applyQueueState(state, sentAt) {
2368
+ if (TERMINAL_STATUSES.has(state.status)) {
2369
+ throw new TerminalStatusError(state.status);
2370
+ }
2371
+ if (state.seconds_remaining != null) {
2372
+ this.capped = true;
2373
+ this.emit({
2374
+ type: "time-remaining",
2375
+ secondsRemaining: state.seconds_remaining
2376
+ });
2377
+ this.armLimit(state.seconds_remaining, sentAt);
2378
+ }
2379
+ if (state.status === "admitted" || state.status === "ready") {
2380
+ this.admitted?.resolve();
2381
+ this.admitted = null;
2382
+ return;
2383
+ }
2384
+ if (state.status !== "queued" || state.position == null) {
2385
+ return;
2386
+ }
2387
+ if (this._state !== "queued") {
2388
+ this.setState("queued");
2389
+ }
2390
+ this.emit({
2391
+ type: "queue-changed",
2392
+ position: state.position,
2393
+ estimatedWaitSeconds: this.smoothEstimate(state.position, state.estimated_wait_seconds ?? null),
2394
+ poolSlots: state.pool_slots ?? null
2395
+ });
2396
+ }
2397
+ smoothEstimate(position, estimate) {
2398
+ if (estimate == null) {
2399
+ return null;
2400
+ }
2401
+ const lostGround = this.lastPosition != null && position > this.lastPosition;
2402
+ this.lastPosition = position;
2403
+ if (lostGround || this.lastEstimate == null) {
2404
+ this.lastEstimate = estimate;
2405
+ } else {
2406
+ this.lastEstimate = Math.min(this.lastEstimate, estimate);
2407
+ }
2408
+ return this.lastEstimate;
2409
+ }
2410
+ armLimit(seconds, sentAt) {
2411
+ this.clearLimit();
2412
+ this.limitAt = sentAt + Math.max(0, seconds) * 1000;
2413
+ this.limitTimer = setTimeout(() => this.endSession("time-limit"), Math.max(0, seconds) * 1000);
2414
+ }
2415
+ clearLimit() {
2416
+ this.limitAt = null;
2417
+ if (this.limitTimer) {
2418
+ clearTimeout(this.limitTimer);
2419
+ this.limitTimer = null;
2420
+ }
2421
+ }
2422
+ async release() {
2423
+ const held = this.held;
2424
+ this.held = null;
2425
+ if (held === null || this.ended) {
2426
+ return;
2427
+ }
2428
+ await this.api.abandonConversation(held).catch(() => {});
2429
+ }
2430
+ endSession(reason) {
2431
+ if (this.ended) {
2432
+ return;
2433
+ }
2434
+ this.ended = true;
2435
+ this.clearLimit();
2436
+ this.stopHeartbeat();
2437
+ this.emit({ type: "session-ended", reason });
2438
+ }
2439
+ stopHeartbeat() {
2440
+ this.stopBeats?.();
2441
+ this.stopBeats = null;
2442
+ }
2443
+ emit(event) {
2444
+ for (const handler of this.changeHandlers) {
2445
+ handler(event);
2446
+ }
2447
+ }
2448
+ atLimit() {
2449
+ return this.limitAt !== null && Date.now() >= this.limitAt - HEARTBEAT_INTERVAL_MS;
2450
+ }
2451
+ cutOff() {
2452
+ if (!this.resilient) {
2453
+ return false;
2454
+ }
2455
+ if (this._state === "reconnecting" || this.lostContact) {
2456
+ return true;
2457
+ }
2458
+ return this.reconnectingAt !== null && Date.now() - this.reconnectingAt < LOST_WINDOW_MS;
2459
+ }
2460
+ setState(state) {
2461
+ if (state === this._state) {
2462
+ return;
2463
+ }
2464
+ if (state === "reconnecting" || this._state === "reconnecting") {
2465
+ this.reconnectingAt = Date.now();
2466
+ }
2467
+ this._state = state;
2468
+ this.emit({ type: "state-changed", state });
2469
+ }
2470
+ }
2471
+ // src/core/types.ts
2472
+ var ConversationState;
2473
+ ((ConversationState2) => {
2474
+ ConversationState2["Idle"] = "idle";
2475
+ ConversationState2["Queued"] = "queued";
2476
+ ConversationState2["Connecting"] = "connecting";
2477
+ ConversationState2["Connected"] = "connected";
2478
+ ConversationState2["Reconnecting"] = "reconnecting";
2479
+ ConversationState2["Disconnected"] = "disconnected";
2480
+ ConversationState2["Failed"] = "failed";
2481
+ })(ConversationState ||= {});
2482
+ export {
2483
+ createNuanceConversation,
2484
+ PolicyCurfewError,
2485
+ MediaAccessError,
2486
+ MaintenanceError,
2487
+ LegalRequiredError,
2488
+ ConversationState,
2489
+ ActiveSessionError
2490
+ };