@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/README.md +152 -16
- package/dist/api.cjs +10 -0
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +74 -1
- package/dist/api.d.ts +74 -1
- package/dist/api.js +10 -1
- package/dist/api.js.map +1 -1
- package/dist/index.cjs +208 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +215 -9
- package/dist/index.d.ts +215 -9
- package/dist/index.js +204 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return
|
|
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
|
|
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
|
-
|
|
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:
|
|
253
|
-
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 {
|
|
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
|