@runwayml/avatars-react 0.10.0-beta.0 → 0.11.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,6 +1,6 @@
1
- import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices, TrackToggle } from '@livekit/components-react';
1
+ import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices } from '@livekit/components-react';
2
2
  export { RoomAudioRenderer as AudioRenderer, VideoTrack, isTrackReference } from '@livekit/components-react';
3
- import { createContext, useRef, useEffect, useCallback, useState, useContext, useSyncExternalStore } from 'react';
3
+ import { createContext, useRef, useEffect, useState, useCallback, useMemo, useContext, useSyncExternalStore } from 'react';
4
4
  import { ConnectionState, Track, RoomEvent } from 'livekit-client';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
6
 
@@ -169,53 +169,6 @@ function parseClientEvent(payload) {
169
169
  return null;
170
170
  }
171
171
  }
172
- async function hasMediaDevice(kind, timeoutMs = 1e3) {
173
- try {
174
- const timeoutPromise = new Promise(
175
- (resolve) => setTimeout(() => resolve(false), timeoutMs)
176
- );
177
- const checkPromise = navigator.mediaDevices.enumerateDevices().then((devices) => devices.some((device) => device.kind === kind));
178
- return await Promise.race([checkPromise, timeoutPromise]);
179
- } catch {
180
- return false;
181
- }
182
- }
183
- function useDeviceAvailability(requestAudio, requestVideo) {
184
- const [state, setState] = useState({
185
- audio: requestAudio,
186
- // Optimistically assume devices exist
187
- video: requestVideo
188
- });
189
- useEffect(() => {
190
- let cancelled = false;
191
- async function checkDevices() {
192
- const [hasAudio, hasVideo] = await Promise.all([
193
- requestAudio ? hasMediaDevice("audioinput") : Promise.resolve(false),
194
- requestVideo ? hasMediaDevice("videoinput") : Promise.resolve(false)
195
- ]);
196
- if (!cancelled) {
197
- setState({
198
- audio: requestAudio && hasAudio,
199
- video: requestVideo && hasVideo
200
- });
201
- }
202
- }
203
- checkDevices();
204
- return () => {
205
- cancelled = true;
206
- };
207
- }, [requestAudio, requestVideo]);
208
- return state;
209
- }
210
- var MEDIA_DEVICE_ERROR_NAMES = /* @__PURE__ */ new Set([
211
- "NotAllowedError",
212
- "NotFoundError",
213
- "NotReadableError",
214
- "OverconstrainedError"
215
- ]);
216
- function isMediaDeviceError(error) {
217
- return MEDIA_DEVICE_ERROR_NAMES.has(error.name);
218
- }
219
172
  var DEFAULT_ROOM_OPTIONS = {
220
173
  adaptiveStream: false,
221
174
  dynacast: false
@@ -237,6 +190,7 @@ function mapConnectionState(connectionState) {
237
190
  var AvatarSessionContext = createContext(
238
191
  null
239
192
  );
193
+ var MediaDeviceErrorContext = createContext(null);
240
194
  function AvatarSession({
241
195
  credentials,
242
196
  children,
@@ -249,12 +203,9 @@ function AvatarSession({
249
203
  __unstable_roomOptions
250
204
  }) {
251
205
  const errorRef = useRef(null);
252
- const deviceAvailability = useDeviceAvailability(requestAudio, requestVideo);
253
206
  const handleError = (error) => {
254
207
  onError?.(error);
255
- if (!isMediaDeviceError(error)) {
256
- errorRef.current = error;
257
- }
208
+ errorRef.current = error;
258
209
  };
259
210
  const roomOptions = {
260
211
  ...DEFAULT_ROOM_OPTIONS,
@@ -266,8 +217,8 @@ function AvatarSession({
266
217
  serverUrl: credentials.serverUrl,
267
218
  token: credentials.token,
268
219
  connect: true,
269
- audio: deviceAvailability.audio,
270
- video: deviceAvailability.video,
220
+ audio: false,
221
+ video: false,
271
222
  onDisconnected: () => onEnd?.(),
272
223
  onError: handleError,
273
224
  options: roomOptions,
@@ -279,6 +230,8 @@ function AvatarSession({
279
230
  AvatarSessionContextInner,
280
231
  {
281
232
  sessionId: credentials.sessionId,
233
+ requestAudio,
234
+ requestVideo,
282
235
  onEnd,
283
236
  onClientEvent,
284
237
  errorRef,
@@ -293,6 +246,8 @@ function AvatarSession({
293
246
  }
294
247
  function AvatarSessionContextInner({
295
248
  sessionId,
249
+ requestAudio,
250
+ requestVideo,
296
251
  onEnd,
297
252
  onClientEvent,
298
253
  errorRef,
@@ -328,6 +283,64 @@ function AvatarSessionContextInner({
328
283
  });
329
284
  };
330
285
  }, [connectionState, initialScreenStream, room]);
286
+ const [micError, setMicError] = useState(null);
287
+ const [cameraError, setCameraError] = useState(null);
288
+ const mediaEnabledRef = useRef(false);
289
+ useEffect(() => {
290
+ if (connectionState !== ConnectionState.Connected) return;
291
+ if (mediaEnabledRef.current) return;
292
+ mediaEnabledRef.current = true;
293
+ async function enableMedia() {
294
+ if (requestAudio) {
295
+ try {
296
+ await room.localParticipant.setMicrophoneEnabled(true);
297
+ } catch (err) {
298
+ if (err instanceof Error) setMicError(err);
299
+ }
300
+ }
301
+ if (requestVideo) {
302
+ try {
303
+ await room.localParticipant.setCameraEnabled(true);
304
+ } catch (err) {
305
+ if (err instanceof Error) setCameraError(err);
306
+ }
307
+ }
308
+ }
309
+ enableMedia();
310
+ }, [connectionState, room, requestAudio, requestVideo]);
311
+ useEffect(() => {
312
+ function handleMediaDevicesError(error, kind) {
313
+ if (kind === "audioinput") {
314
+ setMicError(error);
315
+ } else if (kind === "videoinput") {
316
+ setCameraError(error);
317
+ }
318
+ }
319
+ room.on(RoomEvent.MediaDevicesError, handleMediaDevicesError);
320
+ return () => {
321
+ room.off(RoomEvent.MediaDevicesError, handleMediaDevicesError);
322
+ };
323
+ }, [room]);
324
+ const retryMic = useCallback(async () => {
325
+ try {
326
+ await room.localParticipant.setMicrophoneEnabled(true);
327
+ setMicError(null);
328
+ } catch (err) {
329
+ if (err instanceof Error) setMicError(err);
330
+ }
331
+ }, [room]);
332
+ const retryCamera = useCallback(async () => {
333
+ try {
334
+ await room.localParticipant.setCameraEnabled(true);
335
+ setCameraError(null);
336
+ } catch (err) {
337
+ if (err instanceof Error) setCameraError(err);
338
+ }
339
+ }, [room]);
340
+ const mediaDeviceErrors = useMemo(
341
+ () => ({ micError, cameraError, retryMic, retryCamera }),
342
+ [micError, cameraError, retryMic, retryCamera]
343
+ );
331
344
  useEffect(() => {
332
345
  function handleDataReceived(payload) {
333
346
  const event = parseClientEvent(payload);
@@ -356,7 +369,7 @@ function AvatarSessionContextInner({
356
369
  error: errorRef.current,
357
370
  end
358
371
  };
359
- return /* @__PURE__ */ jsx(AvatarSessionContext.Provider, { value: contextValue, children });
372
+ return /* @__PURE__ */ jsx(AvatarSessionContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(MediaDeviceErrorContext.Provider, { value: mediaDeviceErrors, children }) });
360
373
  }
361
374
  function useAvatarSessionContext() {
362
375
  const context = useContext(AvatarSessionContext);
@@ -367,6 +380,9 @@ function useAvatarSessionContext() {
367
380
  }
368
381
  return context;
369
382
  }
383
+ function useMediaDeviceErrorContext() {
384
+ return useContext(MediaDeviceErrorContext);
385
+ }
370
386
  function useAvatar() {
371
387
  const remoteParticipants = useRemoteParticipants();
372
388
  const avatarParticipant = remoteParticipants[0] ?? null;
@@ -432,8 +448,24 @@ function AvatarVideo({ children, ...props }) {
432
448
  }
433
449
  );
434
450
  }
451
+ var NOOP_ASYNC = async () => {
452
+ };
453
+ function createCaptureController() {
454
+ if (typeof window === "undefined" || !("CaptureController" in window)) {
455
+ return void 0;
456
+ }
457
+ const controller = new window.CaptureController();
458
+ controller.setFocusBehavior("no-focus-change");
459
+ return controller;
460
+ }
435
461
  function useLocalMedia() {
436
462
  const { localParticipant } = useLocalParticipant();
463
+ const {
464
+ micError = null,
465
+ cameraError = null,
466
+ retryMic = NOOP_ASYNC,
467
+ retryCamera = NOOP_ASYNC
468
+ } = useMediaDeviceErrorContext() ?? {};
437
469
  const audioDevices = useMediaDevices({ kind: "audioinput" });
438
470
  const videoDevices = useMediaDevices({ kind: "videoinput" });
439
471
  const hasMic = audioDevices?.length > 0;
@@ -457,7 +489,16 @@ function useLocalMedia() {
457
489
  }
458
490
  }, [localParticipant]);
459
491
  const toggleScreenShare = useCallback(() => {
460
- localParticipant?.setScreenShareEnabled(!isScreenShareEnabledRef.current);
492
+ const next = !isScreenShareEnabledRef.current;
493
+ if (next) {
494
+ const controller = createCaptureController();
495
+ localParticipant?.setScreenShareEnabled(true, {
496
+ controller,
497
+ surfaceSwitching: "include"
498
+ });
499
+ } else {
500
+ localParticipant?.setScreenShareEnabled(false);
501
+ }
461
502
  }, [localParticipant]);
462
503
  const tracks = useTracks(
463
504
  [{ source: Track.Source.Camera, withPlaceholder: true }],
@@ -479,7 +520,11 @@ function useLocalMedia() {
479
520
  toggleMic,
480
521
  toggleCamera,
481
522
  toggleScreenShare,
482
- localVideoTrackRef
523
+ localVideoTrackRef,
524
+ micError,
525
+ cameraError,
526
+ retryMic,
527
+ retryCamera
483
528
  };
484
529
  }
485
530
  function ControlBar({
@@ -497,9 +542,17 @@ function ControlBar({
497
542
  isScreenShareEnabled,
498
543
  toggleMic,
499
544
  toggleCamera,
500
- toggleScreenShare
545
+ toggleScreenShare,
546
+ micError,
547
+ cameraError,
548
+ retryMic,
549
+ retryCamera
501
550
  } = useLocalMedia();
502
551
  const isActive = session.state === "active";
552
+ const handleStopScreenShare = useCallback(() => {
553
+ if (!isScreenShareEnabled) return;
554
+ toggleScreenShare();
555
+ }, [isScreenShareEnabled, toggleScreenShare]);
503
556
  const state = {
504
557
  isMicEnabled,
505
558
  isCameraEnabled,
@@ -508,7 +561,11 @@ function ControlBar({
508
561
  toggleCamera,
509
562
  toggleScreenShare,
510
563
  endCall: session.end,
511
- isActive
564
+ isActive,
565
+ micError,
566
+ cameraError,
567
+ retryMic,
568
+ retryCamera
512
569
  };
513
570
  if (children) {
514
571
  return /* @__PURE__ */ jsx(Fragment, { children: children(state) });
@@ -517,57 +574,71 @@ function ControlBar({
517
574
  return null;
518
575
  }
519
576
  return /* @__PURE__ */ jsxs("div", { ...props, "data-avatar-control-bar": "", "data-avatar-active": isActive, children: [
520
- showMicrophone && /* @__PURE__ */ jsx(
521
- "button",
522
- {
523
- type: "button",
524
- onClick: toggleMic,
525
- "data-avatar-control": "microphone",
526
- "data-avatar-enabled": isMicEnabled,
527
- "aria-label": isMicEnabled ? "Mute microphone" : "Unmute microphone",
528
- children: microphoneIcon
529
- }
530
- ),
531
- showCamera && /* @__PURE__ */ jsx(
532
- "button",
533
- {
534
- type: "button",
535
- onClick: toggleCamera,
536
- "data-avatar-control": "camera",
537
- "data-avatar-enabled": isCameraEnabled,
538
- "aria-label": isCameraEnabled ? "Turn off camera" : "Turn on camera",
539
- children: cameraIcon
540
- }
541
- ),
542
- showScreenShare && /* @__PURE__ */ jsx(
543
- TrackToggle,
544
- {
545
- source: Track.Source.ScreenShare,
546
- showIcon: false,
547
- "data-avatar-control": "screen-share",
548
- "data-avatar-enabled": isScreenShareEnabled,
549
- "aria-label": "Toggle screen share",
550
- children: screenShareIcon
551
- }
552
- ),
553
- showEndCall && /* @__PURE__ */ jsx(
554
- "button",
555
- {
556
- type: "button",
557
- onClick: session.end,
558
- "data-avatar-control": "end-call",
559
- "data-avatar-enabled": true,
560
- "aria-label": "End call",
561
- children: phoneIcon
562
- }
563
- )
577
+ showScreenShare && isScreenShareEnabled && /* @__PURE__ */ jsxs("div", { "data-avatar-share-indicator": "", "aria-live": "polite", children: [
578
+ /* @__PURE__ */ jsx("span", { "data-avatar-share-label": "", children: "You're sharing your screen" }),
579
+ /* @__PURE__ */ jsx("div", { "data-avatar-share-actions": "", children: /* @__PURE__ */ jsx(
580
+ "button",
581
+ {
582
+ type: "button",
583
+ onClick: handleStopScreenShare,
584
+ "data-avatar-share-action": "stop",
585
+ children: "Stop"
586
+ }
587
+ ) })
588
+ ] }),
589
+ /* @__PURE__ */ jsxs("div", { "data-avatar-controls": "", children: [
590
+ showMicrophone && /* @__PURE__ */ jsx(
591
+ "button",
592
+ {
593
+ type: "button",
594
+ onClick: toggleMic,
595
+ "data-avatar-control": "microphone",
596
+ "data-avatar-enabled": isMicEnabled,
597
+ "aria-label": isMicEnabled ? "Mute microphone" : "Unmute microphone",
598
+ children: microphoneIcon
599
+ }
600
+ ),
601
+ showCamera && /* @__PURE__ */ jsx(
602
+ "button",
603
+ {
604
+ type: "button",
605
+ onClick: toggleCamera,
606
+ "data-avatar-control": "camera",
607
+ "data-avatar-enabled": isCameraEnabled,
608
+ "aria-label": isCameraEnabled ? "Turn off camera" : "Turn on camera",
609
+ children: cameraIcon
610
+ }
611
+ ),
612
+ showScreenShare && /* @__PURE__ */ jsx(
613
+ "button",
614
+ {
615
+ type: "button",
616
+ onClick: toggleScreenShare,
617
+ "data-avatar-control": "screen-share",
618
+ "data-avatar-enabled": isScreenShareEnabled,
619
+ "aria-label": isScreenShareEnabled ? "Stop sharing screen" : "Share screen",
620
+ children: screenShareIcon
621
+ }
622
+ ),
623
+ showEndCall && /* @__PURE__ */ jsx(
624
+ "button",
625
+ {
626
+ type: "button",
627
+ onClick: session.end,
628
+ "data-avatar-control": "end-call",
629
+ "data-avatar-enabled": true,
630
+ "aria-label": "End call",
631
+ children: phoneIcon
632
+ }
633
+ )
634
+ ] })
564
635
  ] });
565
636
  }
566
637
  var microphoneIcon = /* @__PURE__ */ jsxs(
567
638
  "svg",
568
639
  {
569
- width: "20",
570
- height: "20",
640
+ width: "16",
641
+ height: "16",
571
642
  viewBox: "0 0 24 24",
572
643
  fill: "none",
573
644
  stroke: "currentColor",
@@ -585,8 +656,8 @@ var microphoneIcon = /* @__PURE__ */ jsxs(
585
656
  var cameraIcon = /* @__PURE__ */ jsxs(
586
657
  "svg",
587
658
  {
588
- width: "20",
589
- height: "20",
659
+ width: "16",
660
+ height: "16",
590
661
  viewBox: "0 0 24 24",
591
662
  fill: "none",
592
663
  stroke: "currentColor",
@@ -603,8 +674,8 @@ var cameraIcon = /* @__PURE__ */ jsxs(
603
674
  var screenShareIcon = /* @__PURE__ */ jsxs(
604
675
  "svg",
605
676
  {
606
- width: "20",
607
- height: "20",
677
+ width: "16",
678
+ height: "16",
608
679
  viewBox: "0 0 24 24",
609
680
  fill: "none",
610
681
  stroke: "currentColor",
@@ -613,17 +684,19 @@ var screenShareIcon = /* @__PURE__ */ jsxs(
613
684
  strokeLinejoin: "round",
614
685
  "aria-hidden": "true",
615
686
  children: [
616
- /* @__PURE__ */ jsx("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }),
617
- /* @__PURE__ */ jsx("line", { x1: "8", x2: "16", y1: "21", y2: "21" }),
618
- /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "17", y2: "21" })
687
+ /* @__PURE__ */ jsx("path", { d: "M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3" }),
688
+ /* @__PURE__ */ jsx("path", { d: "M8 21h8" }),
689
+ /* @__PURE__ */ jsx("path", { d: "M12 17v4" }),
690
+ /* @__PURE__ */ jsx("path", { d: "m17 8 5-5" }),
691
+ /* @__PURE__ */ jsx("path", { d: "M17 3h5v5" })
619
692
  ]
620
693
  }
621
694
  );
622
695
  var phoneIcon = /* @__PURE__ */ jsx(
623
696
  "svg",
624
697
  {
625
- width: "20",
626
- height: "20",
698
+ width: "16",
699
+ height: "16",
627
700
  viewBox: "8 14 24 12",
628
701
  fill: "currentColor",
629
702
  "aria-hidden": "true",