@runwayml/avatars 0.16.0-beta.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/dist/index.cjs ADDED
@@ -0,0 +1,928 @@
1
+ 'use strict';
2
+
3
+ var livekitClient = require('livekit-client');
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
8
+
9
+ // src/error.ts
10
+ var AvatarError = class extends Error {
11
+ constructor(code, message, cause) {
12
+ super(message);
13
+ __publicField(this, "code");
14
+ __publicField(this, "cause");
15
+ this.name = "AvatarError";
16
+ this.code = code;
17
+ this.cause = cause;
18
+ }
19
+ };
20
+ function toAvatarError(code, fallbackMessage, err) {
21
+ if (err instanceof AvatarError) return err;
22
+ const cause = err instanceof Error ? err : void 0;
23
+ const message = cause?.message ?? fallbackMessage;
24
+ return new AvatarError(code, message, cause);
25
+ }
26
+
27
+ // src/api/config.ts
28
+ var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
29
+
30
+ // src/api/consume.ts
31
+ async function consumeSession(options) {
32
+ const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;
33
+ const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;
34
+ const response = await fetch(url, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ Authorization: `Bearer ${sessionKey}`
39
+ }
40
+ });
41
+ if (!response.ok) {
42
+ const errorText = await response.text();
43
+ throw new AvatarError(
44
+ "CONSUME_FAILED",
45
+ `Failed to consume session: ${response.status} ${errorText}`
46
+ );
47
+ }
48
+ return response.json();
49
+ }
50
+
51
+ // src/utils/flatDeltaAccumulator.ts
52
+ var FlatDeltaAccumulator = class {
53
+ constructor() {
54
+ __publicField(this, "turns", /* @__PURE__ */ new Map());
55
+ __publicField(this, "finalized", /* @__PURE__ */ new Set());
56
+ }
57
+ ingest(delta, participantIdentity) {
58
+ const id = `runway-transcription-${delta.role}-${delta.turn}`;
59
+ const prev = this.turns.get(id);
60
+ const text = (prev?.text ?? "") + delta.textDelta;
61
+ const identity = prev?.participantIdentity ?? participantIdentity;
62
+ this.turns.set(id, { text, participantIdentity: identity });
63
+ const prefix = `runway-transcription-${delta.role}-`;
64
+ const finalized = [];
65
+ for (const [key, turn] of this.turns) {
66
+ if (key === id || !key.startsWith(prefix)) continue;
67
+ if (this.finalized.has(key)) continue;
68
+ this.finalized.add(key);
69
+ finalized.push({
70
+ id: key,
71
+ text: turn.text,
72
+ participantIdentity: turn.participantIdentity
73
+ });
74
+ }
75
+ return {
76
+ finalized,
77
+ active: { id, text, participantIdentity: identity }
78
+ };
79
+ }
80
+ reset() {
81
+ this.turns.clear();
82
+ this.finalized.clear();
83
+ }
84
+ };
85
+
86
+ // src/utils/parseClientEvent.ts
87
+ function isAckMessage(args) {
88
+ return "status" in args && args.status === "event_sent";
89
+ }
90
+ function parseClientEvent(payload) {
91
+ try {
92
+ const message = JSON.parse(new TextDecoder().decode(payload));
93
+ if (message?.type === "client_event" && typeof message.tool === "string" && message.args != null && typeof message.args === "object" && !isAckMessage(message.args)) {
94
+ return message;
95
+ }
96
+ return null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ // src/utils/parseTranscription.ts
103
+ function tryDecodeJSON(payload) {
104
+ try {
105
+ const text = new TextDecoder().decode(payload).trim();
106
+ if (!text.startsWith("{")) return null;
107
+ const parsed = JSON.parse(text);
108
+ return parsed && typeof parsed === "object" ? parsed : null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ function tryParseSegmentArray(root, participant) {
114
+ const type = root.type;
115
+ const typeAllowed = type === void 0 || type === "transcription" || type === "transcript" || type === "voice_transcript";
116
+ if (!typeAllowed) return null;
117
+ let segments = root.segments;
118
+ if (!Array.isArray(segments) && root.data && typeof root.data === "object") {
119
+ segments = root.data.segments;
120
+ }
121
+ if (!Array.isArray(segments)) return null;
122
+ const identity = participant?.identity ?? "unknown";
123
+ const out = [];
124
+ for (const item of segments) {
125
+ if (!item || typeof item !== "object") continue;
126
+ const seg = item;
127
+ if (typeof seg.id !== "string" || typeof seg.text !== "string") continue;
128
+ out.push({
129
+ id: seg.id,
130
+ text: seg.text,
131
+ final: typeof seg.final === "boolean" ? seg.final : true,
132
+ participantIdentity: typeof seg.participantIdentity === "string" ? seg.participantIdentity : identity
133
+ });
134
+ }
135
+ return out.length > 0 ? out : null;
136
+ }
137
+ function tryParseFlatDelta(root) {
138
+ if (root.type !== "transcription") return null;
139
+ if (typeof root.text !== "string") return null;
140
+ return {
141
+ role: typeof root.role === "string" ? root.role : "assistant",
142
+ turn: typeof root.turn === "number" ? root.turn : 0,
143
+ textDelta: root.text
144
+ };
145
+ }
146
+
147
+ // src/emitter.ts
148
+ var Emitter = class {
149
+ constructor() {
150
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
151
+ }
152
+ on(event, handler) {
153
+ let set = this.listeners.get(event);
154
+ if (!set) {
155
+ set = /* @__PURE__ */ new Set();
156
+ this.listeners.set(event, set);
157
+ }
158
+ set.add(handler);
159
+ return this;
160
+ }
161
+ once(event, handler) {
162
+ const wrapper = ((...args) => {
163
+ this.off(event, wrapper);
164
+ handler(...args);
165
+ });
166
+ return this.on(event, wrapper);
167
+ }
168
+ off(event, handler) {
169
+ this.listeners.get(event)?.delete(handler);
170
+ return this;
171
+ }
172
+ emit(event, ...args) {
173
+ const set = this.listeners.get(event);
174
+ if (!set) return;
175
+ for (const handler of set) {
176
+ try {
177
+ handler(...args);
178
+ } catch (err) {
179
+ console.error(`[@runwayml/avatars] Error in ${String(event)} handler:`, err);
180
+ }
181
+ }
182
+ }
183
+ removeAllListeners(event) {
184
+ if (event) {
185
+ this.listeners.delete(event);
186
+ } else {
187
+ this.listeners.clear();
188
+ }
189
+ return this;
190
+ }
191
+ };
192
+
193
+ // src/transcript-accumulator.ts
194
+ var DEFAULT_BUFFER_SIZE = 100;
195
+ var TranscriptAccumulator = class extends Emitter {
196
+ constructor(options) {
197
+ super();
198
+ __publicField(this, "map", /* @__PURE__ */ new Map());
199
+ __publicField(this, "flatAcc", new FlatDeltaAccumulator());
200
+ __publicField(this, "interim");
201
+ __publicField(this, "bufferSize");
202
+ __publicField(this, "snapshot", []);
203
+ this.interim = options?.interim ?? false;
204
+ this.bufferSize = options?.bufferSize ?? DEFAULT_BUFFER_SIZE;
205
+ }
206
+ get entries() {
207
+ return this.snapshot;
208
+ }
209
+ /**
210
+ * Ingest a native transcription segment (from `RoomEvent.TranscriptionReceived`).
211
+ */
212
+ ingestNative(segments, participantIdentity) {
213
+ let changed = false;
214
+ for (const segment of segments) {
215
+ if (!this.interim && !segment.final) continue;
216
+ this.map.set(segment.id, {
217
+ id: segment.id,
218
+ text: segment.text,
219
+ final: segment.final,
220
+ participantIdentity,
221
+ channel: "native"
222
+ });
223
+ changed = true;
224
+ }
225
+ if (changed) this.flush();
226
+ }
227
+ /**
228
+ * Ingest a raw data-channel payload (from `RoomEvent.DataReceived`).
229
+ * Handles both segment arrays and flat deltas.
230
+ */
231
+ ingestDataChannel(payload, participantIdentity) {
232
+ const json = tryDecodeJSON(payload);
233
+ if (!json) return;
234
+ const segments = tryParseSegmentArray(json, {
235
+ identity: participantIdentity
236
+ });
237
+ if (segments) {
238
+ let changed2 = false;
239
+ for (const entry of segments) {
240
+ if (!this.interim && !entry.final) continue;
241
+ this.map.set(entry.id, { ...entry, channel: "custom" });
242
+ changed2 = true;
243
+ }
244
+ if (changed2) this.flush();
245
+ return;
246
+ }
247
+ const delta = tryParseFlatDelta(json);
248
+ if (!delta) return;
249
+ const { finalized, active } = this.flatAcc.ingest(
250
+ delta,
251
+ participantIdentity
252
+ );
253
+ let changed = false;
254
+ for (const turn of finalized) {
255
+ this.map.set(turn.id, { ...turn, final: true, channel: "custom" });
256
+ changed = true;
257
+ }
258
+ if (this.interim) {
259
+ this.map.set(active.id, { ...active, final: false, channel: "custom" });
260
+ changed = true;
261
+ }
262
+ if (changed) this.flush();
263
+ }
264
+ dispose() {
265
+ this.map.clear();
266
+ this.flatAcc.reset();
267
+ this.snapshot = [];
268
+ this.removeAllListeners();
269
+ }
270
+ flush() {
271
+ const values = Array.from(this.map.values());
272
+ this.snapshot = values.length > this.bufferSize ? values.slice(-this.bufferSize) : values;
273
+ this.emit("update", this.snapshot);
274
+ }
275
+ };
276
+
277
+ // src/types.ts
278
+ var AvatarEvent = {
279
+ StateChanged: "stateChanged",
280
+ Transcript: "transcript",
281
+ ClientEvent: "clientEvent",
282
+ Error: "error",
283
+ AvatarVideoReady: "avatarVideoReady",
284
+ AvatarAudioReady: "avatarAudioReady",
285
+ ScreenShareReady: "screenShareReady",
286
+ LocalVideoReady: "localVideoReady",
287
+ MediaChanged: "mediaChanged",
288
+ UserSpeechStarted: "userSpeechStarted",
289
+ UserSpeechEnded: "userSpeechEnded",
290
+ AvatarSpeechStarted: "avatarSpeechStarted",
291
+ AvatarSpeechEnded: "avatarSpeechEnded",
292
+ ConnectionQualityChanged: "connectionQualityChanged",
293
+ MicPermissionChanged: "micPermissionChanged",
294
+ ActiveSpeakersChanged: "activeSpeakersChanged"
295
+ };
296
+
297
+ // src/client.ts
298
+ function toLKQuality(q) {
299
+ switch (q) {
300
+ case livekitClient.ConnectionQuality.Excellent:
301
+ return "excellent";
302
+ case livekitClient.ConnectionQuality.Good:
303
+ return "good";
304
+ case livekitClient.ConnectionQuality.Poor:
305
+ return "poor";
306
+ case livekitClient.ConnectionQuality.Lost:
307
+ return "lost";
308
+ default:
309
+ return "unknown";
310
+ }
311
+ }
312
+ function toSessionState(cs) {
313
+ switch (cs) {
314
+ case livekitClient.ConnectionState.Connecting:
315
+ return "connecting";
316
+ case livekitClient.ConnectionState.Connected:
317
+ return "active";
318
+ case livekitClient.ConnectionState.Reconnecting:
319
+ return "reconnecting";
320
+ case livekitClient.ConnectionState.Disconnected:
321
+ return "ended";
322
+ default:
323
+ return "ended";
324
+ }
325
+ }
326
+ var AvatarSession = class extends Emitter {
327
+ constructor(sessionId) {
328
+ super();
329
+ __publicField(this, "room", null);
330
+ __publicField(this, "_sessionId");
331
+ __publicField(this, "_state", "idle");
332
+ __publicField(this, "_error", null);
333
+ __publicField(this, "_micEnabled", false);
334
+ __publicField(this, "_cameraEnabled", false);
335
+ __publicField(this, "_screenShareActive", false);
336
+ __publicField(this, "attachedVideoElement", null);
337
+ __publicField(this, "attachedAudioElement", null);
338
+ __publicField(this, "avatarVideoTrack", null);
339
+ __publicField(this, "avatarAudioTrack", null);
340
+ __publicField(this, "_localVideoTrack", null);
341
+ __publicField(this, "autoAudioElement", null);
342
+ __publicField(this, "flatDeltaAcc", new FlatDeltaAccumulator());
343
+ __publicField(this, "_userSpeaking", false);
344
+ __publicField(this, "_avatarSpeaking", false);
345
+ __publicField(this, "_connectedAt", null);
346
+ __publicField(this, "mic");
347
+ __publicField(this, "camera");
348
+ __publicField(this, "screenShare");
349
+ this._sessionId = sessionId;
350
+ const self = this;
351
+ this.mic = {
352
+ get isEnabled() {
353
+ return self._micEnabled;
354
+ },
355
+ enable: () => this.setMic(true),
356
+ disable: () => this.setMic(false),
357
+ toggle: () => this.setMic(!this._micEnabled),
358
+ setDevice: (deviceId) => this.switchDevice("audioinput", deviceId)
359
+ };
360
+ this.camera = {
361
+ get isEnabled() {
362
+ return self._cameraEnabled;
363
+ },
364
+ enable: () => this.setCamera(true),
365
+ disable: () => this.setCamera(false),
366
+ toggle: () => this.setCamera(!this._cameraEnabled),
367
+ setDevice: (deviceId) => this.switchDevice("videoinput", deviceId)
368
+ };
369
+ this.screenShare = {
370
+ get isActive() {
371
+ return self._screenShareActive;
372
+ },
373
+ start: () => this.setScreenShare(true),
374
+ stop: () => this.setScreenShare(false),
375
+ toggle: () => this.setScreenShare(!this._screenShareActive)
376
+ };
377
+ }
378
+ get state() {
379
+ return this._state;
380
+ }
381
+ get sessionId() {
382
+ return this._sessionId;
383
+ }
384
+ get error() {
385
+ return this._error;
386
+ }
387
+ get localVideoTrack() {
388
+ return this._localVideoTrack;
389
+ }
390
+ get duration() {
391
+ if (!this._connectedAt) return 0;
392
+ return Date.now() - this._connectedAt;
393
+ }
394
+ waitFor(event) {
395
+ return new Promise((resolve) => {
396
+ this.once(event, ((...args) => {
397
+ resolve(args[0]);
398
+ }));
399
+ });
400
+ }
401
+ streamTo(element) {
402
+ this.attachedVideoElement = element;
403
+ if (this.avatarVideoTrack) {
404
+ element.srcObject = new MediaStream([this.avatarVideoTrack]);
405
+ element.play().catch(() => {
406
+ });
407
+ }
408
+ }
409
+ stopStreaming() {
410
+ if (this.attachedVideoElement) {
411
+ this.attachedVideoElement.srcObject = null;
412
+ }
413
+ this.attachedVideoElement = null;
414
+ }
415
+ attachAudio(element) {
416
+ this.attachedAudioElement = element;
417
+ if (this.avatarAudioTrack) {
418
+ element.srcObject = new MediaStream([this.avatarAudioTrack]);
419
+ element.play().catch(() => {
420
+ });
421
+ }
422
+ }
423
+ detachAudio() {
424
+ if (this.attachedAudioElement) {
425
+ this.attachedAudioElement.srcObject = null;
426
+ }
427
+ this.attachedAudioElement = null;
428
+ }
429
+ async end() {
430
+ if (!this.room) return;
431
+ this.setState("ending");
432
+ try {
433
+ const encoder = new TextEncoder();
434
+ const data = encoder.encode(JSON.stringify({ type: "END_CALL" }));
435
+ await this.room.localParticipant.publishData(data, { reliable: true });
436
+ } catch {
437
+ }
438
+ this.cleanup();
439
+ }
440
+ transcript(options) {
441
+ const acc = new TranscriptAccumulator(options);
442
+ const handleTranscript = (entry) => {
443
+ acc.ingestNative(
444
+ [{ id: entry.id, text: entry.text, final: entry.final }],
445
+ entry.participantIdentity
446
+ );
447
+ };
448
+ this.on(AvatarEvent.Transcript, handleTranscript);
449
+ const originalDispose = acc.dispose.bind(acc);
450
+ acc.dispose = () => {
451
+ this.off(AvatarEvent.Transcript, handleTranscript);
452
+ originalDispose();
453
+ };
454
+ return acc;
455
+ }
456
+ onClientEvent(toolOrName, handler) {
457
+ const name = typeof toolOrName === "string" ? toolOrName : toolOrName.name;
458
+ const wrappedHandler = (event) => {
459
+ if (event.tool === name) {
460
+ handler(event.args);
461
+ }
462
+ };
463
+ this.on(AvatarEvent.ClientEvent, wrappedHandler);
464
+ return () => this.off(AvatarEvent.ClientEvent, wrappedHandler);
465
+ }
466
+ /** @internal Called by `streamTo` and `connect` entry points. */
467
+ async _connect(serverUrl, token, options) {
468
+ this.setState("connecting");
469
+ const room = new livekitClient.Room({
470
+ adaptiveStream: false,
471
+ dynacast: false
472
+ });
473
+ try {
474
+ this.room = room;
475
+ this.bindRoomEvents(room);
476
+ await room.connect(serverUrl, token, { autoSubscribe: true });
477
+ await this.enableInitialMedia(room, options);
478
+ } catch (err) {
479
+ room.removeAllListeners();
480
+ room.disconnect().catch(() => {
481
+ });
482
+ this.room = null;
483
+ const error = toAvatarError("CONNECTION_FAILED", "Failed to connect to session", err);
484
+ this.setError(error);
485
+ throw error;
486
+ }
487
+ }
488
+ async publishScreenShare(stream) {
489
+ if (!this.room) return;
490
+ const videoTrack = stream.getVideoTracks()[0];
491
+ if (videoTrack) {
492
+ await this.room.localParticipant.publishTrack(videoTrack, {
493
+ source: livekitClient.Track.Source.ScreenShare
494
+ });
495
+ }
496
+ const audioTrack = stream.getAudioTracks()[0];
497
+ if (audioTrack) {
498
+ await this.room.localParticipant.publishTrack(audioTrack, {
499
+ source: livekitClient.Track.Source.ScreenShareAudio
500
+ });
501
+ }
502
+ }
503
+ bindRoomEvents(room) {
504
+ room.on(livekitClient.RoomEvent.ConnectionStateChanged, (state) => {
505
+ this.setState(toSessionState(state));
506
+ });
507
+ room.on(livekitClient.RoomEvent.Disconnected, () => {
508
+ this.setState("ended");
509
+ });
510
+ room.on(livekitClient.RoomEvent.Reconnected, () => {
511
+ this.reattachTracks();
512
+ });
513
+ room.on(
514
+ livekitClient.RoomEvent.TrackSubscribed,
515
+ (track, publication) => {
516
+ if (track.kind === "video" && publication.source === livekitClient.Track.Source.Camera) {
517
+ const mediaTrack = track.mediaStreamTrack;
518
+ this.avatarVideoTrack = mediaTrack;
519
+ this.emit(AvatarEvent.AvatarVideoReady, mediaTrack);
520
+ this.syncVideoElement(mediaTrack);
521
+ }
522
+ if (track.kind === "audio" && publication.source === livekitClient.Track.Source.Microphone) {
523
+ const mediaTrack = track.mediaStreamTrack;
524
+ this.avatarAudioTrack = mediaTrack;
525
+ this.emit(AvatarEvent.AvatarAudioReady, mediaTrack);
526
+ if (this.attachedAudioElement) {
527
+ this.attachedAudioElement.srcObject = new MediaStream([mediaTrack]);
528
+ this.attachedAudioElement.play().catch(() => {
529
+ });
530
+ } else {
531
+ this.autoPlayAudio(mediaTrack);
532
+ }
533
+ }
534
+ if (track.kind === "video" && publication.source === livekitClient.Track.Source.ScreenShare) {
535
+ this.emit(AvatarEvent.ScreenShareReady, track.mediaStreamTrack);
536
+ }
537
+ }
538
+ );
539
+ room.on(livekitClient.RoomEvent.TrackUnsubscribed, (_track, publication) => {
540
+ if (publication.source === livekitClient.Track.Source.Camera) {
541
+ this.avatarVideoTrack = null;
542
+ }
543
+ if (publication.source === livekitClient.Track.Source.Microphone) {
544
+ this.avatarAudioTrack = null;
545
+ }
546
+ });
547
+ room.on(
548
+ livekitClient.RoomEvent.DataReceived,
549
+ (payload, participant) => {
550
+ const event = parseClientEvent(payload);
551
+ if (event) {
552
+ this.emit(AvatarEvent.ClientEvent, event);
553
+ return;
554
+ }
555
+ const identity = participant?.identity ?? "unknown";
556
+ const json = tryDecodeJSON(payload);
557
+ if (!json) return;
558
+ const segments = tryParseSegmentArray(json, { identity });
559
+ if (segments) {
560
+ for (const entry of segments) {
561
+ this.emit(AvatarEvent.Transcript, { ...entry, channel: "custom" });
562
+ }
563
+ return;
564
+ }
565
+ const delta = tryParseFlatDelta(json);
566
+ if (delta) {
567
+ const { finalized, active } = this.flatDeltaAcc.ingest(
568
+ delta,
569
+ identity
570
+ );
571
+ for (const turn of finalized) {
572
+ this.emit(AvatarEvent.Transcript, {
573
+ ...turn,
574
+ final: true,
575
+ channel: "custom"
576
+ });
577
+ }
578
+ this.emit(AvatarEvent.Transcript, {
579
+ ...active,
580
+ final: false,
581
+ channel: "custom"
582
+ });
583
+ }
584
+ }
585
+ );
586
+ room.on(
587
+ livekitClient.RoomEvent.TranscriptionReceived,
588
+ (segments, participant) => {
589
+ const identity = participant?.identity ?? "unknown";
590
+ for (const segment of segments) {
591
+ this.emit(AvatarEvent.Transcript, {
592
+ id: segment.id,
593
+ text: segment.text,
594
+ final: segment.final,
595
+ participantIdentity: identity,
596
+ channel: "native"
597
+ });
598
+ }
599
+ }
600
+ );
601
+ room.on(livekitClient.RoomEvent.MediaDevicesError, (error) => {
602
+ this.emit(AvatarEvent.Error, error);
603
+ });
604
+ room.on(livekitClient.RoomEvent.ActiveSpeakersChanged, (speakers) => {
605
+ const localIdentity = room.localParticipant.identity;
606
+ const localSpeaking = speakers.some((p) => p.identity === localIdentity);
607
+ const remoteSpeaking = speakers.some((p) => p.identity !== localIdentity);
608
+ if (localSpeaking && !this._userSpeaking) {
609
+ this._userSpeaking = true;
610
+ this.emit(AvatarEvent.UserSpeechStarted);
611
+ } else if (!localSpeaking && this._userSpeaking) {
612
+ this._userSpeaking = false;
613
+ this.emit(AvatarEvent.UserSpeechEnded);
614
+ }
615
+ if (remoteSpeaking && !this._avatarSpeaking) {
616
+ this._avatarSpeaking = true;
617
+ this.emit(AvatarEvent.AvatarSpeechStarted);
618
+ } else if (!remoteSpeaking && this._avatarSpeaking) {
619
+ this._avatarSpeaking = false;
620
+ this.emit(AvatarEvent.AvatarSpeechEnded);
621
+ }
622
+ const active = [];
623
+ if (localSpeaking) active.push("user");
624
+ if (remoteSpeaking) active.push("avatar");
625
+ this.emit(AvatarEvent.ActiveSpeakersChanged, active);
626
+ });
627
+ room.on(
628
+ livekitClient.RoomEvent.ConnectionQualityChanged,
629
+ (quality, _participant) => {
630
+ this.emit(AvatarEvent.ConnectionQualityChanged, toLKQuality(quality));
631
+ }
632
+ );
633
+ }
634
+ reattachTracks() {
635
+ if (this.avatarVideoTrack) {
636
+ this.syncVideoElement(this.avatarVideoTrack);
637
+ }
638
+ if (this.avatarAudioTrack) {
639
+ if (this.attachedAudioElement) {
640
+ this.attachedAudioElement.srcObject = new MediaStream([this.avatarAudioTrack]);
641
+ this.attachedAudioElement.play().catch(() => {
642
+ });
643
+ } else {
644
+ this.autoPlayAudio(this.avatarAudioTrack);
645
+ }
646
+ }
647
+ }
648
+ syncVideoElement(track) {
649
+ if (this.attachedVideoElement) {
650
+ this.attachedVideoElement.srcObject = new MediaStream([track]);
651
+ this.attachedVideoElement.play().catch(() => {
652
+ });
653
+ }
654
+ }
655
+ async enableInitialMedia(room, options) {
656
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
657
+ console.warn(
658
+ "[@runwayml/avatars] mediaDevices not available (requires secure context). Skipping mic/camera."
659
+ );
660
+ return;
661
+ }
662
+ const { audio = true, video = true } = options;
663
+ if (audio) {
664
+ this.emit(AvatarEvent.MicPermissionChanged, "pending");
665
+ try {
666
+ await room.localParticipant.setMicrophoneEnabled(true);
667
+ this._micEnabled = true;
668
+ this.emit(AvatarEvent.MicPermissionChanged, "granted");
669
+ } catch (err) {
670
+ this.emit(AvatarEvent.MicPermissionChanged, "denied");
671
+ this.emit(AvatarEvent.Error, toAvatarError("MEDIA_PERMISSION_DENIED", "Microphone access denied", err));
672
+ }
673
+ }
674
+ if (video) {
675
+ try {
676
+ await room.localParticipant.setCameraEnabled(true);
677
+ this._cameraEnabled = true;
678
+ this.emitLocalVideoTrack(room);
679
+ } catch (err) {
680
+ this.emit(AvatarEvent.Error, toAvatarError("MEDIA_PERMISSION_DENIED", "Camera access denied", err));
681
+ }
682
+ }
683
+ this.emit(AvatarEvent.MediaChanged);
684
+ }
685
+ async setMic(enabled) {
686
+ if (!this.room || !navigator.mediaDevices?.getUserMedia) return;
687
+ try {
688
+ await this.room.localParticipant.setMicrophoneEnabled(enabled);
689
+ this._micEnabled = enabled;
690
+ this.emit(AvatarEvent.MediaChanged);
691
+ } catch (err) {
692
+ this.emit(AvatarEvent.Error, toAvatarError("MEDIA_DEVICE_ERROR", "Failed to toggle microphone", err));
693
+ }
694
+ }
695
+ async setCamera(enabled) {
696
+ if (!this.room || !navigator.mediaDevices?.getUserMedia) return;
697
+ try {
698
+ await this.room.localParticipant.setCameraEnabled(enabled);
699
+ this._cameraEnabled = enabled;
700
+ if (enabled) {
701
+ this.emitLocalVideoTrack(this.room);
702
+ } else {
703
+ this._localVideoTrack = null;
704
+ }
705
+ this.emit(AvatarEvent.MediaChanged);
706
+ } catch (err) {
707
+ this.emit(AvatarEvent.Error, toAvatarError("MEDIA_DEVICE_ERROR", "Failed to toggle camera", err));
708
+ }
709
+ }
710
+ emitLocalVideoTrack(room) {
711
+ const pub = room.localParticipant.getTrackPublication(livekitClient.Track.Source.Camera);
712
+ const mediaTrack = pub?.track?.mediaStreamTrack;
713
+ if (mediaTrack) {
714
+ this._localVideoTrack = mediaTrack;
715
+ this.emit(AvatarEvent.LocalVideoReady, mediaTrack);
716
+ return;
717
+ }
718
+ const onPublished = () => {
719
+ const retryPub = room.localParticipant.getTrackPublication(livekitClient.Track.Source.Camera);
720
+ const retryTrack = retryPub?.track?.mediaStreamTrack;
721
+ if (retryTrack) {
722
+ this._localVideoTrack = retryTrack;
723
+ this.emit(AvatarEvent.LocalVideoReady, retryTrack);
724
+ room.off(livekitClient.RoomEvent.LocalTrackPublished, onPublished);
725
+ }
726
+ };
727
+ room.on(livekitClient.RoomEvent.LocalTrackPublished, onPublished);
728
+ }
729
+ async setScreenShare(active) {
730
+ if (!this.room || !navigator.mediaDevices?.getDisplayMedia) return;
731
+ try {
732
+ await this.room.localParticipant.setScreenShareEnabled(active);
733
+ this._screenShareActive = active;
734
+ this.emit(AvatarEvent.MediaChanged);
735
+ } catch (err) {
736
+ this.emit(AvatarEvent.Error, toAvatarError("SCREEN_SHARE_FAILED", "Failed to toggle screen share", err));
737
+ }
738
+ }
739
+ async switchDevice(kind, deviceId) {
740
+ if (!this.room) return;
741
+ try {
742
+ await this.room.switchActiveDevice(kind, deviceId);
743
+ this.emit(AvatarEvent.MediaChanged);
744
+ } catch (err) {
745
+ this.emit(AvatarEvent.Error, toAvatarError("MEDIA_DEVICE_ERROR", `Failed to switch ${kind}`, err));
746
+ }
747
+ }
748
+ autoPlayAudio(track) {
749
+ if (!this.autoAudioElement) {
750
+ this.autoAudioElement = document.createElement("audio");
751
+ this.autoAudioElement.autoplay = true;
752
+ }
753
+ this.autoAudioElement.srcObject = new MediaStream([track]);
754
+ this.autoAudioElement.play().catch(() => {
755
+ });
756
+ }
757
+ setError(error) {
758
+ this._error = error;
759
+ this.setState("error");
760
+ this.emit(AvatarEvent.Error, error);
761
+ }
762
+ setState(state) {
763
+ if (this._state === state) return;
764
+ this._state = state;
765
+ if (state === "active" && !this._connectedAt) {
766
+ this._connectedAt = Date.now();
767
+ }
768
+ this.emit(AvatarEvent.StateChanged, state);
769
+ }
770
+ cleanup() {
771
+ this.stopStreaming();
772
+ this.detachAudio();
773
+ if (this.autoAudioElement) {
774
+ this.autoAudioElement.srcObject = null;
775
+ this.autoAudioElement.remove();
776
+ this.autoAudioElement = null;
777
+ }
778
+ this.avatarVideoTrack = null;
779
+ this.avatarAudioTrack = null;
780
+ this._localVideoTrack = null;
781
+ if (this.room) {
782
+ this.room.removeAllListeners();
783
+ this.room.disconnect();
784
+ this.room = null;
785
+ }
786
+ this._micEnabled = false;
787
+ this._cameraEnabled = false;
788
+ this._screenShareActive = false;
789
+ this._userSpeaking = false;
790
+ this._avatarSpeaking = false;
791
+ this._connectedAt = null;
792
+ this.flatDeltaAcc.reset();
793
+ this.setState("ended");
794
+ }
795
+ };
796
+ async function resolveCredentials(options) {
797
+ const { url: serverUrl, token } = await consumeSession({
798
+ sessionId: options.sessionId,
799
+ sessionKey: options.sessionKey,
800
+ baseUrl: options.baseUrl
801
+ });
802
+ return { serverUrl, token };
803
+ }
804
+ async function streamTo(options) {
805
+ const { credentials, target, audio, video } = options;
806
+ const { sessionId, sessionKey, baseUrl } = credentials;
807
+ const { serverUrl, token } = await resolveCredentials({ sessionId, sessionKey, baseUrl });
808
+ const session = new AvatarSession(sessionId);
809
+ session.streamTo(target);
810
+ await session._connect(serverUrl, token, { audio, video });
811
+ return session;
812
+ }
813
+ async function connect(options) {
814
+ const { credentials, audio, video } = options;
815
+ const { sessionId, sessionKey, baseUrl } = credentials;
816
+ const { serverUrl, token } = await resolveCredentials({ sessionId, sessionKey, baseUrl });
817
+ const session = new AvatarSession(sessionId);
818
+ await session._connect(serverUrl, token, { audio, video });
819
+ return session;
820
+ }
821
+
822
+ // src/tools.ts
823
+ var toolSchemas = /* @__PURE__ */ new WeakMap();
824
+ var asyncSchemaWarned = /* @__PURE__ */ new WeakSet();
825
+ function getClientToolSchema(tool) {
826
+ return toolSchemas.get(tool);
827
+ }
828
+ function validateClientToolArgs(tool, args) {
829
+ const schema = toolSchemas.get(tool);
830
+ if (!schema) {
831
+ return args;
832
+ }
833
+ const result = schema["~standard"].validate(args);
834
+ if (result instanceof Promise) {
835
+ if (!asyncSchemaWarned.has(tool) && typeof console !== "undefined") {
836
+ asyncSchemaWarned.add(tool);
837
+ console.warn(
838
+ `[@runwayml/avatars-react] Async Standard Schema validation is not supported for client events (tool "${tool.name}"); subsequent events for this tool will be dropped silently.`
839
+ );
840
+ }
841
+ return null;
842
+ }
843
+ return isSuccess(result) ? result.value : null;
844
+ }
845
+ function isSuccess(result) {
846
+ return result.issues == null;
847
+ }
848
+ function clientTool(name, config) {
849
+ const tool = {
850
+ type: "client_event",
851
+ name,
852
+ description: config.description
853
+ };
854
+ if ("schema" in config) {
855
+ toolSchemas.set(tool, config.schema);
856
+ }
857
+ return tool;
858
+ }
859
+
860
+ // src/api/page-actions.ts
861
+ var clickTool = clientTool("click", {
862
+ description: "Clicks an interactive element on the page by its target ID",
863
+ args: {}
864
+ });
865
+ var scrollToTool = clientTool("scroll_to", {
866
+ description: "Scrolls the page to an element by its target ID",
867
+ args: {}
868
+ });
869
+ var highlightTool = clientTool("highlight", {
870
+ description: "Highlights an element on the page to draw attention to it by its target ID",
871
+ args: {}
872
+ });
873
+ var pageActionTools = [
874
+ {
875
+ ...clickTool,
876
+ parameters: [
877
+ {
878
+ name: "target",
879
+ type: "string",
880
+ description: "The ID or data-avatar-target value of the element to click"
881
+ }
882
+ ]
883
+ },
884
+ {
885
+ ...scrollToTool,
886
+ parameters: [
887
+ {
888
+ name: "target",
889
+ type: "string",
890
+ description: "The ID or data-avatar-target value of the element to scroll to"
891
+ }
892
+ ]
893
+ },
894
+ {
895
+ ...highlightTool,
896
+ parameters: [
897
+ {
898
+ name: "target",
899
+ type: "string",
900
+ description: "The ID or data-avatar-target value of the element to highlight"
901
+ },
902
+ {
903
+ name: "duration",
904
+ type: "number",
905
+ description: "How long to highlight in milliseconds. Defaults to 2000"
906
+ }
907
+ ]
908
+ }
909
+ ];
910
+
911
+ exports.AvatarError = AvatarError;
912
+ exports.AvatarEvent = AvatarEvent;
913
+ exports.AvatarSession = AvatarSession;
914
+ exports.FlatDeltaAccumulator = FlatDeltaAccumulator;
915
+ exports.TranscriptAccumulator = TranscriptAccumulator;
916
+ exports.clientTool = clientTool;
917
+ exports.connect = connect;
918
+ exports.consumeSession = consumeSession;
919
+ exports.getClientToolSchema = getClientToolSchema;
920
+ exports.pageActionTools = pageActionTools;
921
+ exports.parseClientEvent = parseClientEvent;
922
+ exports.streamTo = streamTo;
923
+ exports.tryDecodeJSON = tryDecodeJSON;
924
+ exports.tryParseFlatDelta = tryParseFlatDelta;
925
+ exports.tryParseSegmentArray = tryParseSegmentArray;
926
+ exports.validateClientToolArgs = validateClientToolArgs;
927
+ //# sourceMappingURL=index.cjs.map
928
+ //# sourceMappingURL=index.cjs.map