@runwayml/avatars-react 0.5.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.d.cts CHANGED
@@ -124,7 +124,7 @@ declare function AvatarCall({ avatarId, sessionId, sessionKey, credentials: dire
124
124
  * Establishes a WebRTC connection and provides session state to children.
125
125
  * This is a headless component that renders minimal DOM.
126
126
  */
127
- declare function AvatarSession({ credentials, children, audio, video, onEnd, onError, __unstable_roomOptions, }: AvatarSessionProps): react_jsx_runtime.JSX.Element;
127
+ declare function AvatarSession({ credentials, children, audio: requestAudio, video: requestVideo, onEnd, onError, __unstable_roomOptions, }: AvatarSessionProps): react_jsx_runtime.JSX.Element;
128
128
 
129
129
  /**
130
130
  * useAvatarStatus Hook
@@ -273,6 +273,10 @@ type UseAvatarSessionReturn = {
273
273
  */
274
274
  declare function useAvatarSession(): UseAvatarSessionReturn;
275
275
 
276
+ /**
277
+ * Hook for local media controls (mic, camera, screen share).
278
+ * Returns safe defaults when called outside of LiveKitRoom context.
279
+ */
276
280
  declare function useLocalMedia(): UseLocalMediaReturn;
277
281
 
278
282
  export { AvatarCall, type AvatarCallProps, AvatarSession, type AvatarStatus, AvatarVideo, type AvatarVideoStatus, ControlBar, ScreenShareVideo, type SessionCredentials, type SessionState, UserVideo, useAvatar, useAvatarSession, useAvatarStatus, useLocalMedia };
package/dist/index.d.ts CHANGED
@@ -124,7 +124,7 @@ declare function AvatarCall({ avatarId, sessionId, sessionKey, credentials: dire
124
124
  * Establishes a WebRTC connection and provides session state to children.
125
125
  * This is a headless component that renders minimal DOM.
126
126
  */
127
- declare function AvatarSession({ credentials, children, audio, video, onEnd, onError, __unstable_roomOptions, }: AvatarSessionProps): react_jsx_runtime.JSX.Element;
127
+ declare function AvatarSession({ credentials, children, audio: requestAudio, video: requestVideo, onEnd, onError, __unstable_roomOptions, }: AvatarSessionProps): react_jsx_runtime.JSX.Element;
128
128
 
129
129
  /**
130
130
  * useAvatarStatus Hook
@@ -273,6 +273,10 @@ type UseAvatarSessionReturn = {
273
273
  */
274
274
  declare function useAvatarSession(): UseAvatarSessionReturn;
275
275
 
276
+ /**
277
+ * Hook for local media controls (mic, camera, screen share).
278
+ * Returns safe defaults when called outside of LiveKitRoom context.
279
+ */
276
280
  declare function useLocalMedia(): UseLocalMediaReturn;
277
281
 
278
282
  export { AvatarCall, type AvatarCallProps, AvatarSession, type AvatarStatus, AvatarVideo, type AvatarVideoStatus, ControlBar, ScreenShareVideo, type SessionCredentials, type SessionState, UserVideo, useAvatar, useAvatarSession, useAvatarStatus, useLocalMedia };
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,
@@ -282,37 +367,46 @@ function AvatarVideo({ children, ...props }) {
282
367
  );
283
368
  }
284
369
  function useLocalMedia() {
285
- const { localParticipant } = useLocalParticipant();
370
+ const room = useMaybeRoomContext();
371
+ const hasRoomContext = room !== void 0;
372
+ const { localParticipant } = useLocalParticipant({ room });
286
373
  const audioDevices = useMediaDevices({ kind: "audioinput" });
287
374
  const videoDevices = useMediaDevices({ kind: "videoinput" });
288
- const hasMic = audioDevices.length > 0;
289
- const hasCamera = videoDevices.length > 0;
375
+ const hasMic = audioDevices?.length > 0;
376
+ const hasCamera = videoDevices?.length > 0;
290
377
  const isMicEnabled = localParticipant?.isMicrophoneEnabled ?? false;
291
378
  const isCameraEnabled = localParticipant?.isCameraEnabled ?? false;
292
379
  const isScreenShareEnabled = localParticipant?.isScreenShareEnabled ?? false;
293
380
  const isMicEnabledRef = useLatest(isMicEnabled);
294
381
  const isCameraEnabledRef = useLatest(isCameraEnabled);
295
382
  const isScreenShareEnabledRef = useLatest(isScreenShareEnabled);
383
+ const hasMicRef = useLatest(hasMic);
384
+ const hasCameraRef = useLatest(hasCamera);
296
385
  const toggleMic = useCallback(() => {
297
- localParticipant?.setMicrophoneEnabled(!isMicEnabledRef.current);
298
- }, [localParticipant, isMicEnabledRef]);
386
+ if (hasMicRef.current || isMicEnabledRef.current) {
387
+ localParticipant?.setMicrophoneEnabled(!isMicEnabledRef.current);
388
+ }
389
+ }, [localParticipant]);
299
390
  const toggleCamera = useCallback(() => {
300
- localParticipant?.setCameraEnabled(!isCameraEnabledRef.current);
301
- }, [localParticipant, isCameraEnabledRef]);
391
+ if (hasCameraRef.current || isCameraEnabledRef.current) {
392
+ localParticipant?.setCameraEnabled(!isCameraEnabledRef.current);
393
+ }
394
+ }, [localParticipant]);
302
395
  const toggleScreenShare = useCallback(() => {
303
396
  localParticipant?.setScreenShareEnabled(!isScreenShareEnabledRef.current);
304
- }, [localParticipant, isScreenShareEnabledRef]);
397
+ }, [localParticipant]);
305
398
  const tracks = useTracks(
306
399
  [{ source: Track.Source.Camera, withPlaceholder: true }],
307
400
  {
308
401
  onlySubscribed: false,
309
- updateOnlyOn: []
402
+ updateOnlyOn: [],
403
+ room: hasRoomContext ? room : void 0
310
404
  }
311
405
  );
312
406
  const localIdentity = localParticipant?.identity;
313
- const localVideoTrackRef = tracks.find(
407
+ const localVideoTrackRef = hasRoomContext ? tracks.find(
314
408
  (trackRef) => trackRef.participant.identity === localIdentity && trackRef.source === Track.Source.Camera
315
- ) ?? null;
409
+ ) ?? null : null;
316
410
  return {
317
411
  hasMic,
318
412
  hasCamera,
@@ -516,19 +610,44 @@ function AvatarCall({
516
610
  ...props
517
611
  }) {
518
612
  const onErrorRef = useLatest(onError);
519
- const credentials = useCredentials({
613
+ const credentialsState = useCredentials({
520
614
  avatarId,
521
615
  sessionId,
522
616
  sessionKey,
523
617
  credentials: directCredentials,
524
618
  connectUrl,
525
619
  connect,
526
- baseUrl
620
+ baseUrl,
621
+ onError: (err) => onErrorRef.current?.(err)
527
622
  });
528
623
  const handleSessionError = (err) => {
529
624
  onErrorRef.current?.(err);
530
625
  };
531
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
+ }
532
651
  return /* @__PURE__ */ jsx(
533
652
  "div",
534
653
  {
@@ -539,15 +658,11 @@ function AvatarCall({
539
658
  children: /* @__PURE__ */ jsx(
540
659
  AvatarSession,
541
660
  {
542
- credentials,
661
+ credentials: credentialsState.credentials,
543
662
  onEnd,
544
663
  onError: handleSessionError,
545
664
  __unstable_roomOptions,
546
- children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
547
- /* @__PURE__ */ jsx(AvatarVideo, {}),
548
- /* @__PURE__ */ jsx(UserVideo, {}),
549
- /* @__PURE__ */ jsx(ControlBar, {})
550
- ] })
665
+ children: children ?? defaultChildren
551
666
  }
552
667
  )
553
668
  }
@@ -557,15 +672,17 @@ function ScreenShareVideo({
557
672
  children,
558
673
  ...props
559
674
  }) {
560
- const { localParticipant } = useLocalParticipant();
675
+ const room = useMaybeRoomContext();
676
+ const hasRoomContext = room !== void 0;
677
+ const { localParticipant } = useLocalParticipant({ room });
561
678
  const tracks = useTracks(
562
679
  [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
563
- { onlySubscribed: false }
680
+ { onlySubscribed: false, room: hasRoomContext ? room : void 0 }
564
681
  );
565
682
  const localIdentity = localParticipant?.identity;
566
- const screenShareTrackRef = tracks.find(
683
+ const screenShareTrackRef = hasRoomContext ? tracks.find(
567
684
  (trackRef) => trackRef.participant.identity === localIdentity && trackRef.source === Track.Source.ScreenShare
568
- ) ?? null;
685
+ ) ?? null : null;
569
686
  const isSharing = screenShareTrackRef !== null && isTrackReference(screenShareTrackRef);
570
687
  const state = {
571
688
  isSharing,