@stream-io/video-react-sdk 1.4.5 → 1.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.js CHANGED
@@ -4,10 +4,10 @@ var videoClient = require('@stream-io/video-client');
4
4
  var videoReactBindings = require('@stream-io/video-react-bindings');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var react = require('react');
7
+ var react$1 = require('@floating-ui/react');
7
8
  var clsx = require('clsx');
8
9
  var reactDom = require('react-dom');
9
10
  var videoFiltersWeb = require('@stream-io/video-filters-web');
10
- var react$1 = require('@floating-ui/react');
11
11
 
12
12
  const Audio = ({ participant, trackType = 'audioTrack', ...rest }) => {
13
13
  const call = videoReactBindings.useCall();
@@ -44,167 +44,6 @@ ParticipantsAudio.displayName = 'ParticipantsAudio';
44
44
  const ParticipantViewContext = react.createContext(undefined);
45
45
  const useParticipantViewContext = () => react.useContext(ParticipantViewContext);
46
46
 
47
- const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
48
- const [error, setError] = react.useState(false);
49
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(!imageSrc || error) && name && (jsxRuntime.jsx(AvatarFallback, { className: className, style: style, names: [name] })), imageSrc && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: "avatar", className: clsx('str-video__avatar', className), src: imageSrc, style: style, ...rest }))] }));
50
- };
51
- const AvatarFallback = ({ className, names, style, }) => {
52
- return (jsxRuntime.jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxRuntime.jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
53
- };
54
-
55
- /**
56
- * The context for the background filters.
57
- */
58
- const BackgroundFiltersContext = react.createContext(undefined);
59
- /**
60
- * A hook to access the background filters context API.
61
- */
62
- const useBackgroundFilters = () => {
63
- const context = react.useContext(BackgroundFiltersContext);
64
- if (!context) {
65
- throw new Error('useBackgroundFilters must be used within a BackgroundFiltersProvider');
66
- }
67
- return context;
68
- };
69
- /**
70
- * A provider component that enables the use of background filters in your app.
71
- *
72
- * Please make sure you have the `@stream-io/video-filters-web` package installed
73
- * in your project before using this component.
74
- */
75
- const BackgroundFiltersProvider = (props) => {
76
- const { children, backgroundImages = [], backgroundFilter: bgFilterFromProps = undefined, backgroundImage: bgImageFromProps = undefined, backgroundBlurLevel: bgBlurLevelFromProps = 'high', tfFilePath, modelFilePath, basePath, onError, } = props;
77
- const [backgroundFilter, setBackgroundFilter] = react.useState(bgFilterFromProps);
78
- const [backgroundImage, setBackgroundImage] = react.useState(bgImageFromProps);
79
- const [backgroundBlurLevel, setBackgroundBlurLevel] = react.useState(bgBlurLevelFromProps);
80
- const applyBackgroundImageFilter = react.useCallback((imageUrl) => {
81
- setBackgroundFilter('image');
82
- setBackgroundImage(imageUrl);
83
- }, []);
84
- const applyBackgroundBlurFilter = react.useCallback((blurLevel = 'high') => {
85
- setBackgroundFilter('blur');
86
- setBackgroundBlurLevel(blurLevel);
87
- }, []);
88
- const disableBackgroundFilter = react.useCallback(() => {
89
- setBackgroundFilter(undefined);
90
- setBackgroundImage(undefined);
91
- setBackgroundBlurLevel('high');
92
- }, []);
93
- const [isSupported, setIsSupported] = react.useState(false);
94
- react.useEffect(() => {
95
- videoFiltersWeb.isPlatformSupported().then(setIsSupported);
96
- }, []);
97
- const [tfLite, setTfLite] = react.useState();
98
- react.useEffect(() => {
99
- // don't try to load TFLite if the platform is not supported
100
- if (!isSupported)
101
- return;
102
- videoFiltersWeb.loadTFLite({ basePath, modelFilePath, tfFilePath })
103
- .then(setTfLite)
104
- .catch((err) => console.error('Failed to load TFLite', err));
105
- }, [basePath, isSupported, modelFilePath, tfFilePath]);
106
- const handleError = react.useCallback((error) => {
107
- videoClient.getLogger(['filters'])('warn', 'Filter encountered an error and will be disabled');
108
- disableBackgroundFilter();
109
- onError?.(error);
110
- }, [disableBackgroundFilter, onError]);
111
- return (jsxRuntime.jsxs(BackgroundFiltersContext.Provider, { value: {
112
- isSupported,
113
- isReady: !!tfLite,
114
- backgroundImage,
115
- backgroundBlurLevel,
116
- backgroundFilter,
117
- disableBackgroundFilter,
118
- applyBackgroundBlurFilter,
119
- applyBackgroundImageFilter,
120
- backgroundImages,
121
- tfFilePath,
122
- modelFilePath,
123
- basePath,
124
- onError: handleError,
125
- }, children: [children, tfLite && jsxRuntime.jsx(BackgroundFilters, { tfLite: tfLite })] }));
126
- };
127
- const BackgroundFilters = (props) => {
128
- const call = videoReactBindings.useCall();
129
- const { children, start } = useRenderer(props.tfLite);
130
- const { backgroundFilter, onError } = useBackgroundFilters();
131
- const handleErrorRef = react.useRef(undefined);
132
- handleErrorRef.current = onError;
133
- react.useEffect(() => {
134
- if (!call || !backgroundFilter)
135
- return;
136
- const { unregister } = call.camera.registerFilter((ms) => start(ms, (error) => handleErrorRef.current?.(error)));
137
- return () => {
138
- unregister();
139
- };
140
- }, [backgroundFilter, call, start]);
141
- return children;
142
- };
143
- const useRenderer = (tfLite) => {
144
- const { backgroundFilter, backgroundBlurLevel, backgroundImage } = useBackgroundFilters();
145
- const videoRef = react.useRef(null);
146
- const canvasRef = react.useRef(null);
147
- const bgImageRef = react.useRef(null);
148
- const [videoSize, setVideoSize] = react.useState({
149
- width: 1920,
150
- height: 1080,
151
- });
152
- const start = react.useCallback((ms, onError) => {
153
- let outputStream;
154
- let renderer;
155
- const output = new Promise((resolve, reject) => {
156
- if (!backgroundFilter) {
157
- reject(new Error('No filter specified'));
158
- return;
159
- }
160
- const videoEl = videoRef.current;
161
- const canvasEl = canvasRef.current;
162
- const bgImageEl = bgImageRef.current;
163
- if (!videoEl || !canvasEl || (backgroundImage && !bgImageEl)) {
164
- // You should start renderer in effect or event handlers
165
- reject(new Error('Renderer started before elements are ready'));
166
- return;
167
- }
168
- videoEl.srcObject = ms;
169
- videoEl.play().then(() => {
170
- const [track] = ms.getVideoTracks();
171
- if (!track) {
172
- reject(new Error('No video tracks in input media stream'));
173
- return;
174
- }
175
- const trackSettings = track.getSettings();
176
- reactDom.flushSync(() => setVideoSize({
177
- width: trackSettings.width ?? 0,
178
- height: trackSettings.height ?? 0,
179
- }));
180
- renderer = videoFiltersWeb.createRenderer(tfLite, videoEl, canvasEl, {
181
- backgroundFilter,
182
- backgroundBlurLevel,
183
- backgroundImage: bgImageEl ?? undefined,
184
- }, onError);
185
- outputStream = canvasEl.captureStream();
186
- resolve(outputStream);
187
- }, () => {
188
- reject(new Error('Could not play the source video stream'));
189
- });
190
- });
191
- return {
192
- output,
193
- stop: () => {
194
- renderer?.dispose();
195
- videoRef.current && (videoRef.current.srcObject = null);
196
- outputStream && videoClient.disposeOfMediaStream(outputStream);
197
- },
198
- };
199
- }, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
200
- const children = (jsxRuntime.jsxs("div", { className: "str-video__background-filters", children: [jsxRuntime.jsx("video", { className: clsx('str-video__background-filters__video', videoSize.height > videoSize.width &&
201
- 'str-video__background-filters__video--tall'), ref: videoRef, playsInline: true, muted: true, controls: false, ...videoSize }), backgroundImage && (jsxRuntime.jsx("img", { className: "str-video__background-filters__background-image", alt: "Background", ref: bgImageRef, src: backgroundImage, ...videoSize })), jsxRuntime.jsx("canvas", { className: "str-video__background-filters__target-canvas", ...videoSize, ref: canvasRef })] }));
202
- return {
203
- start,
204
- children,
205
- };
206
- };
207
-
208
47
  const useFloatingUIPreset = ({ middleware = [], placement, strategy, offset: offsetInPx = 10, }) => {
209
48
  const { refs, x, y, update, elements: { domReference, floating }, context, } = react$1.useFloating({
210
49
  placement,
@@ -552,8 +391,426 @@ const GenericMenuButtonItem = ({ children, ...rest }) => {
552
391
  return (jsxRuntime.jsx("li", { className: "str-video__generic-menu--item", children: jsxRuntime.jsx("button", { ...rest, children: children }) }));
553
392
  };
554
393
 
555
- const Icon = ({ className, icon }) => (jsxRuntime.jsx("span", { className: clsx('str-video__icon', icon && `str-video__icon--${icon}`, className) }));
556
-
394
+ const Icon = ({ className, icon }) => (jsxRuntime.jsx("span", { className: clsx('str-video__icon', icon && `str-video__icon--${icon}`, className) }));
395
+
396
+ const ParticipantActionsContextMenu = () => {
397
+ const { participant, participantViewElement, videoElement } = useParticipantViewContext();
398
+ const [fullscreenModeOn, setFullscreenModeOn] = react.useState(!!document.fullscreenElement);
399
+ const [pictureInPictureElement, setPictureInPictureElement] = react.useState(document.pictureInPictureElement);
400
+ const call = videoReactBindings.useCall();
401
+ const { t } = videoReactBindings.useI18n();
402
+ const { pin, sessionId, userId } = participant;
403
+ const hasAudioTrack = videoClient.hasAudio(participant);
404
+ const hasVideoTrack = videoClient.hasVideo(participant);
405
+ const hasScreenShareTrack = videoClient.hasScreenShare(participant);
406
+ const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
407
+ const blockUser = () => call?.blockUser(userId);
408
+ const muteAudio = () => call?.muteUser(userId, 'audio');
409
+ const muteVideo = () => call?.muteUser(userId, 'video');
410
+ const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
411
+ const muteScreenShareAudio = () => call?.muteUser(userId, 'screenshare_audio');
412
+ const grantPermission = (permission) => () => {
413
+ call?.updateUserPermissions({
414
+ user_id: userId,
415
+ grant_permissions: [permission],
416
+ });
417
+ };
418
+ const revokePermission = (permission) => () => {
419
+ call?.updateUserPermissions({
420
+ user_id: userId,
421
+ revoke_permissions: [permission],
422
+ });
423
+ };
424
+ const toggleParticipantPin = () => {
425
+ if (pin) {
426
+ call?.unpin(sessionId);
427
+ }
428
+ else {
429
+ call?.pin(sessionId);
430
+ }
431
+ };
432
+ const pinForEveryone = () => {
433
+ call
434
+ ?.pinForEveryone({
435
+ user_id: userId,
436
+ session_id: sessionId,
437
+ })
438
+ .catch((err) => {
439
+ console.error(`Failed to pin participant ${userId}`, err);
440
+ });
441
+ };
442
+ const unpinForEveryone = () => {
443
+ call
444
+ ?.unpinForEveryone({
445
+ user_id: userId,
446
+ session_id: sessionId,
447
+ })
448
+ .catch((err) => {
449
+ console.error(`Failed to unpin participant ${userId}`, err);
450
+ });
451
+ };
452
+ const toggleFullscreenMode = () => {
453
+ if (!fullscreenModeOn) {
454
+ return participantViewElement?.requestFullscreen().catch(console.error);
455
+ }
456
+ return document.exitFullscreen().catch(console.error);
457
+ };
458
+ react.useEffect(() => {
459
+ // handles the case when fullscreen mode is toggled externally,
460
+ // e.g., by pressing ESC key or some other keyboard shortcut
461
+ const handleFullscreenChange = () => {
462
+ setFullscreenModeOn(!!document.fullscreenElement);
463
+ };
464
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
465
+ return () => {
466
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
467
+ };
468
+ }, []);
469
+ react.useEffect(() => {
470
+ if (!videoElement)
471
+ return;
472
+ const handlePiP = () => {
473
+ setPictureInPictureElement(document.pictureInPictureElement);
474
+ };
475
+ videoElement.addEventListener('enterpictureinpicture', handlePiP);
476
+ videoElement.addEventListener('leavepictureinpicture', handlePiP);
477
+ return () => {
478
+ videoElement.removeEventListener('enterpictureinpicture', handlePiP);
479
+ videoElement.removeEventListener('leavepictureinpicture', handlePiP);
480
+ };
481
+ }, [videoElement]);
482
+ const togglePictureInPicture = () => {
483
+ if (videoElement && pictureInPictureElement !== videoElement) {
484
+ return videoElement
485
+ .requestPictureInPicture()
486
+ .catch(console.error);
487
+ }
488
+ return document.exitPictureInPicture().catch(console.error);
489
+ };
490
+ const { close } = useMenuContext() || {};
491
+ return (jsxRuntime.jsxs(GenericMenu, { onItemClick: close, children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.PIN_FOR_EVERYONE], children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.BLOCK_USERS], children: jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsxRuntime.jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], children: [hasVideoTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsxRuntime.jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShareTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
492
+ direction: fullscreenModeOn ? t('Leave') : t('Enter'),
493
+ }) })), videoElement && document.pictureInPictureEnabled && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
494
+ direction: pictureInPictureElement === videoElement
495
+ ? t('Leave')
496
+ : t('Enter'),
497
+ }) })), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
498
+ };
499
+
500
+ const isComponentType = (elementOrComponent) => {
501
+ return elementOrComponent === null
502
+ ? false
503
+ : !react.isValidElement(elementOrComponent);
504
+ };
505
+
506
+ const chunk = (array, size) => {
507
+ const chunkCount = Math.ceil(array.length / size);
508
+ return Array.from({ length: chunkCount }, (_, index) => array.slice(size * index, size * index + size));
509
+ };
510
+
511
+ const applyElementToRef = (ref, element) => {
512
+ if (!ref)
513
+ return;
514
+ if (typeof ref === 'function')
515
+ return ref(element);
516
+ ref.current = element;
517
+ };
518
+
519
+ /**
520
+ * @description Extends video element with `stream` property
521
+ * (`srcObject`) to reactively handle stream changes
522
+ */
523
+ const BaseVideo = react.forwardRef(function BaseVideo({ stream, ...rest }, ref) {
524
+ const [videoElement, setVideoElement] = react.useState(null);
525
+ react.useEffect(() => {
526
+ if (!videoElement || !stream)
527
+ return;
528
+ if (stream === videoElement.srcObject)
529
+ return;
530
+ videoElement.srcObject = stream;
531
+ if (videoClient.Browsers.isSafari() || videoClient.Browsers.isFirefox()) {
532
+ // Firefox and Safari have some timing issue
533
+ setTimeout(() => {
534
+ videoElement.srcObject = stream;
535
+ videoElement.play().catch((e) => {
536
+ console.error(`Failed to play stream`, e);
537
+ });
538
+ }, 0);
539
+ }
540
+ return () => {
541
+ videoElement.pause();
542
+ videoElement.srcObject = null;
543
+ };
544
+ }, [stream, videoElement]);
545
+ return (jsxRuntime.jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
546
+ applyElementToRef(ref, element);
547
+ setVideoElement(element);
548
+ } }));
549
+ });
550
+
551
+ const DefaultVideoPlaceholder = react.forwardRef(function DefaultVideoPlaceholder({ participant, style }, ref) {
552
+ const { t } = videoReactBindings.useI18n();
553
+ const [error, setError] = react.useState(false);
554
+ const name = participant.name || participant.userId;
555
+ return (jsxRuntime.jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
556
+ (name ? (jsxRuntime.jsx(InitialsFallback, { name: name })) : (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__no-video-label", children: t('Video is disabled') }))), participant.image && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: "video-placeholder", className: "str-video__video-placeholder__avatar", src: participant.image }))] }));
557
+ });
558
+ const InitialsFallback = (props) => {
559
+ const { name } = props;
560
+ const initials = name
561
+ .split(' ')
562
+ .slice(0, 2)
563
+ .map((n) => n[0])
564
+ .join('');
565
+ return (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: initials }));
566
+ };
567
+
568
+ const Video$1 = ({ enabled, mirror, trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
569
+ const { sessionId, videoStream, screenShareStream, viewportVisibilityState, isLocalParticipant, userId, } = participant;
570
+ const call = videoReactBindings.useCall();
571
+ const [videoElement, setVideoElement] = react.useState(null);
572
+ // start with true, will flip once the video starts playing
573
+ const [isVideoPaused, setIsVideoPaused] = react.useState(true);
574
+ const [isWideMode, setIsWideMode] = react.useState(true);
575
+ const stream = trackType === 'videoTrack'
576
+ ? videoStream
577
+ : trackType === 'screenShareTrack'
578
+ ? screenShareStream
579
+ : undefined;
580
+ react.useLayoutEffect(() => {
581
+ if (!call || !videoElement || trackType === 'none')
582
+ return;
583
+ const cleanup = call.bindVideoElement(videoElement, sessionId, trackType);
584
+ return () => {
585
+ cleanup?.();
586
+ };
587
+ }, [call, trackType, sessionId, videoElement]);
588
+ react.useEffect(() => {
589
+ if (!stream || !videoElement)
590
+ return;
591
+ const [track] = stream.getVideoTracks();
592
+ if (!track)
593
+ return;
594
+ const handlePlayPause = () => {
595
+ setIsVideoPaused(videoElement.paused);
596
+ const { width = 0, height = 0 } = track.getSettings();
597
+ setIsWideMode(width >= height);
598
+ };
599
+ // playback may have started before we had a chance to
600
+ // attach the 'play/pause' event listener, so we set the state
601
+ // here to make sure it's in sync
602
+ setIsVideoPaused(videoElement.paused);
603
+ videoElement.addEventListener('play', handlePlayPause);
604
+ videoElement.addEventListener('pause', handlePlayPause);
605
+ track.addEventListener('unmute', handlePlayPause);
606
+ return () => {
607
+ videoElement.removeEventListener('play', handlePlayPause);
608
+ videoElement.removeEventListener('pause', handlePlayPause);
609
+ track.removeEventListener('unmute', handlePlayPause);
610
+ // reset the 'pause' state once we unmount the video element
611
+ setIsVideoPaused(true);
612
+ };
613
+ }, [stream, videoElement]);
614
+ if (!call)
615
+ return null;
616
+ const isPublishingTrack = trackType === 'videoTrack'
617
+ ? videoClient.hasVideo(participant)
618
+ : trackType === 'screenShareTrack'
619
+ ? videoClient.hasScreenShare(participant)
620
+ : false;
621
+ const isInvisible = trackType === 'none' ||
622
+ viewportVisibilityState?.[trackType] === videoClient.VisibilityState.INVISIBLE;
623
+ const hasNoVideoOrInvisible = !enabled || !isPublishingTrack || isInvisible;
624
+ const mirrorVideo = mirror === undefined
625
+ ? isLocalParticipant && trackType === 'videoTrack'
626
+ : mirror;
627
+ const isScreenShareTrack = trackType === 'screenShareTrack';
628
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!hasNoVideoOrInvisible && (jsxRuntime.jsx("video", { ...rest, className: clsx('str-video__video', className, {
629
+ 'str-video__video--not-playing': isVideoPaused,
630
+ 'str-video__video--tall': !isWideMode,
631
+ 'str-video__video--mirror': mirrorVideo,
632
+ 'str-video__video--screen-share': isScreenShareTrack,
633
+ }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
634
+ setVideoElement(element);
635
+ refs?.setVideoElement?.(element);
636
+ } })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsxRuntime.jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
637
+ };
638
+ Video$1.displayName = 'Video';
639
+
640
+ const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
641
+ const call = videoReactBindings.useCall();
642
+ const manager = propsDynascaleManager ?? call?.dynascaleManager;
643
+ react.useEffect(() => {
644
+ if (!trackedElement || !manager || !call || trackType === 'none')
645
+ return;
646
+ const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
647
+ return () => {
648
+ unobserve();
649
+ };
650
+ }, [trackedElement, manager, call, sessionId, trackType]);
651
+ };
652
+
653
+ const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
654
+ const [error, setError] = react.useState(false);
655
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(!imageSrc || error) && name && (jsxRuntime.jsx(AvatarFallback, { className: className, style: style, names: [name] })), imageSrc && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: "avatar", className: clsx('str-video__avatar', className), src: imageSrc, style: style, ...rest }))] }));
656
+ };
657
+ const AvatarFallback = ({ className, names, style, }) => {
658
+ return (jsxRuntime.jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxRuntime.jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
659
+ };
660
+
661
+ /**
662
+ * The context for the background filters.
663
+ */
664
+ const BackgroundFiltersContext = react.createContext(undefined);
665
+ /**
666
+ * A hook to access the background filters context API.
667
+ */
668
+ const useBackgroundFilters = () => {
669
+ const context = react.useContext(BackgroundFiltersContext);
670
+ if (!context) {
671
+ throw new Error('useBackgroundFilters must be used within a BackgroundFiltersProvider');
672
+ }
673
+ return context;
674
+ };
675
+ /**
676
+ * A provider component that enables the use of background filters in your app.
677
+ *
678
+ * Please make sure you have the `@stream-io/video-filters-web` package installed
679
+ * in your project before using this component.
680
+ */
681
+ const BackgroundFiltersProvider = (props) => {
682
+ const { children, backgroundImages = [], backgroundFilter: bgFilterFromProps = undefined, backgroundImage: bgImageFromProps = undefined, backgroundBlurLevel: bgBlurLevelFromProps = 'high', tfFilePath, modelFilePath, basePath, onError, } = props;
683
+ const [backgroundFilter, setBackgroundFilter] = react.useState(bgFilterFromProps);
684
+ const [backgroundImage, setBackgroundImage] = react.useState(bgImageFromProps);
685
+ const [backgroundBlurLevel, setBackgroundBlurLevel] = react.useState(bgBlurLevelFromProps);
686
+ const applyBackgroundImageFilter = react.useCallback((imageUrl) => {
687
+ setBackgroundFilter('image');
688
+ setBackgroundImage(imageUrl);
689
+ }, []);
690
+ const applyBackgroundBlurFilter = react.useCallback((blurLevel = 'high') => {
691
+ setBackgroundFilter('blur');
692
+ setBackgroundBlurLevel(blurLevel);
693
+ }, []);
694
+ const disableBackgroundFilter = react.useCallback(() => {
695
+ setBackgroundFilter(undefined);
696
+ setBackgroundImage(undefined);
697
+ setBackgroundBlurLevel('high');
698
+ }, []);
699
+ const [isSupported, setIsSupported] = react.useState(false);
700
+ react.useEffect(() => {
701
+ videoFiltersWeb.isPlatformSupported().then(setIsSupported);
702
+ }, []);
703
+ const [tfLite, setTfLite] = react.useState();
704
+ react.useEffect(() => {
705
+ // don't try to load TFLite if the platform is not supported
706
+ if (!isSupported)
707
+ return;
708
+ videoFiltersWeb.loadTFLite({ basePath, modelFilePath, tfFilePath })
709
+ .then(setTfLite)
710
+ .catch((err) => console.error('Failed to load TFLite', err));
711
+ }, [basePath, isSupported, modelFilePath, tfFilePath]);
712
+ const handleError = react.useCallback((error) => {
713
+ videoClient.getLogger(['filters'])('warn', 'Filter encountered an error and will be disabled');
714
+ disableBackgroundFilter();
715
+ onError?.(error);
716
+ }, [disableBackgroundFilter, onError]);
717
+ return (jsxRuntime.jsxs(BackgroundFiltersContext.Provider, { value: {
718
+ isSupported,
719
+ isReady: !!tfLite,
720
+ backgroundImage,
721
+ backgroundBlurLevel,
722
+ backgroundFilter,
723
+ disableBackgroundFilter,
724
+ applyBackgroundBlurFilter,
725
+ applyBackgroundImageFilter,
726
+ backgroundImages,
727
+ tfFilePath,
728
+ modelFilePath,
729
+ basePath,
730
+ onError: handleError,
731
+ }, children: [children, tfLite && jsxRuntime.jsx(BackgroundFilters, { tfLite: tfLite })] }));
732
+ };
733
+ const BackgroundFilters = (props) => {
734
+ const call = videoReactBindings.useCall();
735
+ const { children, start } = useRenderer(props.tfLite);
736
+ const { backgroundFilter, onError } = useBackgroundFilters();
737
+ const handleErrorRef = react.useRef(undefined);
738
+ handleErrorRef.current = onError;
739
+ react.useEffect(() => {
740
+ if (!call || !backgroundFilter)
741
+ return;
742
+ const { unregister } = call.camera.registerFilter((ms) => start(ms, (error) => handleErrorRef.current?.(error)));
743
+ return () => {
744
+ unregister();
745
+ };
746
+ }, [backgroundFilter, call, start]);
747
+ return children;
748
+ };
749
+ const useRenderer = (tfLite) => {
750
+ const { backgroundFilter, backgroundBlurLevel, backgroundImage } = useBackgroundFilters();
751
+ const videoRef = react.useRef(null);
752
+ const canvasRef = react.useRef(null);
753
+ const bgImageRef = react.useRef(null);
754
+ const [videoSize, setVideoSize] = react.useState({
755
+ width: 1920,
756
+ height: 1080,
757
+ });
758
+ const start = react.useCallback((ms, onError) => {
759
+ let outputStream;
760
+ let renderer;
761
+ const output = new Promise((resolve, reject) => {
762
+ if (!backgroundFilter) {
763
+ reject(new Error('No filter specified'));
764
+ return;
765
+ }
766
+ const videoEl = videoRef.current;
767
+ const canvasEl = canvasRef.current;
768
+ const bgImageEl = bgImageRef.current;
769
+ if (!videoEl || !canvasEl || (backgroundImage && !bgImageEl)) {
770
+ // You should start renderer in effect or event handlers
771
+ reject(new Error('Renderer started before elements are ready'));
772
+ return;
773
+ }
774
+ videoEl.srcObject = ms;
775
+ videoEl.play().then(() => {
776
+ const [track] = ms.getVideoTracks();
777
+ if (!track) {
778
+ reject(new Error('No video tracks in input media stream'));
779
+ return;
780
+ }
781
+ const trackSettings = track.getSettings();
782
+ reactDom.flushSync(() => setVideoSize({
783
+ width: trackSettings.width ?? 0,
784
+ height: trackSettings.height ?? 0,
785
+ }));
786
+ renderer = videoFiltersWeb.createRenderer(tfLite, videoEl, canvasEl, {
787
+ backgroundFilter,
788
+ backgroundBlurLevel,
789
+ backgroundImage: bgImageEl ?? undefined,
790
+ }, onError);
791
+ outputStream = canvasEl.captureStream();
792
+ resolve(outputStream);
793
+ }, () => {
794
+ reject(new Error('Could not play the source video stream'));
795
+ });
796
+ });
797
+ return {
798
+ output,
799
+ stop: () => {
800
+ renderer?.dispose();
801
+ videoRef.current && (videoRef.current.srcObject = null);
802
+ outputStream && videoClient.disposeOfMediaStream(outputStream);
803
+ },
804
+ };
805
+ }, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
806
+ const children = (jsxRuntime.jsxs("div", { className: "str-video__background-filters", children: [jsxRuntime.jsx("video", { className: clsx('str-video__background-filters__video', videoSize.height > videoSize.width &&
807
+ 'str-video__background-filters__video--tall'), ref: videoRef, playsInline: true, muted: true, controls: false, ...videoSize }), backgroundImage && (jsxRuntime.jsx("img", { className: "str-video__background-filters__background-image", alt: "Background", ref: bgImageRef, src: backgroundImage, ...videoSize })), jsxRuntime.jsx("canvas", { className: "str-video__background-filters__target-canvas", ...videoSize, ref: canvasRef })] }));
808
+ return {
809
+ start,
810
+ children,
811
+ };
812
+ };
813
+
557
814
  const IconButton = react.forwardRef(function IconButton(props, ref) {
558
815
  const { icon, enabled, variant, onClick, className, ...rest } = props;
559
816
  return (jsxRuntime.jsx("button", { className: clsx('str-video__call-controls__button', className, {
@@ -565,26 +822,7 @@ const IconButton = react.forwardRef(function IconButton(props, ref) {
565
822
  }, ref: ref, ...rest, children: jsxRuntime.jsx(Icon, { icon: icon }) }));
566
823
  });
567
824
 
568
- const isComponentType = (elementOrComponent) => {
569
- return elementOrComponent === null
570
- ? false
571
- : !react.isValidElement(elementOrComponent);
572
- };
573
-
574
- const chunk = (array, size) => {
575
- const chunkCount = Math.ceil(array.length / size);
576
- return Array.from({ length: chunkCount }, (_, index) => array.slice(size * index, size * index + size));
577
- };
578
-
579
- const applyElementToRef = (ref, element) => {
580
- if (!ref)
581
- return;
582
- if (typeof ref === 'function')
583
- return ref(element);
584
- ref.current = element;
585
- };
586
-
587
- const CompositeButton = react.forwardRef(function CompositeButton({ caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
825
+ const CompositeButton = react.forwardRef(function CompositeButton({ disabled, caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
588
826
  return (jsxRuntime.jsxs("div", { className: clsx('str-video__composite-button', className, {
589
827
  'str-video__composite-button--caption': caption,
590
828
  'str-video__composite-button--menu': Menu,
@@ -592,10 +830,11 @@ const CompositeButton = react.forwardRef(function CompositeButton({ caption, chi
592
830
  'str-video__composite-button__button-group--active': active,
593
831
  'str-video__composite-button__button-group--active-primary': active && variant === 'primary',
594
832
  'str-video__composite-button__button-group--active-secondary': active && variant === 'secondary',
833
+ 'str-video__composite-button__button-group--disabled': disabled,
595
834
  }), children: [jsxRuntime.jsx("button", { type: "button", className: "str-video__composite-button__button", onClick: (e) => {
596
835
  e.preventDefault();
597
836
  onClick?.(e);
598
- }, ...restButtonProps, children: children }), Menu && (jsxRuntime.jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, onToggle: onMenuToggle, children: isComponentType(Menu) ? jsxRuntime.jsx(Menu, {}) : Menu }))] }), caption && (jsxRuntime.jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
837
+ }, disabled: disabled, ...restButtonProps, children: children }), Menu && (jsxRuntime.jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, onToggle: onMenuToggle, children: isComponentType(Menu) ? jsxRuntime.jsx(Menu, {}) : Menu }))] }), caption && (jsxRuntime.jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
599
838
  });
600
839
  const DefaultToggleMenuButton = react.forwardRef(function DefaultToggleMenuButton({ menuShown }, ref) {
601
840
  return (jsxRuntime.jsx(IconButton, { className: clsx('str-video__menu-toggle-button', {
@@ -966,7 +1205,7 @@ const DropDownSelectOption = (props) => {
966
1205
  'str-video__dropdown-option--selected': selected,
967
1206
  }), ref: ref, ...getItemProps({
968
1207
  onClick: () => handleSelect(index),
969
- }), children: [jsxRuntime.jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsxRuntime.jsx("span", { className: "str-video__dropdown-label", children: label })] }));
1208
+ }), children: [icon && jsxRuntime.jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsxRuntime.jsx("span", { className: "str-video__dropdown-label", children: label })] }));
970
1209
  };
971
1210
  const DropDownSelect = (props) => {
972
1211
  const { children, icon, handleSelect, defaultSelectedLabel, defaultSelectedIndex, } = props;
@@ -1771,125 +2010,6 @@ const StreamTheme = ({ as: Component = 'div', className, children, ...props }) =
1771
2010
  return (jsxRuntime.jsx(Component, { ...props, className: clsx('str-video', className), children: children }));
1772
2011
  };
1773
2012
 
1774
- const DefaultVideoPlaceholder = react.forwardRef(function DefaultVideoPlaceholder({ participant, style }, ref) {
1775
- const { t } = videoReactBindings.useI18n();
1776
- const [error, setError] = react.useState(false);
1777
- const name = participant.name || participant.userId;
1778
- return (jsxRuntime.jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
1779
- (name ? (jsxRuntime.jsx(InitialsFallback, { name: name })) : (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__no-video-label", children: t('Video is disabled') }))), participant.image && !error && (jsxRuntime.jsx("img", { onError: () => setError(true), alt: "video-placeholder", className: "str-video__video-placeholder__avatar", src: participant.image }))] }));
1780
- });
1781
- const InitialsFallback = (props) => {
1782
- const { name } = props;
1783
- const initials = name
1784
- .split(' ')
1785
- .slice(0, 2)
1786
- .map((n) => n[0])
1787
- .join('');
1788
- return (jsxRuntime.jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: initials }));
1789
- };
1790
-
1791
- const Video$1 = ({ trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
1792
- const { sessionId, videoStream, screenShareStream, viewportVisibilityState, isLocalParticipant, userId, } = participant;
1793
- const call = videoReactBindings.useCall();
1794
- const [videoElement, setVideoElement] = react.useState(null);
1795
- // start with true, will flip once the video starts playing
1796
- const [isVideoPaused, setIsVideoPaused] = react.useState(true);
1797
- const [isWideMode, setIsWideMode] = react.useState(true);
1798
- const stream = trackType === 'videoTrack'
1799
- ? videoStream
1800
- : trackType === 'screenShareTrack'
1801
- ? screenShareStream
1802
- : undefined;
1803
- react.useLayoutEffect(() => {
1804
- if (!call || !videoElement || trackType === 'none')
1805
- return;
1806
- const cleanup = call.bindVideoElement(videoElement, sessionId, trackType);
1807
- return () => {
1808
- cleanup?.();
1809
- };
1810
- }, [call, trackType, sessionId, videoElement]);
1811
- react.useEffect(() => {
1812
- if (!stream || !videoElement)
1813
- return;
1814
- const [track] = stream.getVideoTracks();
1815
- if (!track)
1816
- return;
1817
- const handlePlayPause = () => {
1818
- setIsVideoPaused(videoElement.paused);
1819
- const { width = 0, height = 0 } = track.getSettings();
1820
- setIsWideMode(width >= height);
1821
- };
1822
- // playback may have started before we had a chance to
1823
- // attach the 'play/pause' event listener, so we set the state
1824
- // here to make sure it's in sync
1825
- setIsVideoPaused(videoElement.paused);
1826
- videoElement.addEventListener('play', handlePlayPause);
1827
- videoElement.addEventListener('pause', handlePlayPause);
1828
- track.addEventListener('unmute', handlePlayPause);
1829
- return () => {
1830
- videoElement.removeEventListener('play', handlePlayPause);
1831
- videoElement.removeEventListener('pause', handlePlayPause);
1832
- track.removeEventListener('unmute', handlePlayPause);
1833
- // reset the 'pause' state once we unmount the video element
1834
- setIsVideoPaused(true);
1835
- };
1836
- }, [stream, videoElement]);
1837
- if (!call)
1838
- return null;
1839
- const isPublishingTrack = trackType === 'videoTrack'
1840
- ? videoClient.hasVideo(participant)
1841
- : trackType === 'screenShareTrack'
1842
- ? videoClient.hasScreenShare(participant)
1843
- : false;
1844
- const isInvisible = trackType === 'none' ||
1845
- viewportVisibilityState?.[trackType] === videoClient.VisibilityState.INVISIBLE;
1846
- const hasNoVideoOrInvisible = !isPublishingTrack || isInvisible;
1847
- const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
1848
- const isScreenShareTrack = trackType === 'screenShareTrack';
1849
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!hasNoVideoOrInvisible && (jsxRuntime.jsx("video", { ...rest, className: clsx('str-video__video', className, {
1850
- 'str-video__video--not-playing': isVideoPaused,
1851
- 'str-video__video--tall': !isWideMode,
1852
- 'str-video__video--mirror': mirrorVideo,
1853
- 'str-video__video--screen-share': isScreenShareTrack,
1854
- }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
1855
- setVideoElement(element);
1856
- refs?.setVideoElement?.(element);
1857
- } })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsxRuntime.jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
1858
- };
1859
- Video$1.displayName = 'Video';
1860
-
1861
- /**
1862
- * @description Extends video element with `stream` property
1863
- * (`srcObject`) to reactively handle stream changes
1864
- */
1865
- const BaseVideo = react.forwardRef(function BaseVideo({ stream, ...rest }, ref) {
1866
- const [videoElement, setVideoElement] = react.useState(null);
1867
- react.useEffect(() => {
1868
- if (!videoElement || !stream)
1869
- return;
1870
- if (stream === videoElement.srcObject)
1871
- return;
1872
- videoElement.srcObject = stream;
1873
- if (videoClient.Browsers.isSafari() || videoClient.Browsers.isFirefox()) {
1874
- // Firefox and Safari have some timing issue
1875
- setTimeout(() => {
1876
- videoElement.srcObject = stream;
1877
- videoElement.play().catch((e) => {
1878
- console.error(`Failed to play stream`, e);
1879
- });
1880
- }, 0);
1881
- }
1882
- return () => {
1883
- videoElement.pause();
1884
- videoElement.srcObject = null;
1885
- };
1886
- }, [stream, videoElement]);
1887
- return (jsxRuntime.jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
1888
- applyElementToRef(ref, element);
1889
- setVideoElement(element);
1890
- } }));
1891
- });
1892
-
1893
2013
  const DefaultDisabledVideoPreview = () => {
1894
2014
  const { t } = videoReactBindings.useI18n();
1895
2015
  return (jsxRuntime.jsx("div", { className: "str_video__video-preview__disabled-video-preview", children: t('Video is disabled') }));
@@ -1918,123 +2038,6 @@ const VideoPreview = ({ className, mirror = true, DisabledVideoPreview = Default
1918
2038
  return (jsxRuntime.jsx("div", { className: clsx('str-video__video-preview-container', className), children: contents }));
1919
2039
  };
1920
2040
 
1921
- const ParticipantActionsContextMenu = () => {
1922
- const { participant, participantViewElement, videoElement } = useParticipantViewContext();
1923
- const [fullscreenModeOn, setFullscreenModeOn] = react.useState(!!document.fullscreenElement);
1924
- const [pictureInPictureElement, setPictureInPictureElement] = react.useState(document.pictureInPictureElement);
1925
- const call = videoReactBindings.useCall();
1926
- const { t } = videoReactBindings.useI18n();
1927
- const { pin, sessionId, userId } = participant;
1928
- const hasAudioTrack = videoClient.hasAudio(participant);
1929
- const hasVideoTrack = videoClient.hasVideo(participant);
1930
- const hasScreenShareTrack = videoClient.hasScreenShare(participant);
1931
- const hasScreenShareAudioTrack = videoClient.hasScreenShareAudio(participant);
1932
- const blockUser = () => call?.blockUser(userId);
1933
- const muteAudio = () => call?.muteUser(userId, 'audio');
1934
- const muteVideo = () => call?.muteUser(userId, 'video');
1935
- const muteScreenShare = () => call?.muteUser(userId, 'screenshare');
1936
- const muteScreenShareAudio = () => call?.muteUser(userId, 'screenshare_audio');
1937
- const grantPermission = (permission) => () => {
1938
- call?.updateUserPermissions({
1939
- user_id: userId,
1940
- grant_permissions: [permission],
1941
- });
1942
- };
1943
- const revokePermission = (permission) => () => {
1944
- call?.updateUserPermissions({
1945
- user_id: userId,
1946
- revoke_permissions: [permission],
1947
- });
1948
- };
1949
- const toggleParticipantPin = () => {
1950
- if (pin) {
1951
- call?.unpin(sessionId);
1952
- }
1953
- else {
1954
- call?.pin(sessionId);
1955
- }
1956
- };
1957
- const pinForEveryone = () => {
1958
- call
1959
- ?.pinForEveryone({
1960
- user_id: userId,
1961
- session_id: sessionId,
1962
- })
1963
- .catch((err) => {
1964
- console.error(`Failed to pin participant ${userId}`, err);
1965
- });
1966
- };
1967
- const unpinForEveryone = () => {
1968
- call
1969
- ?.unpinForEveryone({
1970
- user_id: userId,
1971
- session_id: sessionId,
1972
- })
1973
- .catch((err) => {
1974
- console.error(`Failed to unpin participant ${userId}`, err);
1975
- });
1976
- };
1977
- const toggleFullscreenMode = () => {
1978
- if (!fullscreenModeOn) {
1979
- return participantViewElement?.requestFullscreen().catch(console.error);
1980
- }
1981
- return document.exitFullscreen().catch(console.error);
1982
- };
1983
- react.useEffect(() => {
1984
- // handles the case when fullscreen mode is toggled externally,
1985
- // e.g., by pressing ESC key or some other keyboard shortcut
1986
- const handleFullscreenChange = () => {
1987
- setFullscreenModeOn(!!document.fullscreenElement);
1988
- };
1989
- document.addEventListener('fullscreenchange', handleFullscreenChange);
1990
- return () => {
1991
- document.removeEventListener('fullscreenchange', handleFullscreenChange);
1992
- };
1993
- }, []);
1994
- react.useEffect(() => {
1995
- if (!videoElement)
1996
- return;
1997
- const handlePiP = () => {
1998
- setPictureInPictureElement(document.pictureInPictureElement);
1999
- };
2000
- videoElement.addEventListener('enterpictureinpicture', handlePiP);
2001
- videoElement.addEventListener('leavepictureinpicture', handlePiP);
2002
- return () => {
2003
- videoElement.removeEventListener('enterpictureinpicture', handlePiP);
2004
- videoElement.removeEventListener('leavepictureinpicture', handlePiP);
2005
- };
2006
- }, [videoElement]);
2007
- const togglePictureInPicture = () => {
2008
- if (videoElement && pictureInPictureElement !== videoElement) {
2009
- return videoElement
2010
- .requestPictureInPicture()
2011
- .catch(console.error);
2012
- }
2013
- return document.exitPictureInPicture().catch(console.error);
2014
- };
2015
- const { close } = useMenuContext() || {};
2016
- return (jsxRuntime.jsxs(GenericMenu, { onItemClick: close, children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.PIN_FOR_EVERYONE], children: [jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsxRuntime.jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsxRuntime.jsx(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.BLOCK_USERS], children: jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsxRuntime.jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.MUTE_USERS], children: [hasVideoTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsxRuntime.jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShareTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsxRuntime.jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudioTrack && (jsxRuntime.jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsxRuntime.jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
2017
- direction: fullscreenModeOn ? t('Leave') : t('Enter'),
2018
- }) })), videoElement && document.pictureInPictureEnabled && (jsxRuntime.jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
2019
- direction: pictureInPictureElement === videoElement
2020
- ? t('Leave')
2021
- : t('Enter'),
2022
- }) })), jsxRuntime.jsxs(videoReactBindings.Restricted, { requiredGrants: [videoClient.OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: grantPermission(videoClient.OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsxRuntime.jsx(GenericMenuButtonItem, { onClick: revokePermission(videoClient.OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
2023
- };
2024
-
2025
- const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
2026
- const call = videoReactBindings.useCall();
2027
- const manager = propsDynascaleManager ?? call?.dynascaleManager;
2028
- react.useEffect(() => {
2029
- if (!trackedElement || !manager || !call || trackType === 'none')
2030
- return;
2031
- const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
2032
- return () => {
2033
- unobserve();
2034
- };
2035
- }, [trackedElement, manager, call, sessionId, trackType]);
2036
- };
2037
-
2038
2041
  const ToggleButton = react.forwardRef(function ToggleButton(props, ref) {
2039
2042
  return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
2040
2043
  });
@@ -2077,7 +2080,7 @@ const SpeechIndicator = () => {
2077
2080
  return (jsxRuntime.jsxs("span", { className: clsx('str-video__speech-indicator', isSpeaking && 'str-video__speech-indicator--speaking', isDominantSpeaker && 'str-video__speech-indicator--dominant'), children: [jsxRuntime.jsx("span", { className: "str-video__speech-indicator__bar" }), jsxRuntime.jsx("span", { className: "str-video__speech-indicator__bar" }), jsxRuntime.jsx("span", { className: "str-video__speech-indicator__bar" })] }));
2078
2081
  };
2079
2082
 
2080
- const ParticipantView = react.forwardRef(function ParticipantView({ participant, trackType = 'videoTrack', muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) {
2083
+ const ParticipantView = react.forwardRef(function ParticipantView({ participant, trackType = 'videoTrack', mirror, muteAudio, refs: { setVideoElement, setVideoPlaceholderElement } = {}, className, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, }, ref) {
2081
2084
  const { isLocalParticipant, isSpeaking, isDominantSpeaker, sessionId } = participant;
2082
2085
  const hasAudioTrack = videoClient.hasAudio(participant);
2083
2086
  const hasVideoTrack = videoClient.hasVideo(participant);
@@ -2091,6 +2094,8 @@ const ParticipantView = react.forwardRef(function ParticipantView({ participant,
2091
2094
  trackedElement,
2092
2095
  trackType,
2093
2096
  });
2097
+ const { useIncomingVideoSettings } = videoReactBindings.useCallStateHooks();
2098
+ const { isParticipantVideoEnabled } = useIncomingVideoSettings();
2094
2099
  const participantViewContextValue = react.useMemo(() => ({
2095
2100
  participant,
2096
2101
  participantViewElement: trackedElement,
@@ -2117,7 +2122,9 @@ const ParticipantView = react.forwardRef(function ParticipantView({ participant,
2117
2122
  return (jsxRuntime.jsx("div", { "data-testid": "participant-view", ref: (element) => {
2118
2123
  applyElementToRef(ref, element);
2119
2124
  setTrackedElement(element);
2120
- }, className: clsx('str-video__participant-view', isDominantSpeaker && 'str-video__participant-view--dominant-speaker', isSpeaking && 'str-video__participant-view--speaking', !hasVideoTrack && 'str-video__participant-view--no-video', !hasAudioTrack && 'str-video__participant-view--no-audio', className), children: jsxRuntime.jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsxRuntime.jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2125
+ }, className: clsx('str-video__participant-view', isDominantSpeaker && 'str-video__participant-view--dominant-speaker', isSpeaking && 'str-video__participant-view--speaking', !hasVideoTrack && 'str-video__participant-view--no-video', !hasAudioTrack && 'str-video__participant-view--no-audio', className), children: jsxRuntime.jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudioTrack && (jsxRuntime.jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsxRuntime.jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, enabled: isLocalParticipant ||
2126
+ trackType !== 'videoTrack' ||
2127
+ isParticipantVideoEnabled(participant.sessionId), mirror: mirror, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2121
2128
  });
2122
2129
  ParticipantView.displayName = 'ParticipantView';
2123
2130
 
@@ -2306,7 +2313,7 @@ const LivestreamLayout = (props) => {
2306
2313
  showParticipantCount: floatingParticipantProps?.showParticipantCount ?? false, showDuration: floatingParticipantProps?.showDuration ?? false, showLiveBadge: floatingParticipantProps?.showLiveBadge ?? false, showSpeakerName: floatingParticipantProps?.showSpeakerName ?? true }));
2307
2314
  return (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__wrapper", children: [jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), hasOngoingScreenShare && presenter && (jsxRuntime.jsx(ParticipantView, { className: "str-video__livestream-layout__screen-share", participant: presenter, ParticipantViewUI: Overlay, trackType: "screenShareTrack", muteAudio // audio is rendered by ParticipantsAudio
2308
2315
  : true })), currentSpeaker && (jsxRuntime.jsx(ParticipantView, { className: clsx(hasOngoingScreenShare &&
2309
- clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: FloatingParticipantOverlay || Overlay, muteAudio // audio is rendered by ParticipantsAudio
2316
+ clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: FloatingParticipantOverlay || Overlay, mirror: props.mirrorLocalParticipantVideo !== false ? undefined : false, muteAudio // audio is rendered by ParticipantsAudio
2310
2317
  : true }))] }));
2311
2318
  };
2312
2319
  const ParticipantOverlay = (props) => {
@@ -2368,17 +2375,17 @@ const formatDuration = (durationInMs) => {
2368
2375
  };
2369
2376
 
2370
2377
  const GROUP_SIZE = 16;
2371
- const PaginatedGridLayoutGroup = ({ group, VideoPlaceholder, ParticipantViewUI, }) => {
2378
+ const PaginatedGridLayoutGroup = ({ group, mirror, VideoPlaceholder, ParticipantViewUI, }) => {
2372
2379
  return (jsxRuntime.jsx("div", { className: clsx('str-video__paginated-grid-layout__group', {
2373
2380
  'str-video__paginated-grid-layout--one': group.length === 1,
2374
2381
  'str-video__paginated-grid-layout--two-four': group.length >= 2 && group.length <= 4,
2375
2382
  'str-video__paginated-grid-layout--five-nine': group.length >= 5 && group.length <= 9,
2376
- }), children: group.map((participant) => (jsxRuntime.jsx(ParticipantView, { participant: participant, muteAudio: true, VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI }, participant.sessionId))) }));
2383
+ }), children: group.map((participant) => (jsxRuntime.jsx(ParticipantView, { participant: participant, muteAudio: true, mirror: mirror, VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI }, participant.sessionId))) }));
2377
2384
  };
2378
2385
  const PaginatedGridLayout = (props) => {
2379
2386
  const { groupSize = (props.groupSize || 0) > 0
2380
2387
  ? props.groupSize || GROUP_SIZE
2381
- : GROUP_SIZE, excludeLocalParticipant = false, pageArrowsVisible = true, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, } = props;
2388
+ : GROUP_SIZE, excludeLocalParticipant = false, mirrorLocalParticipantVideo = true, pageArrowsVisible = true, VideoPlaceholder, ParticipantViewUI = DefaultParticipantViewUI, } = props;
2382
2389
  const [page, setPage] = react.useState(0);
2383
2390
  const [paginatedGridLayoutWrapperElement, setPaginatedGridLayoutWrapperElement,] = react.useState(null);
2384
2391
  const call = videoReactBindings.useCall();
@@ -2402,9 +2409,10 @@ const PaginatedGridLayout = (props) => {
2402
2409
  }
2403
2410
  }, [page, pageCount]);
2404
2411
  const selectedGroup = participantGroups[page];
2412
+ const mirror = mirrorLocalParticipantVideo ? undefined : false;
2405
2413
  if (!call)
2406
2414
  return null;
2407
- return (jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout__wrapper", ref: setPaginatedGridLayoutWrapperElement, children: [jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout", children: [pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { icon: "caret-left", disabled: page === 0, onClick: () => setPage((currentPage) => Math.max(0, currentPage - 1)) })), selectedGroup && (jsxRuntime.jsx(PaginatedGridLayoutGroup, { group: selectedGroup, VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI })), pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { disabled: page === pageCount - 1, icon: "caret-right", onClick: () => setPage((currentPage) => Math.min(pageCount - 1, currentPage + 1)) }))] })] }));
2415
+ return (jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout__wrapper", ref: setPaginatedGridLayoutWrapperElement, children: [jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), jsxRuntime.jsxs("div", { className: "str-video__paginated-grid-layout", children: [pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { icon: "caret-left", disabled: page === 0, onClick: () => setPage((currentPage) => Math.max(0, currentPage - 1)) })), selectedGroup && (jsxRuntime.jsx(PaginatedGridLayoutGroup, { group: selectedGroup, mirror: mirror, VideoPlaceholder: VideoPlaceholder, ParticipantViewUI: ParticipantViewUI })), pageArrowsVisible && pageCount > 1 && (jsxRuntime.jsx(IconButton, { disabled: page === pageCount - 1, icon: "caret-right", onClick: () => setPage((currentPage) => Math.min(pageCount - 1, currentPage + 1)) }))] })] }));
2408
2416
  };
2409
2417
  PaginatedGridLayout.displayName = 'PaginatedGridLayout';
2410
2418
 
@@ -2464,7 +2472,7 @@ hostElement, limit) => {
2464
2472
  };
2465
2473
 
2466
2474
  const DefaultParticipantViewUIBar = () => (jsxRuntime.jsx(DefaultParticipantViewUI, { menuPlacement: "top-end" }));
2467
- const SpeakerLayout = ({ ParticipantViewUIBar = DefaultParticipantViewUIBar, ParticipantViewUISpotlight = DefaultParticipantViewUI, VideoPlaceholder, participantsBarPosition = 'bottom', participantsBarLimit, excludeLocalParticipant = false, pageArrowsVisible = true, }) => {
2475
+ const SpeakerLayout = ({ ParticipantViewUIBar = DefaultParticipantViewUIBar, ParticipantViewUISpotlight = DefaultParticipantViewUI, VideoPlaceholder, participantsBarPosition = 'bottom', participantsBarLimit, mirrorLocalParticipantVideo = true, excludeLocalParticipant = false, pageArrowsVisible = true, }) => {
2468
2476
  const call = videoReactBindings.useCall();
2469
2477
  const { useParticipants, useRemoteParticipants } = videoReactBindings.useCallStateHooks();
2470
2478
  const allParticipants = useParticipants();
@@ -2498,10 +2506,11 @@ const SpeakerLayout = ({ ParticipantViewUIBar = DefaultParticipantViewUIBar, Par
2498
2506
  // that one is rendered independently from otherParticipants array
2499
2507
  hardLimitToApply - (isSpeakerScreenSharing ? 1 : 0));
2500
2508
  }
2509
+ const mirror = mirrorLocalParticipantVideo ? undefined : false;
2501
2510
  if (!call)
2502
2511
  return null;
2503
2512
  return (jsxRuntime.jsxs("div", { className: "str-video__speaker-layout__wrapper", children: [jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), jsxRuntime.jsxs("div", { className: clsx('str-video__speaker-layout', participantsBarPosition &&
2504
- `str-video__speaker-layout--variant-${participantsBarPosition}`), children: [jsxRuntime.jsx("div", { className: "str-video__speaker-layout__spotlight", children: participantInSpotlight && (jsxRuntime.jsx(ParticipantView, { participant: participantInSpotlight, muteAudio: true, trackType: isSpeakerScreenSharing ? 'screenShareTrack' : 'videoTrack', ParticipantViewUI: ParticipantViewUISpotlight, VideoPlaceholder: VideoPlaceholder })) }), participantsWithAppliedLimit.length > 0 && participantsBarPosition && (jsxRuntime.jsxs("div", { ref: setButtonsWrapperElement, className: "str-video__speaker-layout__participants-bar-buttons-wrapper", children: [jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participants-bar-wrapper", ref: setParticipantsBarWrapperElement, children: jsxRuntime.jsxs("div", { ref: setParticipantsBarElement, className: "str-video__speaker-layout__participants-bar", children: [isSpeakerScreenSharing && (jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participant-tile", children: jsxRuntime.jsx(ParticipantView, { participant: participantInSpotlight, ParticipantViewUI: ParticipantViewUIBar, VideoPlaceholder: VideoPlaceholder, muteAudio: true }) }, participantInSpotlight.sessionId)), participantsWithAppliedLimit.map((participant) => (jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participant-tile", children: jsxRuntime.jsx(ParticipantView, { participant: participant, ParticipantViewUI: ParticipantViewUIBar, VideoPlaceholder: VideoPlaceholder, muteAudio: true }) }, participant.sessionId)))] }) }), pageArrowsVisible && isVertical && (jsxRuntime.jsx(VerticalScrollButtons, { scrollWrapper: participantsBarWrapperElement })), pageArrowsVisible && isHorizontal && (jsxRuntime.jsx(HorizontalScrollButtons, { scrollWrapper: participantsBarWrapperElement }))] }))] })] }));
2513
+ `str-video__speaker-layout--variant-${participantsBarPosition}`), children: [jsxRuntime.jsx("div", { className: "str-video__speaker-layout__spotlight", children: participantInSpotlight && (jsxRuntime.jsx(ParticipantView, { participant: participantInSpotlight, muteAudio: true, mirror: mirror, trackType: isSpeakerScreenSharing ? 'screenShareTrack' : 'videoTrack', ParticipantViewUI: ParticipantViewUISpotlight, VideoPlaceholder: VideoPlaceholder })) }), participantsWithAppliedLimit.length > 0 && participantsBarPosition && (jsxRuntime.jsxs("div", { ref: setButtonsWrapperElement, className: "str-video__speaker-layout__participants-bar-buttons-wrapper", children: [jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participants-bar-wrapper", ref: setParticipantsBarWrapperElement, children: jsxRuntime.jsxs("div", { ref: setParticipantsBarElement, className: "str-video__speaker-layout__participants-bar", children: [isSpeakerScreenSharing && (jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participant-tile", children: jsxRuntime.jsx(ParticipantView, { participant: participantInSpotlight, ParticipantViewUI: ParticipantViewUIBar, VideoPlaceholder: VideoPlaceholder, mirror: mirror, muteAudio: true }) }, participantInSpotlight.sessionId)), participantsWithAppliedLimit.map((participant) => (jsxRuntime.jsx("div", { className: "str-video__speaker-layout__participant-tile", children: jsxRuntime.jsx(ParticipantView, { participant: participant, ParticipantViewUI: ParticipantViewUIBar, VideoPlaceholder: VideoPlaceholder, mirror: mirror, muteAudio: true }) }, participant.sessionId)))] }) }), pageArrowsVisible && isVertical && (jsxRuntime.jsx(VerticalScrollButtons, { scrollWrapper: participantsBarWrapperElement })), pageArrowsVisible && isHorizontal && (jsxRuntime.jsx(HorizontalScrollButtons, { scrollWrapper: participantsBarWrapperElement }))] }))] })] }));
2505
2514
  };
2506
2515
  SpeakerLayout.displayName = 'SpeakerLayout';
2507
2516
  const HorizontalScrollButtons = ({ scrollWrapper, }) => {
@@ -2547,7 +2556,7 @@ const LivestreamPlayer = (props) => {
2547
2556
  return (jsxRuntime.jsx(StreamCall, { call: call, children: jsxRuntime.jsx(LivestreamLayout, { ...layoutProps }) }));
2548
2557
  };
2549
2558
 
2550
- const [major, minor, patch] = ("1.4.5").split('.');
2559
+ const [major, minor, patch] = ("1.6.0").split('.');
2551
2560
  videoClient.setSdkInfo({
2552
2561
  type: videoClient.SfuModels.SdkType.REACT,
2553
2562
  major,