@runwayml/avatars-react 0.4.0 → 0.6.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,8 +1,8 @@
1
- import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices, TrackToggle } from '@livekit/components-react';
1
+ import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useMaybeRoomContext, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices, TrackToggle } from '@livekit/components-react';
2
2
  export { RoomAudioRenderer as AudioRenderer, VideoTrack } from '@livekit/components-react';
3
- import { createContext, useRef, useCallback, useContext, useEffect } from 'react';
3
+ import { createContext, useRef, useCallback, useState, useEffect, useContext } from 'react';
4
4
  import { Track, ConnectionState } from 'livekit-client';
5
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/api/config.ts
8
8
  var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
@@ -26,44 +26,15 @@ async function consumeSession(options) {
26
26
  }
27
27
  return response.json();
28
28
  }
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
- };
29
+ function useLatest(value) {
30
+ const ref = useRef(value);
31
+ useEffect(() => {
32
+ ref.current = value;
33
+ }, [value]);
34
+ return ref;
57
35
  }
58
36
 
59
37
  // src/hooks/useCredentials.ts
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
38
  async function fetchCredentials(options) {
68
39
  const { avatarId, sessionId, sessionKey, connectUrl, connect, baseUrl } = options;
69
40
  if (sessionId && sessionKey) {
@@ -94,28 +65,106 @@ async function fetchCredentials(options) {
94
65
  );
95
66
  }
96
67
  function useCredentials(options) {
97
- const key = computeKey(options);
68
+ const {
69
+ credentials: directCredentials,
70
+ avatarId,
71
+ sessionId,
72
+ sessionKey,
73
+ connectUrl,
74
+ connect,
75
+ baseUrl,
76
+ onError
77
+ } = options;
78
+ const onErrorRef = useLatest(onError);
79
+ const [state, setState] = useState(() => {
80
+ if (directCredentials) {
81
+ return { status: "ready", credentials: directCredentials, error: null };
82
+ }
83
+ return { status: "loading", credentials: null, error: null };
84
+ });
98
85
  useEffect(() => {
86
+ if (directCredentials) {
87
+ setState({
88
+ status: "ready",
89
+ credentials: directCredentials,
90
+ error: null
91
+ });
92
+ return;
93
+ }
94
+ let cancelled = false;
95
+ setState({ status: "loading", credentials: null, error: null });
96
+ async function load() {
97
+ try {
98
+ const fetchOptions = {
99
+ avatarId,
100
+ sessionId,
101
+ sessionKey,
102
+ connectUrl,
103
+ connect,
104
+ baseUrl
105
+ };
106
+ const credentials = await fetchCredentials(fetchOptions);
107
+ if (!cancelled) {
108
+ setState({ status: "ready", credentials, error: null });
109
+ }
110
+ } catch (err) {
111
+ if (!cancelled) {
112
+ const error = err instanceof Error ? err : new Error(String(err));
113
+ setState({ status: "error", credentials: null, error });
114
+ onErrorRef.current?.(error);
115
+ }
116
+ }
117
+ }
118
+ load();
99
119
  return () => {
100
- resourceCache.delete(key);
120
+ cancelled = true;
101
121
  };
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);
122
+ }, [
123
+ directCredentials,
124
+ avatarId,
125
+ sessionId,
126
+ sessionKey,
127
+ connectUrl,
128
+ connect,
129
+ baseUrl
130
+ ]);
131
+ return state;
132
+ }
133
+ async function hasMediaDevice(kind) {
134
+ try {
135
+ const devices = await navigator.mediaDevices.enumerateDevices();
136
+ return devices.some((device) => device.kind === kind);
137
+ } catch {
138
+ return false;
110
139
  }
111
- return resource.read();
112
140
  }
113
- function useLatest(value) {
114
- const ref = useRef(value);
141
+ function useDeviceAvailability(requestAudio, requestVideo) {
142
+ const [state, setState] = useState({
143
+ audio: false,
144
+ video: false,
145
+ isChecking: true
146
+ });
115
147
  useEffect(() => {
116
- ref.current = value;
117
- }, [value]);
118
- return ref;
148
+ let cancelled = false;
149
+ async function checkDevices() {
150
+ const [hasAudio, hasVideo] = await Promise.all([
151
+ requestAudio ? hasMediaDevice("audioinput") : Promise.resolve(false),
152
+ requestVideo ? hasMediaDevice("videoinput") : Promise.resolve(false)
153
+ ]);
154
+ if (!cancelled) {
155
+ setState({
156
+ audio: requestAudio && hasAudio,
157
+ video: requestVideo && hasVideo,
158
+ isChecking: false
159
+ });
160
+ }
161
+ }
162
+ checkDevices();
163
+ return () => {
164
+ cancelled = true;
165
+ };
166
+ }, [requestAudio, requestVideo]);
167
+ return state;
119
168
  }
120
169
  var DEFAULT_ROOM_OPTIONS = {
121
170
  adaptiveStream: false,
@@ -141,13 +190,14 @@ var AvatarSessionContext = createContext(
141
190
  function AvatarSession({
142
191
  credentials,
143
192
  children,
144
- audio = true,
145
- video = true,
193
+ audio: requestAudio = true,
194
+ video: requestVideo = true,
146
195
  onEnd,
147
196
  onError,
148
197
  __unstable_roomOptions
149
198
  }) {
150
199
  const errorRef = useRef(null);
200
+ const deviceAvailability = useDeviceAvailability(requestAudio, requestVideo);
151
201
  const handleError = (error) => {
152
202
  errorRef.current = error;
153
203
  onError?.(error);
@@ -156,14 +206,29 @@ function AvatarSession({
156
206
  ...DEFAULT_ROOM_OPTIONS,
157
207
  ...__unstable_roomOptions
158
208
  };
209
+ if (deviceAvailability.isChecking) {
210
+ return /* @__PURE__ */ jsx(
211
+ AvatarSessionContext.Provider,
212
+ {
213
+ value: {
214
+ state: "connecting",
215
+ sessionId: credentials.sessionId,
216
+ error: null,
217
+ end: async () => {
218
+ }
219
+ },
220
+ children
221
+ }
222
+ );
223
+ }
159
224
  return /* @__PURE__ */ jsxs(
160
225
  LiveKitRoom,
161
226
  {
162
227
  serverUrl: credentials.serverUrl,
163
228
  token: credentials.token,
164
229
  connect: true,
165
- audio,
166
- video,
230
+ audio: deviceAvailability.audio,
231
+ video: deviceAvailability.video,
167
232
  onDisconnected: () => onEnd?.(),
168
233
  onError: handleError,
169
234
  options: roomOptions,
@@ -222,14 +287,34 @@ function useAvatarSessionContext() {
222
287
  }
223
288
  return context;
224
289
  }
290
+ function LoadingSessionProvider({
291
+ state,
292
+ error,
293
+ children
294
+ }) {
295
+ const contextValue = {
296
+ state,
297
+ sessionId: "",
298
+ error,
299
+ end: async () => {
300
+ }
301
+ };
302
+ return /* @__PURE__ */ jsx(AvatarSessionContext.Provider, { value: contextValue, children });
303
+ }
225
304
  function useAvatar() {
226
- const remoteParticipants = useRemoteParticipants();
305
+ const room = useMaybeRoomContext();
306
+ const hasRoomContext = room !== void 0;
307
+ const remoteParticipants = useRemoteParticipants({ room });
227
308
  const avatarParticipant = remoteParticipants[0] ?? null;
228
309
  const videoTracks = useTracks(
229
310
  [{ source: Track.Source.Camera, withPlaceholder: true }],
230
- { onlySubscribed: true, updateOnlyOn: [] }
311
+ {
312
+ onlySubscribed: true,
313
+ updateOnlyOn: [],
314
+ room: hasRoomContext ? room : void 0
315
+ }
231
316
  ).filter((ref) => !ref.participant.isLocal);
232
- const videoTrackRef = videoTracks[0] ?? null;
317
+ const videoTrackRef = hasRoomContext ? videoTracks[0] ?? null : null;
233
318
  const hasVideo = videoTrackRef !== null && isTrackReference(videoTrackRef);
234
319
  return {
235
320
  participant: avatarParticipant,
@@ -271,40 +356,57 @@ function AvatarVideo({ children, ...props }) {
271
356
  if (children) {
272
357
  return /* @__PURE__ */ jsx(Fragment, { children: children(videoStatus) });
273
358
  }
274
- return /* @__PURE__ */ jsx("div", { ...props, "data-avatar-video": "", "data-status": videoStatus.status, children: videoStatus.status === "ready" && isTrackReference(videoStatus.videoTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: videoStatus.videoTrackRef }) });
359
+ return /* @__PURE__ */ jsx(
360
+ "div",
361
+ {
362
+ ...props,
363
+ "data-avatar-video": "",
364
+ "data-avatar-status": videoStatus.status,
365
+ children: videoStatus.status === "ready" && isTrackReference(videoStatus.videoTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: videoStatus.videoTrackRef })
366
+ }
367
+ );
275
368
  }
276
369
  function useLocalMedia() {
277
- const { localParticipant } = useLocalParticipant();
370
+ const room = useMaybeRoomContext();
371
+ const hasRoomContext = room !== void 0;
372
+ const { localParticipant } = useLocalParticipant({ room });
278
373
  const audioDevices = useMediaDevices({ kind: "audioinput" });
279
374
  const videoDevices = useMediaDevices({ kind: "videoinput" });
280
- const hasMic = audioDevices.length > 0;
281
- const hasCamera = videoDevices.length > 0;
375
+ const hasMic = audioDevices?.length > 0;
376
+ const hasCamera = videoDevices?.length > 0;
282
377
  const isMicEnabled = localParticipant?.isMicrophoneEnabled ?? false;
283
378
  const isCameraEnabled = localParticipant?.isCameraEnabled ?? false;
284
379
  const isScreenShareEnabled = localParticipant?.isScreenShareEnabled ?? false;
285
380
  const isMicEnabledRef = useLatest(isMicEnabled);
286
381
  const isCameraEnabledRef = useLatest(isCameraEnabled);
287
382
  const isScreenShareEnabledRef = useLatest(isScreenShareEnabled);
383
+ const hasMicRef = useLatest(hasMic);
384
+ const hasCameraRef = useLatest(hasCamera);
288
385
  const toggleMic = useCallback(() => {
289
- localParticipant?.setMicrophoneEnabled(!isMicEnabledRef.current);
290
- }, [localParticipant, isMicEnabledRef]);
386
+ if (hasMicRef.current || isMicEnabledRef.current) {
387
+ localParticipant?.setMicrophoneEnabled(!isMicEnabledRef.current);
388
+ }
389
+ }, [localParticipant]);
291
390
  const toggleCamera = useCallback(() => {
292
- localParticipant?.setCameraEnabled(!isCameraEnabledRef.current);
293
- }, [localParticipant, isCameraEnabledRef]);
391
+ if (hasCameraRef.current || isCameraEnabledRef.current) {
392
+ localParticipant?.setCameraEnabled(!isCameraEnabledRef.current);
393
+ }
394
+ }, [localParticipant]);
294
395
  const toggleScreenShare = useCallback(() => {
295
396
  localParticipant?.setScreenShareEnabled(!isScreenShareEnabledRef.current);
296
- }, [localParticipant, isScreenShareEnabledRef]);
397
+ }, [localParticipant]);
297
398
  const tracks = useTracks(
298
399
  [{ source: Track.Source.Camera, withPlaceholder: true }],
299
400
  {
300
401
  onlySubscribed: false,
301
- updateOnlyOn: []
402
+ updateOnlyOn: [],
403
+ room: hasRoomContext ? room : void 0
302
404
  }
303
405
  );
304
406
  const localIdentity = localParticipant?.identity;
305
- const localVideoTrackRef = tracks.find(
407
+ const localVideoTrackRef = hasRoomContext ? tracks.find(
306
408
  (trackRef) => trackRef.participant.identity === localIdentity && trackRef.source === Track.Source.Camera
307
- ) ?? null;
409
+ ) ?? null : null;
308
410
  return {
309
411
  hasMic,
310
412
  hasCamera,
@@ -326,13 +428,22 @@ function ControlBar({
326
428
  ...props
327
429
  }) {
328
430
  const session = useAvatarSession();
329
- const { isMicEnabled, isCameraEnabled, toggleMic, toggleCamera } = useLocalMedia();
431
+ const {
432
+ isMicEnabled,
433
+ isCameraEnabled,
434
+ isScreenShareEnabled,
435
+ toggleMic,
436
+ toggleCamera,
437
+ toggleScreenShare
438
+ } = useLocalMedia();
330
439
  const isActive = session.state === "active";
331
440
  const state = {
332
441
  isMicEnabled,
333
442
  isCameraEnabled,
443
+ isScreenShareEnabled,
334
444
  toggleMic,
335
445
  toggleCamera,
446
+ toggleScreenShare,
336
447
  endCall: session.end,
337
448
  isActive
338
449
  };
@@ -342,14 +453,14 @@ function ControlBar({
342
453
  if (!isActive) {
343
454
  return null;
344
455
  }
345
- return /* @__PURE__ */ jsxs("div", { ...props, "data-active": isActive, children: [
456
+ return /* @__PURE__ */ jsxs("div", { ...props, "data-avatar-control-bar": "", "data-avatar-active": isActive, children: [
346
457
  showMicrophone && /* @__PURE__ */ jsx(
347
458
  "button",
348
459
  {
349
460
  type: "button",
350
461
  onClick: toggleMic,
351
- "data-control": "microphone",
352
- "data-enabled": isMicEnabled,
462
+ "data-avatar-control": "microphone",
463
+ "data-avatar-enabled": isMicEnabled,
353
464
  "aria-label": isMicEnabled ? "Mute microphone" : "Unmute microphone",
354
465
  children: microphoneIcon
355
466
  }
@@ -359,8 +470,8 @@ function ControlBar({
359
470
  {
360
471
  type: "button",
361
472
  onClick: toggleCamera,
362
- "data-control": "camera",
363
- "data-enabled": isCameraEnabled,
473
+ "data-avatar-control": "camera",
474
+ "data-avatar-enabled": isCameraEnabled,
364
475
  "aria-label": isCameraEnabled ? "Turn off camera" : "Turn on camera",
365
476
  children: cameraIcon
366
477
  }
@@ -370,7 +481,8 @@ function ControlBar({
370
481
  {
371
482
  source: Track.Source.ScreenShare,
372
483
  showIcon: false,
373
- "data-control": "screen-share",
484
+ "data-avatar-control": "screen-share",
485
+ "data-avatar-enabled": isScreenShareEnabled,
374
486
  "aria-label": "Toggle screen share",
375
487
  children: screenShareIcon
376
488
  }
@@ -380,7 +492,8 @@ function ControlBar({
380
492
  {
381
493
  type: "button",
382
494
  onClick: session.end,
383
- "data-control": "end-call",
495
+ "data-avatar-control": "end-call",
496
+ "data-avatar-enabled": true,
384
497
  "aria-label": "End call",
385
498
  children: phoneIcon
386
499
  }
@@ -454,7 +567,11 @@ var phoneIcon = /* @__PURE__ */ jsx(
454
567
  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" })
455
568
  }
456
569
  );
457
- function UserVideo({ children, mirror = true, ...props }) {
570
+ function UserVideo({
571
+ children,
572
+ mirror = true,
573
+ ...props
574
+ }) {
458
575
  const { localVideoTrackRef, isCameraEnabled } = useLocalMedia();
459
576
  const hasVideo = localVideoTrackRef !== null && isTrackReference(localVideoTrackRef);
460
577
  const state = {
@@ -469,9 +586,10 @@ function UserVideo({ children, mirror = true, ...props }) {
469
586
  "div",
470
587
  {
471
588
  ...props,
472
- "data-has-video": hasVideo,
473
- "data-camera-enabled": isCameraEnabled,
474
- "data-mirror": mirror,
589
+ "data-avatar-user-video": "",
590
+ "data-avatar-has-video": hasVideo,
591
+ "data-avatar-camera-enabled": isCameraEnabled,
592
+ "data-avatar-mirror": mirror,
475
593
  children: hasVideo && localVideoTrackRef && isTrackReference(localVideoTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: localVideoTrackRef })
476
594
  }
477
595
  );
@@ -492,19 +610,44 @@ function AvatarCall({
492
610
  ...props
493
611
  }) {
494
612
  const onErrorRef = useLatest(onError);
495
- const credentials = useCredentials({
613
+ const credentialsState = useCredentials({
496
614
  avatarId,
497
615
  sessionId,
498
616
  sessionKey,
499
617
  credentials: directCredentials,
500
618
  connectUrl,
501
619
  connect,
502
- baseUrl
620
+ baseUrl,
621
+ onError: (err) => onErrorRef.current?.(err)
503
622
  });
504
623
  const handleSessionError = (err) => {
505
624
  onErrorRef.current?.(err);
506
625
  };
507
626
  const backgroundStyle = avatarImageUrl ? { "--avatar-image": `url(${avatarImageUrl})` } : void 0;
627
+ const defaultChildren = /* @__PURE__ */ jsxs(Fragment, { children: [
628
+ /* @__PURE__ */ jsx(AvatarVideo, {}),
629
+ /* @__PURE__ */ jsx(UserVideo, {}),
630
+ /* @__PURE__ */ jsx(ControlBar, {})
631
+ ] });
632
+ if (credentialsState.status !== "ready") {
633
+ return /* @__PURE__ */ jsx(
634
+ "div",
635
+ {
636
+ ...props,
637
+ "data-avatar-call": "",
638
+ "data-avatar-id": avatarId,
639
+ style: { ...props.style, ...backgroundStyle },
640
+ children: /* @__PURE__ */ jsx(
641
+ LoadingSessionProvider,
642
+ {
643
+ state: credentialsState.status === "error" ? "error" : "connecting",
644
+ error: credentialsState.error,
645
+ children: children ?? defaultChildren
646
+ }
647
+ )
648
+ }
649
+ );
650
+ }
508
651
  return /* @__PURE__ */ jsx(
509
652
  "div",
510
653
  {
@@ -515,30 +658,31 @@ function AvatarCall({
515
658
  children: /* @__PURE__ */ jsx(
516
659
  AvatarSession,
517
660
  {
518
- credentials,
661
+ credentials: credentialsState.credentials,
519
662
  onEnd,
520
663
  onError: handleSessionError,
521
664
  __unstable_roomOptions,
522
- children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
523
- /* @__PURE__ */ jsx(AvatarVideo, {}),
524
- /* @__PURE__ */ jsx(UserVideo, {}),
525
- /* @__PURE__ */ jsx(ControlBar, {})
526
- ] })
665
+ children: children ?? defaultChildren
527
666
  }
528
667
  )
529
668
  }
530
669
  );
531
670
  }
532
- function ScreenShareVideo({ children, ...props }) {
533
- const { localParticipant } = useLocalParticipant();
671
+ function ScreenShareVideo({
672
+ children,
673
+ ...props
674
+ }) {
675
+ const room = useMaybeRoomContext();
676
+ const hasRoomContext = room !== void 0;
677
+ const { localParticipant } = useLocalParticipant({ room });
534
678
  const tracks = useTracks(
535
679
  [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
536
- { onlySubscribed: false }
680
+ { onlySubscribed: false, room: hasRoomContext ? room : void 0 }
537
681
  );
538
682
  const localIdentity = localParticipant?.identity;
539
- const screenShareTrackRef = tracks.find(
683
+ const screenShareTrackRef = hasRoomContext ? tracks.find(
540
684
  (trackRef) => trackRef.participant.identity === localIdentity && trackRef.source === Track.Source.ScreenShare
541
- ) ?? null;
685
+ ) ?? null : null;
542
686
  const isSharing = screenShareTrackRef !== null && isTrackReference(screenShareTrackRef);
543
687
  const state = {
544
688
  isSharing,
@@ -550,7 +694,7 @@ function ScreenShareVideo({ children, ...props }) {
550
694
  if (!isSharing) {
551
695
  return null;
552
696
  }
553
- return /* @__PURE__ */ jsx("div", { ...props, "data-sharing": isSharing, children: screenShareTrackRef && isTrackReference(screenShareTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: screenShareTrackRef }) });
697
+ return /* @__PURE__ */ jsx("div", { ...props, "data-avatar-screen-share": "", "data-avatar-sharing": isSharing, children: screenShareTrackRef && isTrackReference(screenShareTrackRef) && /* @__PURE__ */ jsx(VideoTrack, { trackRef: screenShareTrackRef }) });
554
698
  }
555
699
 
556
700
  export { AvatarCall, AvatarSession, AvatarVideo, ControlBar, ScreenShareVideo, UserVideo, useAvatar, useAvatarSession, useAvatarStatus, useLocalMedia };