@runwayml/avatars-react 0.9.0 → 0.10.0-beta.1

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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _livekit_components_react from '@livekit/components-react';
2
- import { TrackReferenceOrPlaceholder } from '@livekit/components-react';
3
- export { RoomAudioRenderer as AudioRenderer, VideoTrack } from '@livekit/components-react';
2
+ import { TrackReference, TrackReferenceOrPlaceholder } from '@livekit/components-react';
3
+ export { RoomAudioRenderer as AudioRenderer, VideoTrack, isTrackReference } from '@livekit/components-react';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
  import * as livekit_client from 'livekit-client';
6
6
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
@@ -30,7 +30,7 @@ interface SessionCredentials {
30
30
  /**
31
31
  * Props for the AvatarSession component
32
32
  */
33
- interface AvatarSessionProps {
33
+ interface AvatarSessionProps<E extends ClientEvent = ClientEvent> {
34
34
  /** Connection credentials from Runway API */
35
35
  credentials: SessionCredentials;
36
36
  /** Children to render inside the session */
@@ -43,6 +43,8 @@ interface AvatarSessionProps {
43
43
  onEnd?: () => void;
44
44
  /** Callback when an error occurs */
45
45
  onError?: (error: Error) => void;
46
+ /** Callback when a client event is received from the avatar */
47
+ onClientEvent?: ClientEventHandler<E>;
46
48
  /**
47
49
  * Pre-captured screen share stream (from getDisplayMedia).
48
50
  * When provided, screen sharing activates automatically once the session connects.
@@ -57,7 +59,7 @@ interface AvatarSessionProps {
57
59
  /**
58
60
  * Props for the AvatarCall component
59
61
  */
60
- interface AvatarCallProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'onError'> {
62
+ interface AvatarCallProps<E extends ClientEvent = ClientEvent> extends Omit<React.ComponentPropsWithoutRef<'div'>, 'onError'> {
61
63
  /** The avatar ID to connect to */
62
64
  avatarId: string;
63
65
  /** Session ID (use with sessionKey - package will call consumeSession) */
@@ -82,6 +84,8 @@ interface AvatarCallProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'o
82
84
  onEnd?: () => void;
83
85
  /** Callback when an error occurs */
84
86
  onError?: (error: Error) => void;
87
+ /** Callback when a client event is received from the avatar */
88
+ onClientEvent?: ClientEventHandler<E>;
85
89
  /** Custom children - defaults to AvatarVideo + ControlBar if not provided */
86
90
  children?: React.ReactNode;
87
91
  /**
@@ -106,10 +110,24 @@ interface UseAvatarReturn {
106
110
  /** Whether the avatar has video */
107
111
  hasVideo: boolean;
108
112
  }
113
+ /**
114
+ * Media device error state exposed to consumers.
115
+ * Populated when getUserMedia fails (e.g. device held by Zoom).
116
+ */
117
+ interface MediaDeviceErrors {
118
+ /** Error from acquiring the microphone, if any */
119
+ micError: Error | null;
120
+ /** Error from acquiring the camera, if any */
121
+ cameraError: Error | null;
122
+ /** Re-attempt microphone acquisition (e.g. after closing Zoom) */
123
+ retryMic: () => Promise<void>;
124
+ /** Re-attempt camera acquisition */
125
+ retryCamera: () => Promise<void>;
126
+ }
109
127
  /**
110
128
  * Return type for useLocalMedia hook
111
129
  */
112
- interface UseLocalMediaReturn {
130
+ interface UseLocalMediaReturn extends MediaDeviceErrors {
113
131
  /** Whether a microphone device is available */
114
132
  hasMic: boolean;
115
133
  /** Whether a camera device is available */
@@ -129,8 +147,51 @@ interface UseLocalMediaReturn {
129
147
  /** The local video track reference */
130
148
  localVideoTrackRef: _livekit_components_react.TrackReferenceOrPlaceholder | null;
131
149
  }
150
+ /**
151
+ * Client event received from the avatar via the data channel.
152
+ * These are fire-and-forget events triggered by the avatar model.
153
+ *
154
+ * @typeParam T - The tool name (defaults to string for untyped usage)
155
+ * @typeParam A - The args type (defaults to Record<string, unknown>)
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * // Untyped usage
160
+ * const event: ClientEvent = { type: 'client_event', tool: 'show_caption', args: { text: 'Hello' } };
161
+ *
162
+ * // Typed usage with discriminated union
163
+ * type MyEvent = ClientEvent<'show_caption', { text: string }>;
164
+ * ```
165
+ */
166
+ interface ClientEvent<T extends string = string, A = Record<string, unknown>> {
167
+ type: 'client_event';
168
+ tool: T;
169
+ args: A;
170
+ }
171
+ /**
172
+ * Handler function for client events
173
+ */
174
+ type ClientEventHandler<E extends ClientEvent = ClientEvent> = (event: E) => void;
175
+ /**
176
+ * A transcription segment received from the session.
177
+ * SDK-owned type wrapping the underlying transport's transcription data.
178
+ */
179
+ interface TranscriptionEntry {
180
+ /** Unique segment identifier */
181
+ id: string;
182
+ /** Transcribed text */
183
+ text: string;
184
+ /** Whether this is a final (non-streaming) segment */
185
+ final: boolean;
186
+ /** Identity of the participant who spoke */
187
+ participantIdentity: string;
188
+ }
189
+ /**
190
+ * Handler function for transcription events
191
+ */
192
+ type TranscriptionHandler = (entry: TranscriptionEntry) => void;
132
193
 
133
- declare function AvatarCall({ avatarId, sessionId, sessionKey, credentials: directCredentials, connectUrl, connect, baseUrl, audio, video, avatarImageUrl, onEnd, onError, children, initialScreenStream, __unstable_roomOptions, ...props }: AvatarCallProps): react_jsx_runtime.JSX.Element;
194
+ declare function AvatarCall<E extends ClientEvent = ClientEvent>({ avatarId, sessionId, sessionKey, credentials: directCredentials, connectUrl, connect, baseUrl, audio, video, avatarImageUrl, onEnd, onError, onClientEvent, children, initialScreenStream, __unstable_roomOptions, ...props }: AvatarCallProps<E>): react_jsx_runtime.JSX.Element;
134
195
 
135
196
  /**
136
197
  * AvatarSession component - the main entry point for avatar sessions
@@ -138,7 +199,7 @@ declare function AvatarCall({ avatarId, sessionId, sessionKey, credentials: dire
138
199
  * Establishes a WebRTC connection and provides session state to children.
139
200
  * This is a headless component that renders minimal DOM.
140
201
  */
141
- declare function AvatarSession({ credentials, children, audio: requestAudio, video: requestVideo, onEnd, onError, initialScreenStream, __unstable_roomOptions, }: AvatarSessionProps): react_jsx_runtime.JSX.Element;
202
+ declare function AvatarSession<E extends ClientEvent = ClientEvent>({ credentials, children, audio: requestAudio, video: requestVideo, onEnd, onError, onClientEvent, initialScreenStream, __unstable_roomOptions, }: AvatarSessionProps<E>): react_jsx_runtime.JSX.Element;
142
203
 
143
204
  /**
144
205
  * useAvatarStatus Hook
@@ -176,7 +237,7 @@ type AvatarStatus = {
176
237
  status: 'waiting';
177
238
  } | {
178
239
  status: 'ready';
179
- videoTrackRef: TrackReferenceOrPlaceholder;
240
+ videoTrackRef: TrackReference;
180
241
  } | {
181
242
  status: 'ending';
182
243
  } | {
@@ -209,6 +270,10 @@ interface ControlBarState {
209
270
  toggleScreenShare: () => void;
210
271
  endCall: () => Promise<void>;
211
272
  isActive: boolean;
273
+ micError: Error | null;
274
+ cameraError: Error | null;
275
+ retryMic: () => Promise<void>;
276
+ retryCamera: () => Promise<void>;
212
277
  }
213
278
  interface ControlBarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {
214
279
  showMicrophone?: boolean;
@@ -292,6 +357,70 @@ type UseAvatarSessionReturn = {
292
357
  */
293
358
  declare function useAvatarSession(): UseAvatarSessionReturn;
294
359
 
360
+ type EventArgs<E extends ClientEvent, T extends E['tool']> = Extract<E, {
361
+ tool: T;
362
+ }>['args'];
363
+ /**
364
+ * Subscribe to a single client event type by tool name.
365
+ *
366
+ * Returns the latest args as React state (`null` before the first event),
367
+ * and optionally fires a callback on each event for side effects.
368
+ *
369
+ * Must be used within an AvatarSession or AvatarCall component.
370
+ *
371
+ * @example
372
+ * ```tsx
373
+ * // State only — returns latest args
374
+ * const score = useClientEvent<TriviaEvent, 'update_score'>('update_score');
375
+ * // score: { score: number; streak: number } | null
376
+ *
377
+ * // State + side effect
378
+ * const result = useClientEvent<TriviaEvent, 'reveal_answer'>('reveal_answer', (args) => {
379
+ * if (args.correct) fireConfetti();
380
+ * });
381
+ *
382
+ * // Side effect only — ignore the return value
383
+ * useClientEvent<TriviaEvent, 'play_sound'>('play_sound', (args) => {
384
+ * new Audio(SOUNDS[args.sound]).play();
385
+ * });
386
+ * ```
387
+ */
388
+ declare function useClientEvent<E extends ClientEvent, T extends E['tool']>(toolName: T, onEvent?: (args: EventArgs<E, T>) => void): EventArgs<E, T> | null;
389
+
390
+ /**
391
+ * Hook to listen for all client events from the avatar.
392
+ *
393
+ * Use this hook in child components to handle client events without prop drilling.
394
+ * Must be used within an AvatarSession or AvatarCall component.
395
+ *
396
+ * @typeParam E - The expected event type (defaults to ClientEvent for untyped usage)
397
+ *
398
+ * @example
399
+ * ```tsx
400
+ * // Untyped usage
401
+ * useClientEvents((event) => {
402
+ * console.log('Received:', event.tool, event.args);
403
+ * });
404
+ *
405
+ * // Type-safe usage with discriminated union
406
+ * type MyEvents =
407
+ * | ClientEvent<'show_caption', { text: string }>
408
+ * | ClientEvent<'play_sound', { url: string }>;
409
+ *
410
+ * useClientEvents<MyEvents>((event) => {
411
+ * switch (event.tool) {
412
+ * case 'show_caption':
413
+ * setCaption(event.args.text); // TypeScript knows this is string
414
+ * break;
415
+ * case 'play_sound':
416
+ * new Audio(event.args.url).play();
417
+ * break;
418
+ * }
419
+ * });
420
+ * ```
421
+ */
422
+ declare function useClientEvents<E extends ClientEvent = ClientEvent>(handler: ClientEventHandler<E>): void;
423
+
295
424
  /**
296
425
  * Hook for local media controls (mic, camera, screen share).
297
426
  *
@@ -301,4 +430,81 @@ declare function useAvatarSession(): UseAvatarSessionReturn;
301
430
  */
302
431
  declare function useLocalMedia(): UseLocalMediaReturn;
303
432
 
304
- export { AvatarCall, type AvatarCallProps, AvatarSession, type AvatarStatus, AvatarVideo, type AvatarVideoStatus, ControlBar, ScreenShareVideo, type SessionCredentials, type SessionState, UserVideo, useAvatar, useAvatarSession, useAvatarStatus, useLocalMedia };
433
+ /**
434
+ * Hook to listen for transcription events from the session.
435
+ *
436
+ * Fires the handler for each transcription segment received. By default,
437
+ * only final segments are delivered. Pass `{ interim: true }` to also
438
+ * receive partial/streaming segments.
439
+ *
440
+ * Must be used within an AvatarSession or AvatarCall component.
441
+ *
442
+ * @example
443
+ * ```tsx
444
+ * useTranscription((entry) => {
445
+ * console.log(`${entry.participantIdentity}: ${entry.text}`);
446
+ * });
447
+ *
448
+ * // Include interim (non-final) segments
449
+ * useTranscription((entry) => {
450
+ * console.log(entry.final ? 'FINAL' : 'partial', entry.text);
451
+ * }, { interim: true });
452
+ * ```
453
+ */
454
+ declare function useTranscription(handler: TranscriptionHandler, options?: {
455
+ interim?: boolean;
456
+ }): void;
457
+
458
+ /**
459
+ * A standalone client tool definition. Composable — combine into arrays
460
+ * and derive event types with `ClientEventsFrom`.
461
+ *
462
+ * At runtime this is just `{ type, name, description }`. The `Args` generic
463
+ * is phantom — it only exists at the TypeScript level for type narrowing.
464
+ */
465
+ interface ClientToolDef<Name extends string = string, Args = unknown> {
466
+ readonly type: 'client_event';
467
+ readonly name: Name;
468
+ readonly description: string;
469
+ /** @internal phantom field — always `undefined` at runtime */
470
+ readonly _args?: Args;
471
+ }
472
+ /**
473
+ * Derive a discriminated union of ClientEvent types from an array of tools.
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * const tools = [showQuestion, playSound];
478
+ * type MyEvent = ClientEventsFrom<typeof tools>;
479
+ * ```
480
+ */
481
+ type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> = T[number] extends infer U ? U extends ClientToolDef<infer Name, infer Args> ? ClientEvent<Name, Args> : never : never;
482
+ /**
483
+ * Define a single client tool.
484
+ *
485
+ * Returns a standalone object that can be composed into arrays and passed
486
+ * to `realtimeSessions.create({ tools })`.
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * const showQuestion = clientTool('show_question', {
491
+ * description: 'Display a trivia question',
492
+ * args: {} as { question: string; options: Array<string> },
493
+ * });
494
+ *
495
+ * const playSound = clientTool('play_sound', {
496
+ * description: 'Play a sound effect',
497
+ * args: {} as { sound: 'correct' | 'incorrect' },
498
+ * });
499
+ *
500
+ * // Combine and derive types
501
+ * const tools = [showQuestion, playSound];
502
+ * type MyEvent = ClientEventsFrom<typeof tools>;
503
+ * ```
504
+ */
505
+ declare function clientTool<Name extends string, Args>(name: Name, config: {
506
+ description: string;
507
+ args: Args;
508
+ }): ClientToolDef<Name, Args>;
509
+
510
+ export { AvatarCall, type AvatarCallProps, AvatarSession, type AvatarStatus, AvatarVideo, type AvatarVideoStatus, type ClientEvent, type ClientEventHandler, type ClientEventsFrom, type ClientToolDef, ControlBar, type MediaDeviceErrors, ScreenShareVideo, type SessionCredentials, type SessionState, type TranscriptionEntry, type TranscriptionHandler, UserVideo, clientTool, useAvatar, useAvatarSession, useAvatarStatus, useClientEvent, useClientEvents, useLocalMedia, useTranscription };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices, TrackToggle } from '@livekit/components-react';
2
- export { RoomAudioRenderer as AudioRenderer, VideoTrack } from '@livekit/components-react';
3
- import { createContext, useRef, useEffect, useCallback, useState, useContext, useSyncExternalStore } from 'react';
4
- import { ConnectionState, Track } from 'livekit-client';
2
+ export { RoomAudioRenderer as AudioRenderer, VideoTrack, isTrackReference } from '@livekit/components-react';
3
+ import { createContext, useRef, useEffect, useState, useCallback, useMemo, useContext, useSyncExternalStore } from 'react';
4
+ import { ConnectionState, Track, RoomEvent } from 'livekit-client';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/api/config.ts
@@ -153,53 +153,22 @@ function useLatest(value) {
153
153
  }, [value]);
154
154
  return ref;
155
155
  }
156
- async function hasMediaDevice(kind, timeoutMs = 1e3) {
156
+
157
+ // src/utils/parseClientEvent.ts
158
+ function isAckMessage(args) {
159
+ return "status" in args && args.status === "event_sent";
160
+ }
161
+ function parseClientEvent(payload) {
157
162
  try {
158
- const timeoutPromise = new Promise(
159
- (resolve) => setTimeout(() => resolve(false), timeoutMs)
160
- );
161
- const checkPromise = navigator.mediaDevices.enumerateDevices().then((devices) => devices.some((device) => device.kind === kind));
162
- return await Promise.race([checkPromise, timeoutPromise]);
163
+ const message = JSON.parse(new TextDecoder().decode(payload));
164
+ if (message?.type === "client_event" && typeof message.tool === "string" && message.args != null && typeof message.args === "object" && !isAckMessage(message.args)) {
165
+ return message;
166
+ }
167
+ return null;
163
168
  } catch {
164
- return false;
169
+ return null;
165
170
  }
166
171
  }
167
- function useDeviceAvailability(requestAudio, requestVideo) {
168
- const [state, setState] = useState({
169
- audio: requestAudio,
170
- // Optimistically assume devices exist
171
- video: requestVideo
172
- });
173
- useEffect(() => {
174
- let cancelled = false;
175
- async function checkDevices() {
176
- const [hasAudio, hasVideo] = await Promise.all([
177
- requestAudio ? hasMediaDevice("audioinput") : Promise.resolve(false),
178
- requestVideo ? hasMediaDevice("videoinput") : Promise.resolve(false)
179
- ]);
180
- if (!cancelled) {
181
- setState({
182
- audio: requestAudio && hasAudio,
183
- video: requestVideo && hasVideo
184
- });
185
- }
186
- }
187
- checkDevices();
188
- return () => {
189
- cancelled = true;
190
- };
191
- }, [requestAudio, requestVideo]);
192
- return state;
193
- }
194
- var MEDIA_DEVICE_ERROR_NAMES = /* @__PURE__ */ new Set([
195
- "NotAllowedError",
196
- "NotFoundError",
197
- "NotReadableError",
198
- "OverconstrainedError"
199
- ]);
200
- function isMediaDeviceError(error) {
201
- return MEDIA_DEVICE_ERROR_NAMES.has(error.name);
202
- }
203
172
  var DEFAULT_ROOM_OPTIONS = {
204
173
  adaptiveStream: false,
205
174
  dynacast: false
@@ -221,6 +190,7 @@ function mapConnectionState(connectionState) {
221
190
  var AvatarSessionContext = createContext(
222
191
  null
223
192
  );
193
+ var MediaDeviceErrorContext = createContext(null);
224
194
  function AvatarSession({
225
195
  credentials,
226
196
  children,
@@ -228,16 +198,14 @@ function AvatarSession({
228
198
  video: requestVideo = true,
229
199
  onEnd,
230
200
  onError,
201
+ onClientEvent,
231
202
  initialScreenStream,
232
203
  __unstable_roomOptions
233
204
  }) {
234
205
  const errorRef = useRef(null);
235
- const deviceAvailability = useDeviceAvailability(requestAudio, requestVideo);
236
206
  const handleError = (error) => {
237
207
  onError?.(error);
238
- if (!isMediaDeviceError(error)) {
239
- errorRef.current = error;
240
- }
208
+ errorRef.current = error;
241
209
  };
242
210
  const roomOptions = {
243
211
  ...DEFAULT_ROOM_OPTIONS,
@@ -249,8 +217,8 @@ function AvatarSession({
249
217
  serverUrl: credentials.serverUrl,
250
218
  token: credentials.token,
251
219
  connect: true,
252
- audio: deviceAvailability.audio,
253
- video: deviceAvailability.video,
220
+ audio: false,
221
+ video: false,
254
222
  onDisconnected: () => onEnd?.(),
255
223
  onError: handleError,
256
224
  options: roomOptions,
@@ -262,7 +230,10 @@ function AvatarSession({
262
230
  AvatarSessionContextInner,
263
231
  {
264
232
  sessionId: credentials.sessionId,
233
+ requestAudio,
234
+ requestVideo,
265
235
  onEnd,
236
+ onClientEvent,
266
237
  errorRef,
267
238
  initialScreenStream,
268
239
  children
@@ -275,7 +246,10 @@ function AvatarSession({
275
246
  }
276
247
  function AvatarSessionContextInner({
277
248
  sessionId,
249
+ requestAudio,
250
+ requestVideo,
278
251
  onEnd,
252
+ onClientEvent,
279
253
  errorRef,
280
254
  initialScreenStream,
281
255
  children
@@ -284,6 +258,8 @@ function AvatarSessionContextInner({
284
258
  const connectionState = useConnectionState();
285
259
  const onEndRef = useRef(onEnd);
286
260
  onEndRef.current = onEnd;
261
+ const onClientEventRef = useRef(onClientEvent);
262
+ onClientEventRef.current = onClientEvent;
287
263
  const publishedRef = useRef(false);
288
264
  useEffect(() => {
289
265
  if (connectionState !== ConnectionState.Connected) return;
@@ -307,6 +283,76 @@ function AvatarSessionContextInner({
307
283
  });
308
284
  };
309
285
  }, [connectionState, initialScreenStream, room]);
286
+ const [micError, setMicError] = useState(null);
287
+ const [cameraError, setCameraError] = useState(null);
288
+ const mediaEnabledRef = useRef(false);
289
+ useEffect(() => {
290
+ if (connectionState !== ConnectionState.Connected) return;
291
+ if (mediaEnabledRef.current) return;
292
+ mediaEnabledRef.current = true;
293
+ async function enableMedia() {
294
+ if (requestAudio) {
295
+ try {
296
+ await room.localParticipant.setMicrophoneEnabled(true);
297
+ } catch (err) {
298
+ if (err instanceof Error) setMicError(err);
299
+ }
300
+ }
301
+ if (requestVideo) {
302
+ try {
303
+ await room.localParticipant.setCameraEnabled(true);
304
+ } catch (err) {
305
+ if (err instanceof Error) setCameraError(err);
306
+ }
307
+ }
308
+ }
309
+ enableMedia();
310
+ }, [connectionState, room, requestAudio, requestVideo]);
311
+ useEffect(() => {
312
+ function handleMediaDevicesError(error, kind) {
313
+ if (kind === "audioinput") {
314
+ setMicError(error);
315
+ } else if (kind === "videoinput") {
316
+ setCameraError(error);
317
+ }
318
+ }
319
+ room.on(RoomEvent.MediaDevicesError, handleMediaDevicesError);
320
+ return () => {
321
+ room.off(RoomEvent.MediaDevicesError, handleMediaDevicesError);
322
+ };
323
+ }, [room]);
324
+ const retryMic = useCallback(async () => {
325
+ try {
326
+ await room.localParticipant.setMicrophoneEnabled(true);
327
+ setMicError(null);
328
+ } catch (err) {
329
+ if (err instanceof Error) setMicError(err);
330
+ }
331
+ }, [room]);
332
+ const retryCamera = useCallback(async () => {
333
+ try {
334
+ await room.localParticipant.setCameraEnabled(true);
335
+ setCameraError(null);
336
+ } catch (err) {
337
+ if (err instanceof Error) setCameraError(err);
338
+ }
339
+ }, [room]);
340
+ const mediaDeviceErrors = useMemo(
341
+ () => ({ micError, cameraError, retryMic, retryCamera }),
342
+ [micError, cameraError, retryMic, retryCamera]
343
+ );
344
+ useEffect(() => {
345
+ function handleDataReceived(payload) {
346
+ const event = parseClientEvent(payload);
347
+ if (event) {
348
+ onClientEventRef.current?.(event);
349
+ }
350
+ }
351
+ room.on(RoomEvent.DataReceived, handleDataReceived);
352
+ return () => {
353
+ room.off(RoomEvent.DataReceived, handleDataReceived);
354
+ };
355
+ }, [room]);
310
356
  const end = useCallback(async () => {
311
357
  try {
312
358
  const encoder = new TextEncoder();
@@ -323,7 +369,7 @@ function AvatarSessionContextInner({
323
369
  error: errorRef.current,
324
370
  end
325
371
  };
326
- return /* @__PURE__ */ jsx(AvatarSessionContext.Provider, { value: contextValue, children });
372
+ return /* @__PURE__ */ jsx(AvatarSessionContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(MediaDeviceErrorContext.Provider, { value: mediaDeviceErrors, children }) });
327
373
  }
328
374
  function useAvatarSessionContext() {
329
375
  const context = useContext(AvatarSessionContext);
@@ -334,6 +380,9 @@ function useAvatarSessionContext() {
334
380
  }
335
381
  return context;
336
382
  }
383
+ function useMediaDeviceErrorContext() {
384
+ return useContext(MediaDeviceErrorContext);
385
+ }
337
386
  function useAvatar() {
338
387
  const remoteParticipants = useRemoteParticipants();
339
388
  const avatarParticipant = remoteParticipants[0] ?? null;
@@ -369,7 +418,10 @@ function useAvatarStatus() {
369
418
  return { status: "connecting" };
370
419
  case "active":
371
420
  if (hasVideo && videoTrackRef) {
372
- return { status: "ready", videoTrackRef };
421
+ return {
422
+ status: "ready",
423
+ videoTrackRef
424
+ };
373
425
  }
374
426
  return { status: "waiting" };
375
427
  case "ending":
@@ -396,8 +448,16 @@ function AvatarVideo({ children, ...props }) {
396
448
  }
397
449
  );
398
450
  }
451
+ var NOOP_ASYNC = async () => {
452
+ };
399
453
  function useLocalMedia() {
400
454
  const { localParticipant } = useLocalParticipant();
455
+ const {
456
+ micError = null,
457
+ cameraError = null,
458
+ retryMic = NOOP_ASYNC,
459
+ retryCamera = NOOP_ASYNC
460
+ } = useMediaDeviceErrorContext() ?? {};
401
461
  const audioDevices = useMediaDevices({ kind: "audioinput" });
402
462
  const videoDevices = useMediaDevices({ kind: "videoinput" });
403
463
  const hasMic = audioDevices?.length > 0;
@@ -443,7 +503,11 @@ function useLocalMedia() {
443
503
  toggleMic,
444
504
  toggleCamera,
445
505
  toggleScreenShare,
446
- localVideoTrackRef
506
+ localVideoTrackRef,
507
+ micError,
508
+ cameraError,
509
+ retryMic,
510
+ retryCamera
447
511
  };
448
512
  }
449
513
  function ControlBar({
@@ -461,7 +525,11 @@ function ControlBar({
461
525
  isScreenShareEnabled,
462
526
  toggleMic,
463
527
  toggleCamera,
464
- toggleScreenShare
528
+ toggleScreenShare,
529
+ micError,
530
+ cameraError,
531
+ retryMic,
532
+ retryCamera
465
533
  } = useLocalMedia();
466
534
  const isActive = session.state === "active";
467
535
  const state = {
@@ -472,7 +540,11 @@ function ControlBar({
472
540
  toggleCamera,
473
541
  toggleScreenShare,
474
542
  endCall: session.end,
475
- isActive
543
+ isActive,
544
+ micError,
545
+ cameraError,
546
+ retryMic,
547
+ retryCamera
476
548
  };
477
549
  if (children) {
478
550
  return /* @__PURE__ */ jsx(Fragment, { children: children(state) });
@@ -634,6 +706,7 @@ function AvatarCall({
634
706
  avatarImageUrl,
635
707
  onEnd,
636
708
  onError,
709
+ onClientEvent,
637
710
  children,
638
711
  initialScreenStream,
639
712
  __unstable_roomOptions,
@@ -687,6 +760,7 @@ function AvatarCall({
687
760
  video,
688
761
  onEnd,
689
762
  onError: handleSessionError,
763
+ onClientEvent,
690
764
  initialScreenStream,
691
765
  __unstable_roomOptions,
692
766
  children: children ?? defaultChildren
@@ -721,7 +795,79 @@ function ScreenShareVideo({
721
795
  }
722
796
  return /* @__PURE__ */ jsx("div", { ...props, "data-avatar-screen-share": "", "data-avatar-sharing": isSharing, children: screenShareTrackRef && isTrackReference(screenShareTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: screenShareTrackRef }) });
723
797
  }
798
+ function useClientEvent(toolName, onEvent) {
799
+ const room = useRoomContext();
800
+ const [state, setState] = useState(null);
801
+ const onEventRef = useRef(onEvent);
802
+ onEventRef.current = onEvent;
803
+ useEffect(() => {
804
+ function handleDataReceived(payload) {
805
+ const event = parseClientEvent(payload);
806
+ if (event && event.tool === toolName) {
807
+ const args = event.args;
808
+ setState(args);
809
+ onEventRef.current?.(args);
810
+ }
811
+ }
812
+ room.on(RoomEvent.DataReceived, handleDataReceived);
813
+ return () => {
814
+ room.off(RoomEvent.DataReceived, handleDataReceived);
815
+ };
816
+ }, [room, toolName]);
817
+ return state;
818
+ }
819
+ function useClientEvents(handler) {
820
+ const room = useRoomContext();
821
+ const handlerRef = useRef(handler);
822
+ handlerRef.current = handler;
823
+ useEffect(() => {
824
+ function handleDataReceived(payload) {
825
+ const event = parseClientEvent(payload);
826
+ if (event) {
827
+ handlerRef.current(event);
828
+ }
829
+ }
830
+ room.on(RoomEvent.DataReceived, handleDataReceived);
831
+ return () => {
832
+ room.off(RoomEvent.DataReceived, handleDataReceived);
833
+ };
834
+ }, [room]);
835
+ }
836
+ function useTranscription(handler, options) {
837
+ const room = useRoomContext();
838
+ const handlerRef = useRef(handler);
839
+ handlerRef.current = handler;
840
+ const interimRef = useRef(options?.interim ?? false);
841
+ interimRef.current = options?.interim ?? false;
842
+ useEffect(() => {
843
+ function handleTranscription(segments, participant) {
844
+ const identity = participant?.identity ?? "unknown";
845
+ for (const segment of segments) {
846
+ if (!interimRef.current && !segment.final) continue;
847
+ handlerRef.current({
848
+ id: segment.id,
849
+ text: segment.text,
850
+ final: segment.final,
851
+ participantIdentity: identity
852
+ });
853
+ }
854
+ }
855
+ room.on(RoomEvent.TranscriptionReceived, handleTranscription);
856
+ return () => {
857
+ room.off(RoomEvent.TranscriptionReceived, handleTranscription);
858
+ };
859
+ }, [room]);
860
+ }
861
+
862
+ // src/tools.ts
863
+ function clientTool(name, config) {
864
+ return {
865
+ type: "client_event",
866
+ name,
867
+ description: config.description
868
+ };
869
+ }
724
870
 
725
- export { AvatarCall, AvatarSession, AvatarVideo, ControlBar, ScreenShareVideo, UserVideo, useAvatar, useAvatarSession, useAvatarStatus, useLocalMedia };
871
+ export { AvatarCall, AvatarSession, AvatarVideo, ControlBar, ScreenShareVideo, UserVideo, clientTool, useAvatar, useAvatarSession, useAvatarStatus, useClientEvent, useClientEvents, useLocalMedia, useTranscription };
726
872
  //# sourceMappingURL=index.js.map
727
873
  //# sourceMappingURL=index.js.map