@runwayml/avatars-react 0.9.0 → 0.10.0-beta.1

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
@@ -154,53 +154,22 @@ function useLatest(value) {
154
154
  }, [value]);
155
155
  return ref;
156
156
  }
157
- async function hasMediaDevice(kind, timeoutMs = 1e3) {
157
+
158
+ // src/utils/parseClientEvent.ts
159
+ function isAckMessage(args) {
160
+ return "status" in args && args.status === "event_sent";
161
+ }
162
+ function parseClientEvent(payload) {
158
163
  try {
159
- const timeoutPromise = new Promise(
160
- (resolve) => setTimeout(() => resolve(false), timeoutMs)
161
- );
162
- const checkPromise = navigator.mediaDevices.enumerateDevices().then((devices) => devices.some((device) => device.kind === kind));
163
- return await Promise.race([checkPromise, timeoutPromise]);
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;
164
169
  } catch {
165
- return false;
170
+ return null;
166
171
  }
167
172
  }
168
- function useDeviceAvailability(requestAudio, requestVideo) {
169
- const [state, setState] = react.useState({
170
- audio: requestAudio,
171
- // Optimistically assume devices exist
172
- video: requestVideo
173
- });
174
- react.useEffect(() => {
175
- let cancelled = false;
176
- async function checkDevices() {
177
- const [hasAudio, hasVideo] = await Promise.all([
178
- requestAudio ? hasMediaDevice("audioinput") : Promise.resolve(false),
179
- requestVideo ? hasMediaDevice("videoinput") : Promise.resolve(false)
180
- ]);
181
- if (!cancelled) {
182
- setState({
183
- audio: requestAudio && hasAudio,
184
- video: requestVideo && hasVideo
185
- });
186
- }
187
- }
188
- checkDevices();
189
- return () => {
190
- cancelled = true;
191
- };
192
- }, [requestAudio, requestVideo]);
193
- return state;
194
- }
195
- var MEDIA_DEVICE_ERROR_NAMES = /* @__PURE__ */ new Set([
196
- "NotAllowedError",
197
- "NotFoundError",
198
- "NotReadableError",
199
- "OverconstrainedError"
200
- ]);
201
- function isMediaDeviceError(error) {
202
- return MEDIA_DEVICE_ERROR_NAMES.has(error.name);
203
- }
204
173
  var DEFAULT_ROOM_OPTIONS = {
205
174
  adaptiveStream: false,
206
175
  dynacast: false
@@ -222,6 +191,7 @@ function mapConnectionState(connectionState) {
222
191
  var AvatarSessionContext = react.createContext(
223
192
  null
224
193
  );
194
+ var MediaDeviceErrorContext = react.createContext(null);
225
195
  function AvatarSession({
226
196
  credentials,
227
197
  children,
@@ -229,16 +199,14 @@ function AvatarSession({
229
199
  video: requestVideo = true,
230
200
  onEnd,
231
201
  onError,
202
+ onClientEvent,
232
203
  initialScreenStream,
233
204
  __unstable_roomOptions
234
205
  }) {
235
206
  const errorRef = react.useRef(null);
236
- const deviceAvailability = useDeviceAvailability(requestAudio, requestVideo);
237
207
  const handleError = (error) => {
238
208
  onError?.(error);
239
- if (!isMediaDeviceError(error)) {
240
- errorRef.current = error;
241
- }
209
+ errorRef.current = error;
242
210
  };
243
211
  const roomOptions = {
244
212
  ...DEFAULT_ROOM_OPTIONS,
@@ -250,8 +218,8 @@ function AvatarSession({
250
218
  serverUrl: credentials.serverUrl,
251
219
  token: credentials.token,
252
220
  connect: true,
253
- audio: deviceAvailability.audio,
254
- video: deviceAvailability.video,
221
+ audio: false,
222
+ video: false,
255
223
  onDisconnected: () => onEnd?.(),
256
224
  onError: handleError,
257
225
  options: roomOptions,
@@ -263,7 +231,10 @@ function AvatarSession({
263
231
  AvatarSessionContextInner,
264
232
  {
265
233
  sessionId: credentials.sessionId,
234
+ requestAudio,
235
+ requestVideo,
266
236
  onEnd,
237
+ onClientEvent,
267
238
  errorRef,
268
239
  initialScreenStream,
269
240
  children
@@ -276,7 +247,10 @@ function AvatarSession({
276
247
  }
277
248
  function AvatarSessionContextInner({
278
249
  sessionId,
250
+ requestAudio,
251
+ requestVideo,
279
252
  onEnd,
253
+ onClientEvent,
280
254
  errorRef,
281
255
  initialScreenStream,
282
256
  children
@@ -285,6 +259,8 @@ function AvatarSessionContextInner({
285
259
  const connectionState = componentsReact.useConnectionState();
286
260
  const onEndRef = react.useRef(onEnd);
287
261
  onEndRef.current = onEnd;
262
+ const onClientEventRef = react.useRef(onClientEvent);
263
+ onClientEventRef.current = onClientEvent;
288
264
  const publishedRef = react.useRef(false);
289
265
  react.useEffect(() => {
290
266
  if (connectionState !== livekitClient.ConnectionState.Connected) return;
@@ -308,6 +284,76 @@ function AvatarSessionContextInner({
308
284
  });
309
285
  };
310
286
  }, [connectionState, initialScreenStream, room]);
287
+ const [micError, setMicError] = react.useState(null);
288
+ const [cameraError, setCameraError] = react.useState(null);
289
+ const mediaEnabledRef = react.useRef(false);
290
+ react.useEffect(() => {
291
+ if (connectionState !== livekitClient.ConnectionState.Connected) return;
292
+ if (mediaEnabledRef.current) return;
293
+ mediaEnabledRef.current = true;
294
+ async function enableMedia() {
295
+ if (requestAudio) {
296
+ try {
297
+ await room.localParticipant.setMicrophoneEnabled(true);
298
+ } catch (err) {
299
+ if (err instanceof Error) setMicError(err);
300
+ }
301
+ }
302
+ if (requestVideo) {
303
+ try {
304
+ await room.localParticipant.setCameraEnabled(true);
305
+ } catch (err) {
306
+ if (err instanceof Error) setCameraError(err);
307
+ }
308
+ }
309
+ }
310
+ enableMedia();
311
+ }, [connectionState, room, requestAudio, requestVideo]);
312
+ react.useEffect(() => {
313
+ function handleMediaDevicesError(error, kind) {
314
+ if (kind === "audioinput") {
315
+ setMicError(error);
316
+ } else if (kind === "videoinput") {
317
+ setCameraError(error);
318
+ }
319
+ }
320
+ room.on(livekitClient.RoomEvent.MediaDevicesError, handleMediaDevicesError);
321
+ return () => {
322
+ room.off(livekitClient.RoomEvent.MediaDevicesError, handleMediaDevicesError);
323
+ };
324
+ }, [room]);
325
+ const retryMic = react.useCallback(async () => {
326
+ try {
327
+ await room.localParticipant.setMicrophoneEnabled(true);
328
+ setMicError(null);
329
+ } catch (err) {
330
+ if (err instanceof Error) setMicError(err);
331
+ }
332
+ }, [room]);
333
+ const retryCamera = react.useCallback(async () => {
334
+ try {
335
+ await room.localParticipant.setCameraEnabled(true);
336
+ setCameraError(null);
337
+ } catch (err) {
338
+ if (err instanceof Error) setCameraError(err);
339
+ }
340
+ }, [room]);
341
+ const mediaDeviceErrors = react.useMemo(
342
+ () => ({ micError, cameraError, retryMic, retryCamera }),
343
+ [micError, cameraError, retryMic, retryCamera]
344
+ );
345
+ react.useEffect(() => {
346
+ function handleDataReceived(payload) {
347
+ const event = parseClientEvent(payload);
348
+ if (event) {
349
+ onClientEventRef.current?.(event);
350
+ }
351
+ }
352
+ room.on(livekitClient.RoomEvent.DataReceived, handleDataReceived);
353
+ return () => {
354
+ room.off(livekitClient.RoomEvent.DataReceived, handleDataReceived);
355
+ };
356
+ }, [room]);
311
357
  const end = react.useCallback(async () => {
312
358
  try {
313
359
  const encoder = new TextEncoder();
@@ -324,7 +370,7 @@ function AvatarSessionContextInner({
324
370
  error: errorRef.current,
325
371
  end
326
372
  };
327
- return /* @__PURE__ */ jsxRuntime.jsx(AvatarSessionContext.Provider, { value: contextValue, children });
373
+ return /* @__PURE__ */ jsxRuntime.jsx(AvatarSessionContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(MediaDeviceErrorContext.Provider, { value: mediaDeviceErrors, children }) });
328
374
  }
329
375
  function useAvatarSessionContext() {
330
376
  const context = react.useContext(AvatarSessionContext);
@@ -335,6 +381,9 @@ function useAvatarSessionContext() {
335
381
  }
336
382
  return context;
337
383
  }
384
+ function useMediaDeviceErrorContext() {
385
+ return react.useContext(MediaDeviceErrorContext);
386
+ }
338
387
  function useAvatar() {
339
388
  const remoteParticipants = componentsReact.useRemoteParticipants();
340
389
  const avatarParticipant = remoteParticipants[0] ?? null;
@@ -370,7 +419,10 @@ function useAvatarStatus() {
370
419
  return { status: "connecting" };
371
420
  case "active":
372
421
  if (hasVideo && videoTrackRef) {
373
- return { status: "ready", videoTrackRef };
422
+ return {
423
+ status: "ready",
424
+ videoTrackRef
425
+ };
374
426
  }
375
427
  return { status: "waiting" };
376
428
  case "ending":
@@ -397,8 +449,16 @@ function AvatarVideo({ children, ...props }) {
397
449
  }
398
450
  );
399
451
  }
452
+ var NOOP_ASYNC = async () => {
453
+ };
400
454
  function useLocalMedia() {
401
455
  const { localParticipant } = componentsReact.useLocalParticipant();
456
+ const {
457
+ micError = null,
458
+ cameraError = null,
459
+ retryMic = NOOP_ASYNC,
460
+ retryCamera = NOOP_ASYNC
461
+ } = useMediaDeviceErrorContext() ?? {};
402
462
  const audioDevices = componentsReact.useMediaDevices({ kind: "audioinput" });
403
463
  const videoDevices = componentsReact.useMediaDevices({ kind: "videoinput" });
404
464
  const hasMic = audioDevices?.length > 0;
@@ -444,7 +504,11 @@ function useLocalMedia() {
444
504
  toggleMic,
445
505
  toggleCamera,
446
506
  toggleScreenShare,
447
- localVideoTrackRef
507
+ localVideoTrackRef,
508
+ micError,
509
+ cameraError,
510
+ retryMic,
511
+ retryCamera
448
512
  };
449
513
  }
450
514
  function ControlBar({
@@ -462,7 +526,11 @@ function ControlBar({
462
526
  isScreenShareEnabled,
463
527
  toggleMic,
464
528
  toggleCamera,
465
- toggleScreenShare
529
+ toggleScreenShare,
530
+ micError,
531
+ cameraError,
532
+ retryMic,
533
+ retryCamera
466
534
  } = useLocalMedia();
467
535
  const isActive = session.state === "active";
468
536
  const state = {
@@ -473,7 +541,11 @@ function ControlBar({
473
541
  toggleCamera,
474
542
  toggleScreenShare,
475
543
  endCall: session.end,
476
- isActive
544
+ isActive,
545
+ micError,
546
+ cameraError,
547
+ retryMic,
548
+ retryCamera
477
549
  };
478
550
  if (children) {
479
551
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children(state) });
@@ -635,6 +707,7 @@ function AvatarCall({
635
707
  avatarImageUrl,
636
708
  onEnd,
637
709
  onError,
710
+ onClientEvent,
638
711
  children,
639
712
  initialScreenStream,
640
713
  __unstable_roomOptions,
@@ -688,6 +761,7 @@ function AvatarCall({
688
761
  video,
689
762
  onEnd,
690
763
  onError: handleSessionError,
764
+ onClientEvent,
691
765
  initialScreenStream,
692
766
  __unstable_roomOptions,
693
767
  children: children ?? defaultChildren
@@ -722,6 +796,78 @@ function ScreenShareVideo({
722
796
  }
723
797
  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 }) });
724
798
  }
799
+ function useClientEvent(toolName, onEvent) {
800
+ const room = componentsReact.useRoomContext();
801
+ const [state, setState] = react.useState(null);
802
+ const onEventRef = react.useRef(onEvent);
803
+ onEventRef.current = onEvent;
804
+ react.useEffect(() => {
805
+ function handleDataReceived(payload) {
806
+ const event = parseClientEvent(payload);
807
+ if (event && event.tool === toolName) {
808
+ const args = event.args;
809
+ setState(args);
810
+ onEventRef.current?.(args);
811
+ }
812
+ }
813
+ room.on(livekitClient.RoomEvent.DataReceived, handleDataReceived);
814
+ return () => {
815
+ room.off(livekitClient.RoomEvent.DataReceived, handleDataReceived);
816
+ };
817
+ }, [room, toolName]);
818
+ return state;
819
+ }
820
+ function useClientEvents(handler) {
821
+ const room = componentsReact.useRoomContext();
822
+ const handlerRef = react.useRef(handler);
823
+ handlerRef.current = handler;
824
+ react.useEffect(() => {
825
+ function handleDataReceived(payload) {
826
+ const event = parseClientEvent(payload);
827
+ if (event) {
828
+ handlerRef.current(event);
829
+ }
830
+ }
831
+ room.on(livekitClient.RoomEvent.DataReceived, handleDataReceived);
832
+ return () => {
833
+ room.off(livekitClient.RoomEvent.DataReceived, handleDataReceived);
834
+ };
835
+ }, [room]);
836
+ }
837
+ function useTranscription(handler, options) {
838
+ const room = componentsReact.useRoomContext();
839
+ const handlerRef = react.useRef(handler);
840
+ handlerRef.current = handler;
841
+ const interimRef = react.useRef(options?.interim ?? false);
842
+ interimRef.current = options?.interim ?? false;
843
+ react.useEffect(() => {
844
+ function handleTranscription(segments, participant) {
845
+ const identity = participant?.identity ?? "unknown";
846
+ for (const segment of segments) {
847
+ if (!interimRef.current && !segment.final) continue;
848
+ handlerRef.current({
849
+ id: segment.id,
850
+ text: segment.text,
851
+ final: segment.final,
852
+ participantIdentity: identity
853
+ });
854
+ }
855
+ }
856
+ room.on(livekitClient.RoomEvent.TranscriptionReceived, handleTranscription);
857
+ return () => {
858
+ room.off(livekitClient.RoomEvent.TranscriptionReceived, handleTranscription);
859
+ };
860
+ }, [room]);
861
+ }
862
+
863
+ // src/tools.ts
864
+ function clientTool(name, config) {
865
+ return {
866
+ type: "client_event",
867
+ name,
868
+ description: config.description
869
+ };
870
+ }
725
871
 
726
872
  Object.defineProperty(exports, "AudioRenderer", {
727
873
  enumerable: true,
@@ -731,15 +877,23 @@ Object.defineProperty(exports, "VideoTrack", {
731
877
  enumerable: true,
732
878
  get: function () { return componentsReact.VideoTrack; }
733
879
  });
880
+ Object.defineProperty(exports, "isTrackReference", {
881
+ enumerable: true,
882
+ get: function () { return componentsReact.isTrackReference; }
883
+ });
734
884
  exports.AvatarCall = AvatarCall;
735
885
  exports.AvatarSession = AvatarSession;
736
886
  exports.AvatarVideo = AvatarVideo;
737
887
  exports.ControlBar = ControlBar;
738
888
  exports.ScreenShareVideo = ScreenShareVideo;
739
889
  exports.UserVideo = UserVideo;
890
+ exports.clientTool = clientTool;
740
891
  exports.useAvatar = useAvatar;
741
892
  exports.useAvatarSession = useAvatarSession;
742
893
  exports.useAvatarStatus = useAvatarStatus;
894
+ exports.useClientEvent = useClientEvent;
895
+ exports.useClientEvents = useClientEvents;
743
896
  exports.useLocalMedia = useLocalMedia;
897
+ exports.useTranscription = useTranscription;
744
898
  //# sourceMappingURL=index.cjs.map
745
899
  //# sourceMappingURL=index.cjs.map