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