@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/README.md +28 -18
- package/dist/index.cjs +186 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +188 -115
- package/dist/index.js.map +1 -1
- package/dist/styles.css +121 -18
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { LiveKitRoom, RoomAudioRenderer, useRoomContext, useConnectionState, useRemoteParticipants, useTracks, isTrackReference, VideoTrack, useLocalParticipant, useMediaDevices
|
|
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,
|
|
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
|
-
|
|
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:
|
|
270
|
-
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
|
-
|
|
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
|
-
|
|
521
|
-
"
|
|
522
|
-
{
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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: "
|
|
570
|
-
height: "
|
|
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: "
|
|
589
|
-
height: "
|
|
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: "
|
|
607
|
-
height: "
|
|
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("
|
|
617
|
-
/* @__PURE__ */ jsx("
|
|
618
|
-
/* @__PURE__ */ jsx("
|
|
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: "
|
|
626
|
-
height: "
|
|
698
|
+
width: "16",
|
|
699
|
+
height: "16",
|
|
627
700
|
viewBox: "8 14 24 12",
|
|
628
701
|
fill: "currentColor",
|
|
629
702
|
"aria-hidden": "true",
|