@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 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 microphone:
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":";;;AAAO,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":["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"]}
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":";AAAO,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":["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"]}
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 { status: "ready", videoTrackRef };
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