@runwayml/avatars-react 0.10.0-beta.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 +20 -13
- package/dist/index.cjs +93 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +94 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,8 @@ The styles use CSS custom properties for easy customization:
|
|
|
57
57
|
See [`examples/`](./examples) for complete working examples:
|
|
58
58
|
- [`nextjs`](./examples/nextjs) - Next.js App Router
|
|
59
59
|
- [`nextjs-client-events`](./examples/nextjs-client-events) - Client event tools (trivia game)
|
|
60
|
+
- [`nextjs-rpc`](./examples/nextjs-rpc) - Backend RPC + client events (trivia with server-side questions)
|
|
61
|
+
- [`nextjs-rpc-weather`](./examples/nextjs-rpc-weather) - Backend RPC only (weather assistant)
|
|
60
62
|
- [`nextjs-server-actions`](./examples/nextjs-server-actions) - Next.js with Server Actions
|
|
61
63
|
- [`react-router`](./examples/react-router) - React Router v7 framework mode
|
|
62
64
|
- [`express`](./examples/express) - Express + Vite
|
|
@@ -151,38 +153,41 @@ import { AvatarCall, AvatarVideo, ControlBar, UserVideo } from '@runwayml/avatar
|
|
|
151
153
|
|
|
152
154
|
### Render Props
|
|
153
155
|
|
|
154
|
-
All components support render props for complete control
|
|
156
|
+
All display components support render props for complete control. `AvatarVideo` receives a discriminated union with `status`:
|
|
155
157
|
|
|
156
158
|
```tsx
|
|
157
159
|
<AvatarVideo>
|
|
158
|
-
{(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
{(avatar) => {
|
|
161
|
+
switch (avatar.status) {
|
|
162
|
+
case 'connecting': return <Spinner />;
|
|
163
|
+
case 'waiting': return <Placeholder />;
|
|
164
|
+
case 'ready': return <VideoTrack trackRef={avatar.videoTrackRef} />;
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
164
167
|
</AvatarVideo>
|
|
165
168
|
```
|
|
166
169
|
|
|
167
170
|
### CSS Styling with Data Attributes
|
|
168
171
|
|
|
169
|
-
Style
|
|
172
|
+
Style components with the namespaced `data-avatar-*` attributes:
|
|
170
173
|
|
|
171
174
|
```tsx
|
|
172
175
|
<AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect" className="my-avatar" />
|
|
173
176
|
```
|
|
174
177
|
|
|
175
178
|
```css
|
|
176
|
-
|
|
179
|
+
/* Style avatar video by connection status */
|
|
180
|
+
[data-avatar-video][data-avatar-status="connecting"] {
|
|
177
181
|
opacity: 0.5;
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
[data-avatar-video][data-avatar-status="ready"] {
|
|
185
|
+
opacity: 1;
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
/* Style control buttons */
|
|
189
|
+
[data-avatar-control][data-avatar-enabled="false"] {
|
|
190
|
+
opacity: 0.5;
|
|
186
191
|
}
|
|
187
192
|
```
|
|
188
193
|
|
|
@@ -346,6 +351,8 @@ function MediaControls() {
|
|
|
346
351
|
|
|
347
352
|
## Client Events
|
|
348
353
|
|
|
354
|
+
> **Compatibility:** Client events (tool calling) are supported on avatars that use a **preset voice**. Custom voice avatars do not currently support client events.
|
|
355
|
+
|
|
349
356
|
Avatars can trigger UI events via tool calls sent over the data channel. Define tools, pass them when creating a session, and subscribe on the client:
|
|
350
357
|
|
|
351
358
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -170,53 +170,6 @@ function parseClientEvent(payload) {
|
|
|
170
170
|
return null;
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
async function hasMediaDevice(kind, timeoutMs = 1e3) {
|
|
174
|
-
try {
|
|
175
|
-
const timeoutPromise = new Promise(
|
|
176
|
-
(resolve) => setTimeout(() => resolve(false), timeoutMs)
|
|
177
|
-
);
|
|
178
|
-
const checkPromise = navigator.mediaDevices.enumerateDevices().then((devices) => devices.some((device) => device.kind === kind));
|
|
179
|
-
return await Promise.race([checkPromise, timeoutPromise]);
|
|
180
|
-
} catch {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
function useDeviceAvailability(requestAudio, requestVideo) {
|
|
185
|
-
const [state, setState] = react.useState({
|
|
186
|
-
audio: requestAudio,
|
|
187
|
-
// Optimistically assume devices exist
|
|
188
|
-
video: requestVideo
|
|
189
|
-
});
|
|
190
|
-
react.useEffect(() => {
|
|
191
|
-
let cancelled = false;
|
|
192
|
-
async function checkDevices() {
|
|
193
|
-
const [hasAudio, hasVideo] = await Promise.all([
|
|
194
|
-
requestAudio ? hasMediaDevice("audioinput") : Promise.resolve(false),
|
|
195
|
-
requestVideo ? hasMediaDevice("videoinput") : Promise.resolve(false)
|
|
196
|
-
]);
|
|
197
|
-
if (!cancelled) {
|
|
198
|
-
setState({
|
|
199
|
-
audio: requestAudio && hasAudio,
|
|
200
|
-
video: requestVideo && hasVideo
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
checkDevices();
|
|
205
|
-
return () => {
|
|
206
|
-
cancelled = true;
|
|
207
|
-
};
|
|
208
|
-
}, [requestAudio, requestVideo]);
|
|
209
|
-
return state;
|
|
210
|
-
}
|
|
211
|
-
var MEDIA_DEVICE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
212
|
-
"NotAllowedError",
|
|
213
|
-
"NotFoundError",
|
|
214
|
-
"NotReadableError",
|
|
215
|
-
"OverconstrainedError"
|
|
216
|
-
]);
|
|
217
|
-
function isMediaDeviceError(error) {
|
|
218
|
-
return MEDIA_DEVICE_ERROR_NAMES.has(error.name);
|
|
219
|
-
}
|
|
220
173
|
var DEFAULT_ROOM_OPTIONS = {
|
|
221
174
|
adaptiveStream: false,
|
|
222
175
|
dynacast: false
|
|
@@ -238,6 +191,7 @@ function mapConnectionState(connectionState) {
|
|
|
238
191
|
var AvatarSessionContext = react.createContext(
|
|
239
192
|
null
|
|
240
193
|
);
|
|
194
|
+
var MediaDeviceErrorContext = react.createContext(null);
|
|
241
195
|
function AvatarSession({
|
|
242
196
|
credentials,
|
|
243
197
|
children,
|
|
@@ -250,12 +204,9 @@ function AvatarSession({
|
|
|
250
204
|
__unstable_roomOptions
|
|
251
205
|
}) {
|
|
252
206
|
const errorRef = react.useRef(null);
|
|
253
|
-
const deviceAvailability = useDeviceAvailability(requestAudio, requestVideo);
|
|
254
207
|
const handleError = (error) => {
|
|
255
208
|
onError?.(error);
|
|
256
|
-
|
|
257
|
-
errorRef.current = error;
|
|
258
|
-
}
|
|
209
|
+
errorRef.current = error;
|
|
259
210
|
};
|
|
260
211
|
const roomOptions = {
|
|
261
212
|
...DEFAULT_ROOM_OPTIONS,
|
|
@@ -267,8 +218,8 @@ function AvatarSession({
|
|
|
267
218
|
serverUrl: credentials.serverUrl,
|
|
268
219
|
token: credentials.token,
|
|
269
220
|
connect: true,
|
|
270
|
-
audio:
|
|
271
|
-
video:
|
|
221
|
+
audio: false,
|
|
222
|
+
video: false,
|
|
272
223
|
onDisconnected: () => onEnd?.(),
|
|
273
224
|
onError: handleError,
|
|
274
225
|
options: roomOptions,
|
|
@@ -280,6 +231,8 @@ function AvatarSession({
|
|
|
280
231
|
AvatarSessionContextInner,
|
|
281
232
|
{
|
|
282
233
|
sessionId: credentials.sessionId,
|
|
234
|
+
requestAudio,
|
|
235
|
+
requestVideo,
|
|
283
236
|
onEnd,
|
|
284
237
|
onClientEvent,
|
|
285
238
|
errorRef,
|
|
@@ -294,6 +247,8 @@ function AvatarSession({
|
|
|
294
247
|
}
|
|
295
248
|
function AvatarSessionContextInner({
|
|
296
249
|
sessionId,
|
|
250
|
+
requestAudio,
|
|
251
|
+
requestVideo,
|
|
297
252
|
onEnd,
|
|
298
253
|
onClientEvent,
|
|
299
254
|
errorRef,
|
|
@@ -329,6 +284,64 @@ function AvatarSessionContextInner({
|
|
|
329
284
|
});
|
|
330
285
|
};
|
|
331
286
|
}, [connectionState, initialScreenStream, room]);
|
|
287
|
+
const [micError, setMicError] = react.useState(null);
|
|
288
|
+
const [cameraError, setCameraError] = react.useState(null);
|
|
289
|
+
const mediaEnabledRef = react.useRef(false);
|
|
290
|
+
react.useEffect(() => {
|
|
291
|
+
if (connectionState !== livekitClient.ConnectionState.Connected) return;
|
|
292
|
+
if (mediaEnabledRef.current) return;
|
|
293
|
+
mediaEnabledRef.current = true;
|
|
294
|
+
async function enableMedia() {
|
|
295
|
+
if (requestAudio) {
|
|
296
|
+
try {
|
|
297
|
+
await room.localParticipant.setMicrophoneEnabled(true);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
if (err instanceof Error) setMicError(err);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (requestVideo) {
|
|
303
|
+
try {
|
|
304
|
+
await room.localParticipant.setCameraEnabled(true);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err instanceof Error) setCameraError(err);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
enableMedia();
|
|
311
|
+
}, [connectionState, room, requestAudio, requestVideo]);
|
|
312
|
+
react.useEffect(() => {
|
|
313
|
+
function handleMediaDevicesError(error, kind) {
|
|
314
|
+
if (kind === "audioinput") {
|
|
315
|
+
setMicError(error);
|
|
316
|
+
} else if (kind === "videoinput") {
|
|
317
|
+
setCameraError(error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
room.on(livekitClient.RoomEvent.MediaDevicesError, handleMediaDevicesError);
|
|
321
|
+
return () => {
|
|
322
|
+
room.off(livekitClient.RoomEvent.MediaDevicesError, handleMediaDevicesError);
|
|
323
|
+
};
|
|
324
|
+
}, [room]);
|
|
325
|
+
const retryMic = react.useCallback(async () => {
|
|
326
|
+
try {
|
|
327
|
+
await room.localParticipant.setMicrophoneEnabled(true);
|
|
328
|
+
setMicError(null);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (err instanceof Error) setMicError(err);
|
|
331
|
+
}
|
|
332
|
+
}, [room]);
|
|
333
|
+
const retryCamera = react.useCallback(async () => {
|
|
334
|
+
try {
|
|
335
|
+
await room.localParticipant.setCameraEnabled(true);
|
|
336
|
+
setCameraError(null);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err instanceof Error) setCameraError(err);
|
|
339
|
+
}
|
|
340
|
+
}, [room]);
|
|
341
|
+
const mediaDeviceErrors = react.useMemo(
|
|
342
|
+
() => ({ micError, cameraError, retryMic, retryCamera }),
|
|
343
|
+
[micError, cameraError, retryMic, retryCamera]
|
|
344
|
+
);
|
|
332
345
|
react.useEffect(() => {
|
|
333
346
|
function handleDataReceived(payload) {
|
|
334
347
|
const event = parseClientEvent(payload);
|
|
@@ -357,7 +370,7 @@ function AvatarSessionContextInner({
|
|
|
357
370
|
error: errorRef.current,
|
|
358
371
|
end
|
|
359
372
|
};
|
|
360
|
-
return /* @__PURE__ */ jsxRuntime.jsx(AvatarSessionContext.Provider, { value: contextValue, children });
|
|
373
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AvatarSessionContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(MediaDeviceErrorContext.Provider, { value: mediaDeviceErrors, children }) });
|
|
361
374
|
}
|
|
362
375
|
function useAvatarSessionContext() {
|
|
363
376
|
const context = react.useContext(AvatarSessionContext);
|
|
@@ -368,6 +381,9 @@ function useAvatarSessionContext() {
|
|
|
368
381
|
}
|
|
369
382
|
return context;
|
|
370
383
|
}
|
|
384
|
+
function useMediaDeviceErrorContext() {
|
|
385
|
+
return react.useContext(MediaDeviceErrorContext);
|
|
386
|
+
}
|
|
371
387
|
function useAvatar() {
|
|
372
388
|
const remoteParticipants = componentsReact.useRemoteParticipants();
|
|
373
389
|
const avatarParticipant = remoteParticipants[0] ?? null;
|
|
@@ -433,8 +449,16 @@ function AvatarVideo({ children, ...props }) {
|
|
|
433
449
|
}
|
|
434
450
|
);
|
|
435
451
|
}
|
|
452
|
+
var NOOP_ASYNC = async () => {
|
|
453
|
+
};
|
|
436
454
|
function useLocalMedia() {
|
|
437
455
|
const { localParticipant } = componentsReact.useLocalParticipant();
|
|
456
|
+
const {
|
|
457
|
+
micError = null,
|
|
458
|
+
cameraError = null,
|
|
459
|
+
retryMic = NOOP_ASYNC,
|
|
460
|
+
retryCamera = NOOP_ASYNC
|
|
461
|
+
} = useMediaDeviceErrorContext() ?? {};
|
|
438
462
|
const audioDevices = componentsReact.useMediaDevices({ kind: "audioinput" });
|
|
439
463
|
const videoDevices = componentsReact.useMediaDevices({ kind: "videoinput" });
|
|
440
464
|
const hasMic = audioDevices?.length > 0;
|
|
@@ -480,7 +504,11 @@ function useLocalMedia() {
|
|
|
480
504
|
toggleMic,
|
|
481
505
|
toggleCamera,
|
|
482
506
|
toggleScreenShare,
|
|
483
|
-
localVideoTrackRef
|
|
507
|
+
localVideoTrackRef,
|
|
508
|
+
micError,
|
|
509
|
+
cameraError,
|
|
510
|
+
retryMic,
|
|
511
|
+
retryCamera
|
|
484
512
|
};
|
|
485
513
|
}
|
|
486
514
|
function ControlBar({
|
|
@@ -498,7 +526,11 @@ function ControlBar({
|
|
|
498
526
|
isScreenShareEnabled,
|
|
499
527
|
toggleMic,
|
|
500
528
|
toggleCamera,
|
|
501
|
-
toggleScreenShare
|
|
529
|
+
toggleScreenShare,
|
|
530
|
+
micError,
|
|
531
|
+
cameraError,
|
|
532
|
+
retryMic,
|
|
533
|
+
retryCamera
|
|
502
534
|
} = useLocalMedia();
|
|
503
535
|
const isActive = session.state === "active";
|
|
504
536
|
const state = {
|
|
@@ -509,7 +541,11 @@ function ControlBar({
|
|
|
509
541
|
toggleCamera,
|
|
510
542
|
toggleScreenShare,
|
|
511
543
|
endCall: session.end,
|
|
512
|
-
isActive
|
|
544
|
+
isActive,
|
|
545
|
+
micError,
|
|
546
|
+
cameraError,
|
|
547
|
+
retryMic,
|
|
548
|
+
retryCamera
|
|
513
549
|
};
|
|
514
550
|
if (children) {
|
|
515
551
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children(state) });
|