@runwayml/avatars-react 0.2.2 → 0.5.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/dist/index.js CHANGED
@@ -1,30 +1,15 @@
1
1
  import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices, TrackToggle } from '@livekit/components-react';
2
- export { RoomAudioRenderer as AudioRenderer } from '@livekit/components-react';
3
- import { createContext, useRef, useCallback, useContext, useEffect, useReducer } from 'react';
2
+ export { RoomAudioRenderer as AudioRenderer, VideoTrack } from '@livekit/components-react';
3
+ import { createContext, useRef, useCallback, useContext, useEffect } from 'react';
4
4
  import { Track, ConnectionState } from 'livekit-client';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/api/config.ts
8
8
  var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
9
- function getBaseUrl() {
10
- try {
11
- const envUrl = process.env.RUNWAYML_BASE_URL;
12
- if (envUrl) return envUrl;
13
- } catch {
14
- }
15
- return DEFAULT_BASE_URL;
16
- }
17
- var config = null;
18
- function getConfig() {
19
- if (!config) {
20
- config = { baseUrl: getBaseUrl() };
21
- }
22
- return config;
23
- }
24
9
 
25
10
  // src/api/consume.ts
26
11
  async function consumeSession(options) {
27
- const { sessionId, sessionKey, baseUrl = getConfig().baseUrl } = options;
12
+ const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;
28
13
  const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;
29
14
  const response = await fetch(url, {
30
15
  method: "POST",
@@ -42,92 +27,88 @@ async function consumeSession(options) {
42
27
  return response.json();
43
28
  }
44
29
 
30
+ // src/utils/suspense-resource.ts
31
+ function createSuspenseResource(promise) {
32
+ let status = "pending";
33
+ let result;
34
+ let error;
35
+ const suspender = promise.then(
36
+ (value) => {
37
+ status = "fulfilled";
38
+ result = value;
39
+ },
40
+ (err) => {
41
+ status = "rejected";
42
+ error = err;
43
+ }
44
+ );
45
+ return {
46
+ read() {
47
+ switch (status) {
48
+ case "pending":
49
+ throw suspender;
50
+ case "rejected":
51
+ throw error;
52
+ case "fulfilled":
53
+ return result;
54
+ }
55
+ }
56
+ };
57
+ }
58
+
45
59
  // src/hooks/useCredentials.ts
46
- function credentialsReducer(_state, action) {
47
- switch (action.type) {
48
- case "CONNECT":
49
- return { status: "connecting", credentials: null, error: null };
50
- case "CONNECTED":
51
- return {
52
- status: "connected",
53
- credentials: action.credentials,
54
- error: null
55
- };
56
- case "ERROR":
57
- return { status: "error", credentials: null, error: action.error };
60
+ var resourceCache = /* @__PURE__ */ new Map();
61
+ function computeKey(options) {
62
+ if (options.credentials) return `direct:${options.credentials.sessionId}`;
63
+ if (options.sessionId && options.sessionKey)
64
+ return `session:${options.sessionId}`;
65
+ return `connect:${options.avatarId}:${options.connectUrl ?? "custom"}`;
66
+ }
67
+ async function fetchCredentials(options) {
68
+ const { avatarId, sessionId, sessionKey, connectUrl, connect, baseUrl } = options;
69
+ if (sessionId && sessionKey) {
70
+ const { url, token, roomName } = await consumeSession({
71
+ sessionId,
72
+ sessionKey,
73
+ baseUrl
74
+ });
75
+ return { sessionId, serverUrl: url, token, roomName };
76
+ }
77
+ if (connect) {
78
+ return connect(avatarId);
58
79
  }
80
+ if (connectUrl) {
81
+ const response = await fetch(connectUrl, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({ avatarId })
85
+ });
86
+ if (!response.ok) {
87
+ const errorText = await response.text();
88
+ throw new Error(`Failed to connect: ${response.status} ${errorText}`);
89
+ }
90
+ return response.json();
91
+ }
92
+ throw new Error(
93
+ "AvatarCall requires one of: credentials, sessionId+sessionKey, connectUrl, or connect"
94
+ );
59
95
  }
60
96
  function useCredentials(options) {
61
- const {
62
- avatarId,
63
- sessionId,
64
- sessionKey,
65
- credentials: directCredentials,
66
- connectUrl,
67
- connect,
68
- onError
69
- } = options;
70
- const [state, dispatch] = useReducer(credentialsReducer, {
71
- status: "idle",
72
- credentials: null,
73
- error: null
74
- });
75
- const fetchedForRef = useRef(null);
76
- const onErrorRef = useRef(onError);
77
- onErrorRef.current = onError;
78
- const mode = directCredentials ? "direct" : sessionId && sessionKey ? "session" : connectUrl || connect ? "connect" : null;
97
+ const key = computeKey(options);
79
98
  useEffect(() => {
80
- if (mode !== "direct" || !directCredentials) return;
81
- dispatch({ type: "CONNECTED", credentials: directCredentials });
82
- }, [mode, directCredentials]);
83
- useEffect(() => {
84
- if (mode !== "session" || !sessionId || !sessionKey) return;
85
- if (fetchedForRef.current === sessionId) return;
86
- fetchedForRef.current = sessionId;
87
- dispatch({ type: "CONNECT" });
88
- consumeSession({ sessionId, sessionKey }).then(({ url, token, roomName }) => {
89
- dispatch({
90
- type: "CONNECTED",
91
- credentials: { sessionId, serverUrl: url, token, roomName }
92
- });
93
- }).catch((err) => {
94
- const error = err instanceof Error ? err : new Error(String(err));
95
- dispatch({ type: "ERROR", error });
96
- onErrorRef.current?.(error);
97
- });
98
- }, [mode, sessionId, sessionKey]);
99
- useEffect(() => {
100
- if (mode !== "connect") return;
101
- if (fetchedForRef.current === avatarId) return;
102
- fetchedForRef.current = avatarId;
103
- dispatch({ type: "CONNECT" });
104
- async function fetchCredentials() {
105
- if (connect) {
106
- return connect(avatarId);
107
- }
108
- if (connectUrl) {
109
- const response = await fetch(connectUrl, {
110
- method: "POST",
111
- headers: { "Content-Type": "application/json" },
112
- body: JSON.stringify({ avatarId })
113
- });
114
- if (!response.ok) {
115
- const errorText = await response.text();
116
- throw new Error(`Failed to connect: ${response.status} ${errorText}`);
117
- }
118
- return response.json();
119
- }
120
- throw new Error("No connect method available");
121
- }
122
- fetchCredentials().then((credentials) => {
123
- dispatch({ type: "CONNECTED", credentials });
124
- }).catch((err) => {
125
- const error = err instanceof Error ? err : new Error(String(err));
126
- dispatch({ type: "ERROR", error });
127
- onErrorRef.current?.(error);
128
- });
129
- }, [mode, avatarId, connectUrl, connect]);
130
- return state;
99
+ return () => {
100
+ resourceCache.delete(key);
101
+ };
102
+ }, [key]);
103
+ if (options.credentials) {
104
+ return options.credentials;
105
+ }
106
+ let resource = resourceCache.get(key);
107
+ if (!resource) {
108
+ resource = createSuspenseResource(fetchCredentials(options));
109
+ resourceCache.set(key, resource);
110
+ }
111
+ return resource.read();
131
112
  }
132
113
  function useLatest(value) {
133
114
  const ref = useRef(value);
@@ -262,19 +243,43 @@ function useAvatarSession() {
262
243
  const context = useAvatarSessionContext();
263
244
  return context;
264
245
  }
265
- function AvatarVideo({ children, ...props }) {
246
+
247
+ // src/hooks/useAvatarStatus.ts
248
+ function useAvatarStatus() {
266
249
  const session = useAvatarSession();
267
250
  const { videoTrackRef, hasVideo } = useAvatar();
268
- const isConnecting = session.state === "connecting";
269
- const state = {
270
- hasVideo,
271
- isConnecting,
272
- trackRef: videoTrackRef
273
- };
251
+ switch (session.state) {
252
+ case "connecting":
253
+ case "idle":
254
+ return { status: "connecting" };
255
+ case "active":
256
+ if (hasVideo && videoTrackRef) {
257
+ return { status: "ready", videoTrackRef };
258
+ }
259
+ return { status: "waiting" };
260
+ case "ending":
261
+ return { status: "ending" };
262
+ case "ended":
263
+ return { status: "ended" };
264
+ case "error":
265
+ return { status: "error", error: session.error };
266
+ }
267
+ }
268
+ function AvatarVideo({ children, ...props }) {
269
+ const avatar = useAvatarStatus();
270
+ const videoStatus = avatar.status === "ready" ? avatar : avatar.status === "connecting" ? { status: "connecting" } : { status: "waiting" };
274
271
  if (children) {
275
- return /* @__PURE__ */ jsx(Fragment, { children: children(state) });
272
+ return /* @__PURE__ */ jsx(Fragment, { children: children(videoStatus) });
276
273
  }
277
- return /* @__PURE__ */ jsx("div", { ...props, "data-has-video": hasVideo, "data-connecting": isConnecting, children: hasVideo && videoTrackRef && isTrackReference(videoTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: videoTrackRef }) });
274
+ return /* @__PURE__ */ jsx(
275
+ "div",
276
+ {
277
+ ...props,
278
+ "data-avatar-video": "",
279
+ "data-avatar-status": videoStatus.status,
280
+ children: videoStatus.status === "ready" && isTrackReference(videoStatus.videoTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: videoStatus.videoTrackRef })
281
+ }
282
+ );
278
283
  }
279
284
  function useLocalMedia() {
280
285
  const { localParticipant } = useLocalParticipant();
@@ -329,13 +334,22 @@ function ControlBar({
329
334
  ...props
330
335
  }) {
331
336
  const session = useAvatarSession();
332
- const { isMicEnabled, isCameraEnabled, toggleMic, toggleCamera } = useLocalMedia();
337
+ const {
338
+ isMicEnabled,
339
+ isCameraEnabled,
340
+ isScreenShareEnabled,
341
+ toggleMic,
342
+ toggleCamera,
343
+ toggleScreenShare
344
+ } = useLocalMedia();
333
345
  const isActive = session.state === "active";
334
346
  const state = {
335
347
  isMicEnabled,
336
348
  isCameraEnabled,
349
+ isScreenShareEnabled,
337
350
  toggleMic,
338
351
  toggleCamera,
352
+ toggleScreenShare,
339
353
  endCall: session.end,
340
354
  isActive
341
355
  };
@@ -345,14 +359,14 @@ function ControlBar({
345
359
  if (!isActive) {
346
360
  return null;
347
361
  }
348
- return /* @__PURE__ */ jsxs("div", { ...props, "data-active": isActive, children: [
362
+ return /* @__PURE__ */ jsxs("div", { ...props, "data-avatar-control-bar": "", "data-avatar-active": isActive, children: [
349
363
  showMicrophone && /* @__PURE__ */ jsx(
350
364
  "button",
351
365
  {
352
366
  type: "button",
353
367
  onClick: toggleMic,
354
- "data-control": "microphone",
355
- "data-enabled": isMicEnabled,
368
+ "data-avatar-control": "microphone",
369
+ "data-avatar-enabled": isMicEnabled,
356
370
  "aria-label": isMicEnabled ? "Mute microphone" : "Unmute microphone",
357
371
  children: microphoneIcon
358
372
  }
@@ -362,8 +376,8 @@ function ControlBar({
362
376
  {
363
377
  type: "button",
364
378
  onClick: toggleCamera,
365
- "data-control": "camera",
366
- "data-enabled": isCameraEnabled,
379
+ "data-avatar-control": "camera",
380
+ "data-avatar-enabled": isCameraEnabled,
367
381
  "aria-label": isCameraEnabled ? "Turn off camera" : "Turn on camera",
368
382
  children: cameraIcon
369
383
  }
@@ -373,7 +387,8 @@ function ControlBar({
373
387
  {
374
388
  source: Track.Source.ScreenShare,
375
389
  showIcon: false,
376
- "data-control": "screen-share",
390
+ "data-avatar-control": "screen-share",
391
+ "data-avatar-enabled": isScreenShareEnabled,
377
392
  "aria-label": "Toggle screen share",
378
393
  children: screenShareIcon
379
394
  }
@@ -383,7 +398,8 @@ function ControlBar({
383
398
  {
384
399
  type: "button",
385
400
  onClick: session.end,
386
- "data-control": "end-call",
401
+ "data-avatar-control": "end-call",
402
+ "data-avatar-enabled": true,
387
403
  "aria-label": "End call",
388
404
  children: phoneIcon
389
405
  }
@@ -457,7 +473,11 @@ var phoneIcon = /* @__PURE__ */ jsx(
457
473
  children: /* @__PURE__ */ jsx("path", { d: "M12.8429 22.5693L11.4018 21.0986C11.2675 20.9626 11.1625 20.7995 11.0935 20.6197C11.0245 20.4399 10.9931 20.2474 11.0013 20.0545C11.0094 19.8616 11.0569 19.6726 11.1408 19.4995C11.2247 19.3265 11.343 19.1732 11.4883 19.0495C13.127 17.7049 15.0519 16.7714 17.1083 16.3239C19.0064 15.892 20.9744 15.892 22.8725 16.3239C24.9374 16.7743 26.8693 17.7147 28.5117 19.0691C28.6565 19.1924 28.7746 19.3451 28.8585 19.5176C28.9423 19.69 28.99 19.8784 28.9986 20.0707C29.0072 20.263 28.9764 20.455 28.9083 20.6345C28.8402 20.814 28.7362 20.9771 28.603 21.1133L27.1619 22.584C26.9311 22.8242 26.6226 22.9706 26.2938 22.9959C25.9651 23.0211 25.6385 22.9235 25.3751 22.7212C24.8531 22.3127 24.2875 21.9657 23.689 21.6869C23.4525 21.5774 23.2517 21.4009 23.1103 21.1785C22.969 20.9561 22.8931 20.697 22.8917 20.4319V19.1867C21.0053 18.6573 19.0139 18.6573 17.1275 19.1867V20.4319C17.1261 20.697 17.0502 20.9561 16.9089 21.1785C16.7676 21.4009 16.5667 21.5774 16.3302 21.6869C15.7317 21.9657 15.1661 22.3127 14.6442 22.7212C14.3779 22.9258 14.0473 23.0233 13.7152 22.9953C13.383 22.9673 13.0726 22.8156 12.8429 22.5693Z" })
458
474
  }
459
475
  );
460
- function UserVideo({ children, mirror = true, ...props }) {
476
+ function UserVideo({
477
+ children,
478
+ mirror = true,
479
+ ...props
480
+ }) {
461
481
  const { localVideoTrackRef, isCameraEnabled } = useLocalMedia();
462
482
  const hasVideo = localVideoTrackRef !== null && isTrackReference(localVideoTrackRef);
463
483
  const state = {
@@ -472,9 +492,10 @@ function UserVideo({ children, mirror = true, ...props }) {
472
492
  "div",
473
493
  {
474
494
  ...props,
475
- "data-has-video": hasVideo,
476
- "data-camera-enabled": isCameraEnabled,
477
- "data-mirror": mirror,
495
+ "data-avatar-user-video": "",
496
+ "data-avatar-has-video": hasVideo,
497
+ "data-avatar-camera-enabled": isCameraEnabled,
498
+ "data-avatar-mirror": mirror,
478
499
  children: hasVideo && localVideoTrackRef && isTrackReference(localVideoTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: localVideoTrackRef })
479
500
  }
480
501
  );
@@ -486,6 +507,7 @@ function AvatarCall({
486
507
  credentials: directCredentials,
487
508
  connectUrl,
488
509
  connect,
510
+ baseUrl,
489
511
  avatarImageUrl,
490
512
  onEnd,
491
513
  onError,
@@ -494,50 +516,24 @@ function AvatarCall({
494
516
  ...props
495
517
  }) {
496
518
  const onErrorRef = useLatest(onError);
497
- const { status, credentials, error } = useCredentials({
519
+ const credentials = useCredentials({
498
520
  avatarId,
499
521
  sessionId,
500
522
  sessionKey,
501
523
  credentials: directCredentials,
502
524
  connectUrl,
503
525
  connect,
504
- onError
526
+ baseUrl
505
527
  });
506
528
  const handleSessionError = (err) => {
507
529
  onErrorRef.current?.(err);
508
530
  };
509
531
  const backgroundStyle = avatarImageUrl ? { "--avatar-image": `url(${avatarImageUrl})` } : void 0;
510
- if (status === "idle" || status === "connecting") {
511
- return /* @__PURE__ */ jsx(
512
- "div",
513
- {
514
- ...props,
515
- "data-avatar-call": "",
516
- "data-state": "connecting",
517
- "data-avatar-id": avatarId,
518
- style: { ...props.style, ...backgroundStyle }
519
- }
520
- );
521
- }
522
- if (status === "error" || !credentials) {
523
- return /* @__PURE__ */ jsx(
524
- "div",
525
- {
526
- ...props,
527
- "data-avatar-call": "",
528
- "data-state": "error",
529
- "data-avatar-id": avatarId,
530
- "data-error": error?.message,
531
- style: { ...props.style, ...backgroundStyle }
532
- }
533
- );
534
- }
535
532
  return /* @__PURE__ */ jsx(
536
533
  "div",
537
534
  {
538
535
  ...props,
539
536
  "data-avatar-call": "",
540
- "data-state": "connected",
541
537
  "data-avatar-id": avatarId,
542
538
  style: { ...props.style, ...backgroundStyle },
543
539
  children: /* @__PURE__ */ jsx(
@@ -557,7 +553,10 @@ function AvatarCall({
557
553
  }
558
554
  );
559
555
  }
560
- function ScreenShareVideo({ children, ...props }) {
556
+ function ScreenShareVideo({
557
+ children,
558
+ ...props
559
+ }) {
561
560
  const { localParticipant } = useLocalParticipant();
562
561
  const tracks = useTracks(
563
562
  [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
@@ -578,9 +577,9 @@ function ScreenShareVideo({ children, ...props }) {
578
577
  if (!isSharing) {
579
578
  return null;
580
579
  }
581
- return /* @__PURE__ */ jsx("div", { ...props, "data-sharing": isSharing, children: screenShareTrackRef && isTrackReference(screenShareTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: screenShareTrackRef }) });
580
+ return /* @__PURE__ */ jsx("div", { ...props, "data-avatar-screen-share": "", "data-avatar-sharing": isSharing, children: screenShareTrackRef && isTrackReference(screenShareTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: screenShareTrackRef }) });
582
581
  }
583
582
 
584
- export { AvatarCall, AvatarSession, AvatarVideo, ControlBar, ScreenShareVideo, UserVideo, useAvatar, useAvatarSession, useLocalMedia };
583
+ export { AvatarCall, AvatarSession, AvatarVideo, ControlBar, ScreenShareVideo, UserVideo, useAvatar, useAvatarSession, useAvatarStatus, useLocalMedia };
585
584
  //# sourceMappingURL=index.js.map
586
585
  //# sourceMappingURL=index.js.map