@runwayml/avatars-react 0.8.0 → 0.10.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/README.md +132 -3
- 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 +147 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -8
- package/dist/index.d.ts +206 -8
- package/dist/index.js +143 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ The styles use CSS custom properties for easy customization:
|
|
|
56
56
|
|
|
57
57
|
See [`examples/`](./examples) for complete working examples:
|
|
58
58
|
- [`nextjs`](./examples/nextjs) - Next.js App Router
|
|
59
|
+
- [`nextjs-client-events`](./examples/nextjs-client-events) - Client event tools (trivia game)
|
|
59
60
|
- [`nextjs-server-actions`](./examples/nextjs-server-actions) - Next.js with Server Actions
|
|
60
61
|
- [`react-router`](./examples/react-router) - React Router v7 framework mode
|
|
61
62
|
- [`express`](./examples/express) - Express + Vite
|
|
@@ -196,9 +197,96 @@ Style connection states with CSS:
|
|
|
196
197
|
/>
|
|
197
198
|
```
|
|
198
199
|
|
|
200
|
+
## Webcam & Screen Sharing
|
|
201
|
+
|
|
202
|
+
The avatar can see your webcam feed or screen share, enabling visual interactions — show a plant for identification, [hold up a Pokémon card for trivia](https://x.com/technofantasyy/status/2031124673552097412), get [real-time coaching while you play a game](https://x.com/iamneubert/status/2031160102452081046), walk through a presentation, or ask for feedback on a design you're working on.
|
|
203
|
+
|
|
204
|
+
**Compatibility:** Webcam and screen sharing are supported by all preset avatars and custom avatars that use a preset voice. Custom avatars with a custom voice do not support webcam or screen sharing.
|
|
205
|
+
|
|
206
|
+
### Webcam
|
|
207
|
+
|
|
208
|
+
The webcam is enabled by default. The `video` prop controls whether the camera activates on connect, and the `<UserVideo>` component renders the local camera feed:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
<AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect">
|
|
212
|
+
<AvatarVideo />
|
|
213
|
+
<UserVideo />
|
|
214
|
+
<ControlBar />
|
|
215
|
+
</AvatarCall>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
To disable the webcam, set `video={false}`:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect" video={false} />
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Screen Sharing
|
|
225
|
+
|
|
226
|
+
Enable the screen share button by passing `showScreenShare` to `ControlBar`, and use `<ScreenShareVideo>` to display the shared content:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect">
|
|
230
|
+
<AvatarVideo />
|
|
231
|
+
<ScreenShareVideo />
|
|
232
|
+
<ControlBar showScreenShare />
|
|
233
|
+
</AvatarCall>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
You can also start screen sharing automatically by passing a pre-captured stream via `initialScreenStream`. This is useful when you want to prompt the user for screen share permission before the session connects:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
function ScreenShareCall() {
|
|
240
|
+
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
241
|
+
|
|
242
|
+
async function startWithScreenShare() {
|
|
243
|
+
const mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
|
244
|
+
setStream(mediaStream);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!stream) {
|
|
248
|
+
return <button onClick={startWithScreenShare}>Share Screen & Start Call</button>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<AvatarCall
|
|
253
|
+
avatarId="music-superstar"
|
|
254
|
+
connectUrl="/api/avatar/connect"
|
|
255
|
+
initialScreenStream={stream}
|
|
256
|
+
>
|
|
257
|
+
<AvatarVideo />
|
|
258
|
+
<ScreenShareVideo />
|
|
259
|
+
<ControlBar showScreenShare />
|
|
260
|
+
</AvatarCall>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Programmatic Control
|
|
266
|
+
|
|
267
|
+
Use the `useLocalMedia` hook for full programmatic control over camera and screen sharing:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
function MediaControls() {
|
|
271
|
+
const {
|
|
272
|
+
isCameraEnabled,
|
|
273
|
+
isScreenShareEnabled,
|
|
274
|
+
toggleCamera,
|
|
275
|
+
toggleScreenShare,
|
|
276
|
+
} = useLocalMedia();
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div>
|
|
280
|
+
<button onClick={toggleCamera}>{isCameraEnabled ? 'Hide Camera' : 'Show Camera'}</button>
|
|
281
|
+
<button onClick={toggleScreenShare}>{isScreenShareEnabled ? 'Stop Sharing' : 'Share Screen'}</button>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
199
287
|
## Hooks
|
|
200
288
|
|
|
201
|
-
Use hooks for custom components within an `AvatarCall` or `AvatarSession
|
|
289
|
+
Use hooks for custom components within an `AvatarCall` or `AvatarSession`. Also available: `useClientEvent` and `useClientEvents` for [client events](#client-events), and `useTranscription` for real-time transcription.
|
|
202
290
|
|
|
203
291
|
### useAvatarSession
|
|
204
292
|
|
|
@@ -233,13 +321,14 @@ function CustomAvatar() {
|
|
|
233
321
|
|
|
234
322
|
### useLocalMedia
|
|
235
323
|
|
|
236
|
-
Control local camera and
|
|
324
|
+
Control local camera, microphone, and screen sharing:
|
|
237
325
|
|
|
238
326
|
```tsx
|
|
239
327
|
function MediaControls() {
|
|
240
328
|
const {
|
|
241
329
|
isMicEnabled,
|
|
242
330
|
isCameraEnabled,
|
|
331
|
+
isScreenShareEnabled,
|
|
243
332
|
toggleMic,
|
|
244
333
|
toggleCamera,
|
|
245
334
|
toggleScreenShare,
|
|
@@ -249,11 +338,51 @@ function MediaControls() {
|
|
|
249
338
|
<div>
|
|
250
339
|
<button onClick={toggleMic}>{isMicEnabled ? 'Mute' : 'Unmute'}</button>
|
|
251
340
|
<button onClick={toggleCamera}>{isCameraEnabled ? 'Hide' : 'Show'}</button>
|
|
341
|
+
<button onClick={toggleScreenShare}>{isScreenShareEnabled ? 'Stop Sharing' : 'Share Screen'}</button>
|
|
252
342
|
</div>
|
|
253
343
|
);
|
|
254
344
|
}
|
|
255
345
|
```
|
|
256
346
|
|
|
347
|
+
## Client Events
|
|
348
|
+
|
|
349
|
+
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
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// lib/tools.ts — shared between server and client
|
|
353
|
+
import { clientTool, type ClientEventsFrom } from '@runwayml/avatars-react/api';
|
|
354
|
+
|
|
355
|
+
export const showCaption = clientTool('show_caption', {
|
|
356
|
+
description: 'Display a caption overlay',
|
|
357
|
+
args: {} as { text: string },
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
export const tools = [showCaption];
|
|
361
|
+
export type MyEvent = ClientEventsFrom<typeof tools>;
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// Server — pass tools when creating the session
|
|
366
|
+
const { id } = await client.realtimeSessions.create({
|
|
367
|
+
model: 'gwm1_avatars',
|
|
368
|
+
avatar: { type: 'custom', avatarId: '...' },
|
|
369
|
+
tools,
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
// Client — subscribe to events inside AvatarCall
|
|
375
|
+
import { useClientEvent } from '@runwayml/avatars-react';
|
|
376
|
+
import type { MyEvent } from '@/lib/tools';
|
|
377
|
+
|
|
378
|
+
function CaptionOverlay() {
|
|
379
|
+
const caption = useClientEvent<MyEvent, 'show_caption'>('show_caption');
|
|
380
|
+
return caption ? <p>{caption.text}</p> : null;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
See the [`nextjs-client-events`](./examples/nextjs-client-events) example for a full working demo.
|
|
385
|
+
|
|
257
386
|
## Advanced: AvatarSession
|
|
258
387
|
|
|
259
388
|
For full control over session management, use `AvatarSession` directly with pre-fetched credentials:
|
|
@@ -285,7 +414,7 @@ function AdvancedUsage({ credentials }) {
|
|
|
285
414
|
| `AvatarSession` | Low-level wrapper that requires credentials |
|
|
286
415
|
| `AvatarVideo` | Renders the remote avatar video |
|
|
287
416
|
| `UserVideo` | Renders the local user's camera |
|
|
288
|
-
| `ControlBar` | Media control buttons (mic, camera, end call) |
|
|
417
|
+
| `ControlBar` | Media control buttons (mic, camera, screen share, end call) |
|
|
289
418
|
| `ScreenShareVideo` | Renders screen share content |
|
|
290
419
|
| `AudioRenderer` | Handles avatar audio playback |
|
|
291
420
|
|
package/dist/api.cjs
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/tools.ts
|
|
4
|
+
function clientTool(name, config) {
|
|
5
|
+
return {
|
|
6
|
+
type: "client_event",
|
|
7
|
+
name,
|
|
8
|
+
description: config.description
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
// src/api/config.ts
|
|
4
13
|
var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
|
|
5
14
|
|
|
@@ -23,6 +32,7 @@ async function consumeSession(options) {
|
|
|
23
32
|
return response.json();
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
exports.clientTool = clientTool;
|
|
26
36
|
exports.consumeSession = consumeSession;
|
|
27
37
|
//# sourceMappingURL=api.cjs.map
|
|
28
38
|
//# sourceMappingURL=api.cjs.map
|
package/dist/api.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../src/tools.ts","../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";;;AAwDO,SAAS,UAAA,CACd,MACA,MAAA,EAC2B;AAC3B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,cAAA;AAAA,IACN,IAAA;AAAA,IACA,aAAa,MAAA,CAAO;AAAA,GACtB;AACF;;;ACjEO,IAAM,gBAAA,GAAmB,8BAAA;;;ACGhC,eAAsB,eACpB,OAAA,EACiC;AACjC,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,OAAA,GAAU,kBAAiB,GAAI,OAAA;AAE9D,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,sBAAA,EAAyB,SAAS,CAAA,QAAA,CAAA;AACxD,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAChC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,UAAU,UAAU,CAAA;AAAA;AACrC,GACD,CAAA;AAED,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB","file":"api.cjs","sourcesContent":["import type { ClientEvent } from './types';\n\n/**\n * A standalone client tool definition. Composable — combine into arrays\n * and derive event types with `ClientEventsFrom`.\n *\n * At runtime this is just `{ type, name, description }`. The `Args` generic\n * is phantom — it only exists at the TypeScript level for type narrowing.\n */\nexport interface ClientToolDef<Name extends string = string, Args = unknown> {\n readonly type: 'client_event';\n readonly name: Name;\n readonly description: string;\n /** @internal phantom field — always `undefined` at runtime */\n readonly _args?: Args;\n}\n\n/**\n * Derive a discriminated union of ClientEvent types from an array of tools.\n *\n * @example\n * ```typescript\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> =\n T[number] extends infer U\n ? U extends ClientToolDef<infer Name, infer Args>\n ? ClientEvent<Name, Args>\n : never\n : never;\n\n/**\n * Define a single client tool.\n *\n * Returns a standalone object that can be composed into arrays and passed\n * to `realtimeSessions.create({ tools })`.\n *\n * @example\n * ```typescript\n * const showQuestion = clientTool('show_question', {\n * description: 'Display a trivia question',\n * args: {} as { question: string; options: Array<string> },\n * });\n *\n * const playSound = clientTool('play_sound', {\n * description: 'Play a sound effect',\n * args: {} as { sound: 'correct' | 'incorrect' },\n * });\n *\n * // Combine and derive types\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport function clientTool<Name extends string, Args>(\n name: Name,\n config: { description: string; args: Args },\n): ClientToolDef<Name, Args> {\n return {\n type: 'client_event',\n name,\n description: config.description,\n };\n}\n","export const DEFAULT_BASE_URL = 'https://api.dev.runwayml.com';\n","import type { ConsumeSessionOptions, ConsumeSessionResponse } from '../types';\nimport { DEFAULT_BASE_URL } from './config';\n\nexport async function consumeSession(\n options: ConsumeSessionOptions,\n): Promise<ConsumeSessionResponse> {\n const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;\n\n const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${sessionKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to consume session: ${response.status} ${errorText}`,\n );\n }\n\n return response.json();\n}\n"]}
|
package/dist/api.d.cts
CHANGED
|
@@ -20,7 +20,80 @@ interface ConsumeSessionOptions {
|
|
|
20
20
|
/** Optional base URL for the Runway API (defaults to production) */
|
|
21
21
|
baseUrl?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Client event received from the avatar via the data channel.
|
|
25
|
+
* These are fire-and-forget events triggered by the avatar model.
|
|
26
|
+
*
|
|
27
|
+
* @typeParam T - The tool name (defaults to string for untyped usage)
|
|
28
|
+
* @typeParam A - The args type (defaults to Record<string, unknown>)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Untyped usage
|
|
33
|
+
* const event: ClientEvent = { type: 'client_event', tool: 'show_caption', args: { text: 'Hello' } };
|
|
34
|
+
*
|
|
35
|
+
* // Typed usage with discriminated union
|
|
36
|
+
* type MyEvent = ClientEvent<'show_caption', { text: string }>;
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
interface ClientEvent<T extends string = string, A = Record<string, unknown>> {
|
|
40
|
+
type: 'client_event';
|
|
41
|
+
tool: T;
|
|
42
|
+
args: A;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A standalone client tool definition. Composable — combine into arrays
|
|
47
|
+
* and derive event types with `ClientEventsFrom`.
|
|
48
|
+
*
|
|
49
|
+
* At runtime this is just `{ type, name, description }`. The `Args` generic
|
|
50
|
+
* is phantom — it only exists at the TypeScript level for type narrowing.
|
|
51
|
+
*/
|
|
52
|
+
interface ClientToolDef<Name extends string = string, Args = unknown> {
|
|
53
|
+
readonly type: 'client_event';
|
|
54
|
+
readonly name: Name;
|
|
55
|
+
readonly description: string;
|
|
56
|
+
/** @internal phantom field — always `undefined` at runtime */
|
|
57
|
+
readonly _args?: Args;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derive a discriminated union of ClientEvent types from an array of tools.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const tools = [showQuestion, playSound];
|
|
65
|
+
* type MyEvent = ClientEventsFrom<typeof tools>;
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> = T[number] extends infer U ? U extends ClientToolDef<infer Name, infer Args> ? ClientEvent<Name, Args> : never : never;
|
|
69
|
+
/**
|
|
70
|
+
* Define a single client tool.
|
|
71
|
+
*
|
|
72
|
+
* Returns a standalone object that can be composed into arrays and passed
|
|
73
|
+
* to `realtimeSessions.create({ tools })`.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const showQuestion = clientTool('show_question', {
|
|
78
|
+
* description: 'Display a trivia question',
|
|
79
|
+
* args: {} as { question: string; options: Array<string> },
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* const playSound = clientTool('play_sound', {
|
|
83
|
+
* description: 'Play a sound effect',
|
|
84
|
+
* args: {} as { sound: 'correct' | 'incorrect' },
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* // Combine and derive types
|
|
88
|
+
* const tools = [showQuestion, playSound];
|
|
89
|
+
* type MyEvent = ClientEventsFrom<typeof tools>;
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare function clientTool<Name extends string, Args>(name: Name, config: {
|
|
93
|
+
description: string;
|
|
94
|
+
args: Args;
|
|
95
|
+
}): ClientToolDef<Name, Args>;
|
|
23
96
|
|
|
24
97
|
declare function consumeSession(options: ConsumeSessionOptions): Promise<ConsumeSessionResponse>;
|
|
25
98
|
|
|
26
|
-
export { type ConsumeSessionOptions, type ConsumeSessionResponse, consumeSession };
|
|
99
|
+
export { type ClientEvent, type ClientEventsFrom, type ClientToolDef, type ConsumeSessionOptions, type ConsumeSessionResponse, clientTool, consumeSession };
|
package/dist/api.d.ts
CHANGED
|
@@ -20,7 +20,80 @@ interface ConsumeSessionOptions {
|
|
|
20
20
|
/** Optional base URL for the Runway API (defaults to production) */
|
|
21
21
|
baseUrl?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Client event received from the avatar via the data channel.
|
|
25
|
+
* These are fire-and-forget events triggered by the avatar model.
|
|
26
|
+
*
|
|
27
|
+
* @typeParam T - The tool name (defaults to string for untyped usage)
|
|
28
|
+
* @typeParam A - The args type (defaults to Record<string, unknown>)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Untyped usage
|
|
33
|
+
* const event: ClientEvent = { type: 'client_event', tool: 'show_caption', args: { text: 'Hello' } };
|
|
34
|
+
*
|
|
35
|
+
* // Typed usage with discriminated union
|
|
36
|
+
* type MyEvent = ClientEvent<'show_caption', { text: string }>;
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
interface ClientEvent<T extends string = string, A = Record<string, unknown>> {
|
|
40
|
+
type: 'client_event';
|
|
41
|
+
tool: T;
|
|
42
|
+
args: A;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A standalone client tool definition. Composable — combine into arrays
|
|
47
|
+
* and derive event types with `ClientEventsFrom`.
|
|
48
|
+
*
|
|
49
|
+
* At runtime this is just `{ type, name, description }`. The `Args` generic
|
|
50
|
+
* is phantom — it only exists at the TypeScript level for type narrowing.
|
|
51
|
+
*/
|
|
52
|
+
interface ClientToolDef<Name extends string = string, Args = unknown> {
|
|
53
|
+
readonly type: 'client_event';
|
|
54
|
+
readonly name: Name;
|
|
55
|
+
readonly description: string;
|
|
56
|
+
/** @internal phantom field — always `undefined` at runtime */
|
|
57
|
+
readonly _args?: Args;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derive a discriminated union of ClientEvent types from an array of tools.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const tools = [showQuestion, playSound];
|
|
65
|
+
* type MyEvent = ClientEventsFrom<typeof tools>;
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> = T[number] extends infer U ? U extends ClientToolDef<infer Name, infer Args> ? ClientEvent<Name, Args> : never : never;
|
|
69
|
+
/**
|
|
70
|
+
* Define a single client tool.
|
|
71
|
+
*
|
|
72
|
+
* Returns a standalone object that can be composed into arrays and passed
|
|
73
|
+
* to `realtimeSessions.create({ tools })`.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const showQuestion = clientTool('show_question', {
|
|
78
|
+
* description: 'Display a trivia question',
|
|
79
|
+
* args: {} as { question: string; options: Array<string> },
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* const playSound = clientTool('play_sound', {
|
|
83
|
+
* description: 'Play a sound effect',
|
|
84
|
+
* args: {} as { sound: 'correct' | 'incorrect' },
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* // Combine and derive types
|
|
88
|
+
* const tools = [showQuestion, playSound];
|
|
89
|
+
* type MyEvent = ClientEventsFrom<typeof tools>;
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare function clientTool<Name extends string, Args>(name: Name, config: {
|
|
93
|
+
description: string;
|
|
94
|
+
args: Args;
|
|
95
|
+
}): ClientToolDef<Name, Args>;
|
|
23
96
|
|
|
24
97
|
declare function consumeSession(options: ConsumeSessionOptions): Promise<ConsumeSessionResponse>;
|
|
25
98
|
|
|
26
|
-
export { type ConsumeSessionOptions, type ConsumeSessionResponse, consumeSession };
|
|
99
|
+
export { type ClientEvent, type ClientEventsFrom, type ClientToolDef, type ConsumeSessionOptions, type ConsumeSessionResponse, clientTool, consumeSession };
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// src/tools.ts
|
|
2
|
+
function clientTool(name, config) {
|
|
3
|
+
return {
|
|
4
|
+
type: "client_event",
|
|
5
|
+
name,
|
|
6
|
+
description: config.description
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
// src/api/config.ts
|
|
2
11
|
var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
|
|
3
12
|
|
|
@@ -21,6 +30,6 @@ async function consumeSession(options) {
|
|
|
21
30
|
return response.json();
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
export { consumeSession };
|
|
33
|
+
export { clientTool, consumeSession };
|
|
25
34
|
//# sourceMappingURL=api.js.map
|
|
26
35
|
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/tools.ts","../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";AAwDO,SAAS,UAAA,CACd,MACA,MAAA,EAC2B;AAC3B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,cAAA;AAAA,IACN,IAAA;AAAA,IACA,aAAa,MAAA,CAAO;AAAA,GACtB;AACF;;;ACjEO,IAAM,gBAAA,GAAmB,8BAAA;;;ACGhC,eAAsB,eACpB,OAAA,EACiC;AACjC,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,OAAA,GAAU,kBAAiB,GAAI,OAAA;AAE9D,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,sBAAA,EAAyB,SAAS,CAAA,QAAA,CAAA;AACxD,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAChC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,UAAU,UAAU,CAAA;AAAA;AACrC,GACD,CAAA;AAED,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB","file":"api.js","sourcesContent":["import type { ClientEvent } from './types';\n\n/**\n * A standalone client tool definition. Composable — combine into arrays\n * and derive event types with `ClientEventsFrom`.\n *\n * At runtime this is just `{ type, name, description }`. The `Args` generic\n * is phantom — it only exists at the TypeScript level for type narrowing.\n */\nexport interface ClientToolDef<Name extends string = string, Args = unknown> {\n readonly type: 'client_event';\n readonly name: Name;\n readonly description: string;\n /** @internal phantom field — always `undefined` at runtime */\n readonly _args?: Args;\n}\n\n/**\n * Derive a discriminated union of ClientEvent types from an array of tools.\n *\n * @example\n * ```typescript\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> =\n T[number] extends infer U\n ? U extends ClientToolDef<infer Name, infer Args>\n ? ClientEvent<Name, Args>\n : never\n : never;\n\n/**\n * Define a single client tool.\n *\n * Returns a standalone object that can be composed into arrays and passed\n * to `realtimeSessions.create({ tools })`.\n *\n * @example\n * ```typescript\n * const showQuestion = clientTool('show_question', {\n * description: 'Display a trivia question',\n * args: {} as { question: string; options: Array<string> },\n * });\n *\n * const playSound = clientTool('play_sound', {\n * description: 'Play a sound effect',\n * args: {} as { sound: 'correct' | 'incorrect' },\n * });\n *\n * // Combine and derive types\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport function clientTool<Name extends string, Args>(\n name: Name,\n config: { description: string; args: Args },\n): ClientToolDef<Name, Args> {\n return {\n type: 'client_event',\n name,\n description: config.description,\n };\n}\n","export const DEFAULT_BASE_URL = 'https://api.dev.runwayml.com';\n","import type { ConsumeSessionOptions, ConsumeSessionResponse } from '../types';\nimport { DEFAULT_BASE_URL } from './config';\n\nexport async function consumeSession(\n options: ConsumeSessionOptions,\n): Promise<ConsumeSessionResponse> {\n const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;\n\n const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${sessionKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to consume session: ${response.status} ${errorText}`,\n );\n }\n\n return response.json();\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -154,6 +154,22 @@ function useLatest(value) {
|
|
|
154
154
|
}, [value]);
|
|
155
155
|
return ref;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
// src/utils/parseClientEvent.ts
|
|
159
|
+
function isAckMessage(args) {
|
|
160
|
+
return "status" in args && args.status === "event_sent";
|
|
161
|
+
}
|
|
162
|
+
function parseClientEvent(payload) {
|
|
163
|
+
try {
|
|
164
|
+
const message = JSON.parse(new TextDecoder().decode(payload));
|
|
165
|
+
if (message?.type === "client_event" && typeof message.tool === "string" && message.args != null && typeof message.args === "object" && !isAckMessage(message.args)) {
|
|
166
|
+
return message;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
157
173
|
async function hasMediaDevice(kind, timeoutMs = 1e3) {
|
|
158
174
|
try {
|
|
159
175
|
const timeoutPromise = new Promise(
|
|
@@ -229,6 +245,8 @@ function AvatarSession({
|
|
|
229
245
|
video: requestVideo = true,
|
|
230
246
|
onEnd,
|
|
231
247
|
onError,
|
|
248
|
+
onClientEvent,
|
|
249
|
+
initialScreenStream,
|
|
232
250
|
__unstable_roomOptions
|
|
233
251
|
}) {
|
|
234
252
|
const errorRef = react.useRef(null);
|
|
@@ -263,7 +281,9 @@ function AvatarSession({
|
|
|
263
281
|
{
|
|
264
282
|
sessionId: credentials.sessionId,
|
|
265
283
|
onEnd,
|
|
284
|
+
onClientEvent,
|
|
266
285
|
errorRef,
|
|
286
|
+
initialScreenStream,
|
|
267
287
|
children
|
|
268
288
|
}
|
|
269
289
|
),
|
|
@@ -275,13 +295,52 @@ function AvatarSession({
|
|
|
275
295
|
function AvatarSessionContextInner({
|
|
276
296
|
sessionId,
|
|
277
297
|
onEnd,
|
|
298
|
+
onClientEvent,
|
|
278
299
|
errorRef,
|
|
300
|
+
initialScreenStream,
|
|
279
301
|
children
|
|
280
302
|
}) {
|
|
281
303
|
const room = componentsReact.useRoomContext();
|
|
282
304
|
const connectionState = componentsReact.useConnectionState();
|
|
283
305
|
const onEndRef = react.useRef(onEnd);
|
|
284
306
|
onEndRef.current = onEnd;
|
|
307
|
+
const onClientEventRef = react.useRef(onClientEvent);
|
|
308
|
+
onClientEventRef.current = onClientEvent;
|
|
309
|
+
const publishedRef = react.useRef(false);
|
|
310
|
+
react.useEffect(() => {
|
|
311
|
+
if (connectionState !== livekitClient.ConnectionState.Connected) return;
|
|
312
|
+
if (!initialScreenStream || publishedRef.current) return;
|
|
313
|
+
publishedRef.current = true;
|
|
314
|
+
const videoTrack = initialScreenStream.getVideoTracks()[0];
|
|
315
|
+
if (videoTrack) {
|
|
316
|
+
room.localParticipant.publishTrack(videoTrack, {
|
|
317
|
+
source: livekitClient.Track.Source.ScreenShare
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const audioTrack = initialScreenStream.getAudioTracks()[0];
|
|
321
|
+
if (audioTrack) {
|
|
322
|
+
room.localParticipant.publishTrack(audioTrack, {
|
|
323
|
+
source: livekitClient.Track.Source.ScreenShareAudio
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return () => {
|
|
327
|
+
initialScreenStream.getTracks().forEach((t) => {
|
|
328
|
+
t.stop();
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
}, [connectionState, initialScreenStream, room]);
|
|
332
|
+
react.useEffect(() => {
|
|
333
|
+
function handleDataReceived(payload) {
|
|
334
|
+
const event = parseClientEvent(payload);
|
|
335
|
+
if (event) {
|
|
336
|
+
onClientEventRef.current?.(event);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
room.on(livekitClient.RoomEvent.DataReceived, handleDataReceived);
|
|
340
|
+
return () => {
|
|
341
|
+
room.off(livekitClient.RoomEvent.DataReceived, handleDataReceived);
|
|
342
|
+
};
|
|
343
|
+
}, [room]);
|
|
285
344
|
const end = react.useCallback(async () => {
|
|
286
345
|
try {
|
|
287
346
|
const encoder = new TextEncoder();
|
|
@@ -344,7 +403,10 @@ function useAvatarStatus() {
|
|
|
344
403
|
return { status: "connecting" };
|
|
345
404
|
case "active":
|
|
346
405
|
if (hasVideo && videoTrackRef) {
|
|
347
|
-
return {
|
|
406
|
+
return {
|
|
407
|
+
status: "ready",
|
|
408
|
+
videoTrackRef
|
|
409
|
+
};
|
|
348
410
|
}
|
|
349
411
|
return { status: "waiting" };
|
|
350
412
|
case "ending":
|
|
@@ -609,7 +671,9 @@ function AvatarCall({
|
|
|
609
671
|
avatarImageUrl,
|
|
610
672
|
onEnd,
|
|
611
673
|
onError,
|
|
674
|
+
onClientEvent,
|
|
612
675
|
children,
|
|
676
|
+
initialScreenStream,
|
|
613
677
|
__unstable_roomOptions,
|
|
614
678
|
...props
|
|
615
679
|
}) {
|
|
@@ -661,6 +725,8 @@ function AvatarCall({
|
|
|
661
725
|
video,
|
|
662
726
|
onEnd,
|
|
663
727
|
onError: handleSessionError,
|
|
728
|
+
onClientEvent,
|
|
729
|
+
initialScreenStream,
|
|
664
730
|
__unstable_roomOptions,
|
|
665
731
|
children: children ?? defaultChildren
|
|
666
732
|
}
|
|
@@ -694,6 +760,78 @@ function ScreenShareVideo({
|
|
|
694
760
|
}
|
|
695
761
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ...props, "data-avatar-screen-share": "", "data-avatar-sharing": isSharing, children: screenShareTrackRef && componentsReact.isTrackReference(screenShareTrackRef) && /* @__PURE__ */ jsxRuntime.jsx(componentsReact.VideoTrack, { trackRef: screenShareTrackRef }) });
|
|
696
762
|
}
|
|
763
|
+
function useClientEvent(toolName, onEvent) {
|
|
764
|
+
const room = componentsReact.useRoomContext();
|
|
765
|
+
const [state, setState] = react.useState(null);
|
|
766
|
+
const onEventRef = react.useRef(onEvent);
|
|
767
|
+
onEventRef.current = onEvent;
|
|
768
|
+
react.useEffect(() => {
|
|
769
|
+
function handleDataReceived(payload) {
|
|
770
|
+
const event = parseClientEvent(payload);
|
|
771
|
+
if (event && event.tool === toolName) {
|
|
772
|
+
const args = event.args;
|
|
773
|
+
setState(args);
|
|
774
|
+
onEventRef.current?.(args);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
room.on(livekitClient.RoomEvent.DataReceived, handleDataReceived);
|
|
778
|
+
return () => {
|
|
779
|
+
room.off(livekitClient.RoomEvent.DataReceived, handleDataReceived);
|
|
780
|
+
};
|
|
781
|
+
}, [room, toolName]);
|
|
782
|
+
return state;
|
|
783
|
+
}
|
|
784
|
+
function useClientEvents(handler) {
|
|
785
|
+
const room = componentsReact.useRoomContext();
|
|
786
|
+
const handlerRef = react.useRef(handler);
|
|
787
|
+
handlerRef.current = handler;
|
|
788
|
+
react.useEffect(() => {
|
|
789
|
+
function handleDataReceived(payload) {
|
|
790
|
+
const event = parseClientEvent(payload);
|
|
791
|
+
if (event) {
|
|
792
|
+
handlerRef.current(event);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
room.on(livekitClient.RoomEvent.DataReceived, handleDataReceived);
|
|
796
|
+
return () => {
|
|
797
|
+
room.off(livekitClient.RoomEvent.DataReceived, handleDataReceived);
|
|
798
|
+
};
|
|
799
|
+
}, [room]);
|
|
800
|
+
}
|
|
801
|
+
function useTranscription(handler, options) {
|
|
802
|
+
const room = componentsReact.useRoomContext();
|
|
803
|
+
const handlerRef = react.useRef(handler);
|
|
804
|
+
handlerRef.current = handler;
|
|
805
|
+
const interimRef = react.useRef(options?.interim ?? false);
|
|
806
|
+
interimRef.current = options?.interim ?? false;
|
|
807
|
+
react.useEffect(() => {
|
|
808
|
+
function handleTranscription(segments, participant) {
|
|
809
|
+
const identity = participant?.identity ?? "unknown";
|
|
810
|
+
for (const segment of segments) {
|
|
811
|
+
if (!interimRef.current && !segment.final) continue;
|
|
812
|
+
handlerRef.current({
|
|
813
|
+
id: segment.id,
|
|
814
|
+
text: segment.text,
|
|
815
|
+
final: segment.final,
|
|
816
|
+
participantIdentity: identity
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
room.on(livekitClient.RoomEvent.TranscriptionReceived, handleTranscription);
|
|
821
|
+
return () => {
|
|
822
|
+
room.off(livekitClient.RoomEvent.TranscriptionReceived, handleTranscription);
|
|
823
|
+
};
|
|
824
|
+
}, [room]);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/tools.ts
|
|
828
|
+
function clientTool(name, config) {
|
|
829
|
+
return {
|
|
830
|
+
type: "client_event",
|
|
831
|
+
name,
|
|
832
|
+
description: config.description
|
|
833
|
+
};
|
|
834
|
+
}
|
|
697
835
|
|
|
698
836
|
Object.defineProperty(exports, "AudioRenderer", {
|
|
699
837
|
enumerable: true,
|
|
@@ -703,15 +841,23 @@ Object.defineProperty(exports, "VideoTrack", {
|
|
|
703
841
|
enumerable: true,
|
|
704
842
|
get: function () { return componentsReact.VideoTrack; }
|
|
705
843
|
});
|
|
844
|
+
Object.defineProperty(exports, "isTrackReference", {
|
|
845
|
+
enumerable: true,
|
|
846
|
+
get: function () { return componentsReact.isTrackReference; }
|
|
847
|
+
});
|
|
706
848
|
exports.AvatarCall = AvatarCall;
|
|
707
849
|
exports.AvatarSession = AvatarSession;
|
|
708
850
|
exports.AvatarVideo = AvatarVideo;
|
|
709
851
|
exports.ControlBar = ControlBar;
|
|
710
852
|
exports.ScreenShareVideo = ScreenShareVideo;
|
|
711
853
|
exports.UserVideo = UserVideo;
|
|
854
|
+
exports.clientTool = clientTool;
|
|
712
855
|
exports.useAvatar = useAvatar;
|
|
713
856
|
exports.useAvatarSession = useAvatarSession;
|
|
714
857
|
exports.useAvatarStatus = useAvatarStatus;
|
|
858
|
+
exports.useClientEvent = useClientEvent;
|
|
859
|
+
exports.useClientEvents = useClientEvents;
|
|
715
860
|
exports.useLocalMedia = useLocalMedia;
|
|
861
|
+
exports.useTranscription = useTranscription;
|
|
716
862
|
//# sourceMappingURL=index.cjs.map
|
|
717
863
|
//# sourceMappingURL=index.cjs.map
|