@stream-io/video-react-sdk 1.4.4 → 1.5.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.es.js CHANGED
@@ -1,13 +1,13 @@
1
- import { hasAudio, hasScreenShareAudio, getLogger, disposeOfMediaStream, CallingState, OwnCapability, hasVideo, isPinned, name, NoiseCancellationSettingsModeEnum, hasScreenShare, VisibilityState, Browsers, SfuModels, paginatedLayoutSortPreset, combineComparators, screenSharing, speakerLayoutSortPreset, CallTypes, defaultSortPreset, setSdkInfo } from '@stream-io/video-client';
1
+ import { hasAudio, hasScreenShareAudio, CallingState, hasVideo, hasScreenShare, OwnCapability, Browsers, VisibilityState, getLogger, disposeOfMediaStream, isPinned, name, NoiseCancellationSettingsModeEnum, SfuModels, paginatedLayoutSortPreset, combineComparators, screenSharing, speakerLayoutSortPreset, CallTypes, defaultSortPreset, setSdkInfo } from '@stream-io/video-client';
2
2
  export * from '@stream-io/video-client';
3
3
  import { useCall, useCallStateHooks, useI18n, Restricted, useConnectedUser, StreamCallProvider, StreamVideoProvider, useStreamVideoClient } from '@stream-io/video-react-bindings';
4
4
  export * from '@stream-io/video-react-bindings';
5
5
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
6
- import { useState, useEffect, Fragment as Fragment$1, createContext, useContext, useCallback, useRef, useMemo, forwardRef, isValidElement, lazy, Suspense, useLayoutEffect } from 'react';
6
+ import { useState, useEffect, Fragment as Fragment$1, createContext, useContext, useCallback, useRef, useMemo, isValidElement, forwardRef, useLayoutEffect, lazy, Suspense } from 'react';
7
+ import { useFloating, offset, shift, flip, size, autoUpdate, FloatingOverlay, FloatingPortal, arrow, FloatingArrow, useListItem, useListNavigation, useTypeahead, useClick, useDismiss, useRole, useInteractions, FloatingFocusManager, FloatingList, useHover } from '@floating-ui/react';
7
8
  import clsx from 'clsx';
8
9
  import { flushSync } from 'react-dom';
9
10
  import { isPlatformSupported, loadTFLite, createRenderer } from '@stream-io/video-filters-web';
10
- import { useFloating, offset, shift, flip, size, autoUpdate, FloatingOverlay, FloatingPortal, arrow, FloatingArrow, useListItem, useListNavigation, useTypeahead, useClick, useDismiss, useRole, useInteractions, FloatingFocusManager, FloatingList, useHover } from '@floating-ui/react';
11
11
 
12
12
  const Audio = ({ participant, trackType = 'audioTrack', ...rest }) => {
13
13
  const call = useCall();
@@ -44,167 +44,6 @@ ParticipantsAudio.displayName = 'ParticipantsAudio';
44
44
  const ParticipantViewContext = createContext(undefined);
45
45
  const useParticipantViewContext = () => useContext(ParticipantViewContext);
46
46
 
47
- const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
48
- const [error, setError] = useState(false);
49
- return (jsxs(Fragment, { children: [(!imageSrc || error) && name && (jsx(AvatarFallback, { className: className, style: style, names: [name] })), imageSrc && !error && (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 (jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
53
- };
54
-
55
- /**
56
- * The context for the background filters.
57
- */
58
- const BackgroundFiltersContext = createContext(undefined);
59
- /**
60
- * A hook to access the background filters context API.
61
- */
62
- const useBackgroundFilters = () => {
63
- const context = 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] = useState(bgFilterFromProps);
78
- const [backgroundImage, setBackgroundImage] = useState(bgImageFromProps);
79
- const [backgroundBlurLevel, setBackgroundBlurLevel] = useState(bgBlurLevelFromProps);
80
- const applyBackgroundImageFilter = useCallback((imageUrl) => {
81
- setBackgroundFilter('image');
82
- setBackgroundImage(imageUrl);
83
- }, []);
84
- const applyBackgroundBlurFilter = useCallback((blurLevel = 'high') => {
85
- setBackgroundFilter('blur');
86
- setBackgroundBlurLevel(blurLevel);
87
- }, []);
88
- const disableBackgroundFilter = useCallback(() => {
89
- setBackgroundFilter(undefined);
90
- setBackgroundImage(undefined);
91
- setBackgroundBlurLevel('high');
92
- }, []);
93
- const [isSupported, setIsSupported] = useState(false);
94
- useEffect(() => {
95
- isPlatformSupported().then(setIsSupported);
96
- }, []);
97
- const [tfLite, setTfLite] = useState();
98
- useEffect(() => {
99
- // don't try to load TFLite if the platform is not supported
100
- if (!isSupported)
101
- return;
102
- 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 = useCallback((error) => {
107
- getLogger(['filters'])('warn', 'Filter encountered an error and will be disabled');
108
- disableBackgroundFilter();
109
- onError?.(error);
110
- }, [disableBackgroundFilter, onError]);
111
- return (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 && jsx(BackgroundFilters, { tfLite: tfLite })] }));
126
- };
127
- const BackgroundFilters = (props) => {
128
- const call = useCall();
129
- const { children, start } = useRenderer(props.tfLite);
130
- const { backgroundFilter, onError } = useBackgroundFilters();
131
- const handleErrorRef = useRef(undefined);
132
- handleErrorRef.current = onError;
133
- 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 = useRef(null);
146
- const canvasRef = useRef(null);
147
- const bgImageRef = useRef(null);
148
- const [videoSize, setVideoSize] = useState({
149
- width: 1920,
150
- height: 1080,
151
- });
152
- const start = 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
- flushSync(() => setVideoSize({
177
- width: trackSettings.width ?? 0,
178
- height: trackSettings.height ?? 0,
179
- }));
180
- renderer = 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 && disposeOfMediaStream(outputStream);
197
- },
198
- };
199
- }, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
200
- const children = (jsxs("div", { className: "str-video__background-filters", children: [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 && (jsx("img", { className: "str-video__background-filters__background-image", alt: "Background", ref: bgImageRef, src: backgroundImage, ...videoSize })), 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, } = useFloating({
210
49
  placement,
@@ -552,8 +391,424 @@ const GenericMenuButtonItem = ({ children, ...rest }) => {
552
391
  return (jsx("li", { className: "str-video__generic-menu--item", children: jsx("button", { ...rest, children: children }) }));
553
392
  };
554
393
 
555
- const Icon = ({ className, icon }) => (jsx("span", { className: clsx('str-video__icon', icon && `str-video__icon--${icon}`, className) }));
556
-
394
+ const Icon = ({ className, icon }) => (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] = useState(!!document.fullscreenElement);
399
+ const [pictureInPictureElement, setPictureInPictureElement] = useState(document.pictureInPictureElement);
400
+ const call = useCall();
401
+ const { t } = useI18n();
402
+ const { pin, sessionId, userId } = participant;
403
+ const hasAudioTrack = hasAudio(participant);
404
+ const hasVideoTrack = hasVideo(participant);
405
+ const hasScreenShareTrack = hasScreenShare(participant);
406
+ const hasScreenShareAudioTrack = 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
+ 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
+ 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 (jsxs(GenericMenu, { onItemClick: close, children: [jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxs(Restricted, { requiredGrants: [OwnCapability.PIN_FOR_EVERYONE], children: [jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsx(Restricted, { requiredGrants: [OwnCapability.BLOCK_USERS], children: jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxs(Restricted, { requiredGrants: [OwnCapability.MUTE_USERS], children: [hasVideoTrack && (jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShareTrack && (jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudioTrack && (jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudioTrack && (jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
492
+ direction: fullscreenModeOn ? t('Leave') : t('Enter'),
493
+ }) })), videoElement && document.pictureInPictureEnabled && (jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
494
+ direction: pictureInPictureElement === videoElement
495
+ ? t('Leave')
496
+ : t('Enter'),
497
+ }) })), jsxs(Restricted, { requiredGrants: [OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
498
+ };
499
+
500
+ const isComponentType = (elementOrComponent) => {
501
+ return elementOrComponent === null
502
+ ? false
503
+ : !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 = forwardRef(function BaseVideo({ stream, ...rest }, ref) {
524
+ const [videoElement, setVideoElement] = useState(null);
525
+ useEffect(() => {
526
+ if (!videoElement || !stream)
527
+ return;
528
+ if (stream === videoElement.srcObject)
529
+ return;
530
+ videoElement.srcObject = stream;
531
+ if (Browsers.isSafari() || 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 (jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
546
+ applyElementToRef(ref, element);
547
+ setVideoElement(element);
548
+ } }));
549
+ });
550
+
551
+ const DefaultVideoPlaceholder = forwardRef(function DefaultVideoPlaceholder({ participant, style }, ref) {
552
+ const { t } = useI18n();
553
+ const [error, setError] = useState(false);
554
+ const name = participant.name || participant.userId;
555
+ return (jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
556
+ (name ? (jsx(InitialsFallback, { name: name })) : (jsx("div", { className: "str-video__video-placeholder__no-video-label", children: t('Video is disabled') }))), participant.image && !error && (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 (jsx("div", { className: "str-video__video-placeholder__initials-fallback", children: initials }));
566
+ };
567
+
568
+ const Video$1 = ({ enabled, trackType, participant, className, VideoPlaceholder = DefaultVideoPlaceholder, refs, ...rest }) => {
569
+ const { sessionId, videoStream, screenShareStream, viewportVisibilityState, isLocalParticipant, userId, } = participant;
570
+ const call = useCall();
571
+ const [videoElement, setVideoElement] = useState(null);
572
+ // start with true, will flip once the video starts playing
573
+ const [isVideoPaused, setIsVideoPaused] = useState(true);
574
+ const [isWideMode, setIsWideMode] = useState(true);
575
+ const stream = trackType === 'videoTrack'
576
+ ? videoStream
577
+ : trackType === 'screenShareTrack'
578
+ ? screenShareStream
579
+ : undefined;
580
+ 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
+ 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
+ ? hasVideo(participant)
618
+ : trackType === 'screenShareTrack'
619
+ ? hasScreenShare(participant)
620
+ : false;
621
+ const isInvisible = trackType === 'none' ||
622
+ viewportVisibilityState?.[trackType] === VisibilityState.INVISIBLE;
623
+ const hasNoVideoOrInvisible = !enabled || !isPublishingTrack || isInvisible;
624
+ const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
625
+ const isScreenShareTrack = trackType === 'screenShareTrack';
626
+ return (jsxs(Fragment, { children: [!hasNoVideoOrInvisible && (jsx("video", { ...rest, className: clsx('str-video__video', className, {
627
+ 'str-video__video--not-playing': isVideoPaused,
628
+ 'str-video__video--tall': !isWideMode,
629
+ 'str-video__video--mirror': mirrorVideo,
630
+ 'str-video__video--screen-share': isScreenShareTrack,
631
+ }), "data-user-id": userId, "data-session-id": sessionId, ref: (element) => {
632
+ setVideoElement(element);
633
+ refs?.setVideoElement?.(element);
634
+ } })), (hasNoVideoOrInvisible || isVideoPaused) && VideoPlaceholder && (jsx(VideoPlaceholder, { style: { position: 'absolute' }, participant: participant, ref: refs?.setVideoPlaceholderElement }))] }));
635
+ };
636
+ Video$1.displayName = 'Video';
637
+
638
+ const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
639
+ const call = useCall();
640
+ const manager = propsDynascaleManager ?? call?.dynascaleManager;
641
+ useEffect(() => {
642
+ if (!trackedElement || !manager || !call || trackType === 'none')
643
+ return;
644
+ const unobserve = manager.trackElementVisibility(trackedElement, sessionId, trackType);
645
+ return () => {
646
+ unobserve();
647
+ };
648
+ }, [trackedElement, manager, call, sessionId, trackType]);
649
+ };
650
+
651
+ const Avatar = ({ imageSrc, name, style, className, ...rest }) => {
652
+ const [error, setError] = useState(false);
653
+ return (jsxs(Fragment, { children: [(!imageSrc || error) && name && (jsx(AvatarFallback, { className: className, style: style, names: [name] })), imageSrc && !error && (jsx("img", { onError: () => setError(true), alt: "avatar", className: clsx('str-video__avatar', className), src: imageSrc, style: style, ...rest }))] }));
654
+ };
655
+ const AvatarFallback = ({ className, names, style, }) => {
656
+ return (jsx("div", { className: clsx('str-video__avatar--initials-fallback', className), style: style, children: jsxs("div", { children: [names[0][0], names[1]?.[0]] }) }));
657
+ };
658
+
659
+ /**
660
+ * The context for the background filters.
661
+ */
662
+ const BackgroundFiltersContext = createContext(undefined);
663
+ /**
664
+ * A hook to access the background filters context API.
665
+ */
666
+ const useBackgroundFilters = () => {
667
+ const context = useContext(BackgroundFiltersContext);
668
+ if (!context) {
669
+ throw new Error('useBackgroundFilters must be used within a BackgroundFiltersProvider');
670
+ }
671
+ return context;
672
+ };
673
+ /**
674
+ * A provider component that enables the use of background filters in your app.
675
+ *
676
+ * Please make sure you have the `@stream-io/video-filters-web` package installed
677
+ * in your project before using this component.
678
+ */
679
+ const BackgroundFiltersProvider = (props) => {
680
+ const { children, backgroundImages = [], backgroundFilter: bgFilterFromProps = undefined, backgroundImage: bgImageFromProps = undefined, backgroundBlurLevel: bgBlurLevelFromProps = 'high', tfFilePath, modelFilePath, basePath, onError, } = props;
681
+ const [backgroundFilter, setBackgroundFilter] = useState(bgFilterFromProps);
682
+ const [backgroundImage, setBackgroundImage] = useState(bgImageFromProps);
683
+ const [backgroundBlurLevel, setBackgroundBlurLevel] = useState(bgBlurLevelFromProps);
684
+ const applyBackgroundImageFilter = useCallback((imageUrl) => {
685
+ setBackgroundFilter('image');
686
+ setBackgroundImage(imageUrl);
687
+ }, []);
688
+ const applyBackgroundBlurFilter = useCallback((blurLevel = 'high') => {
689
+ setBackgroundFilter('blur');
690
+ setBackgroundBlurLevel(blurLevel);
691
+ }, []);
692
+ const disableBackgroundFilter = useCallback(() => {
693
+ setBackgroundFilter(undefined);
694
+ setBackgroundImage(undefined);
695
+ setBackgroundBlurLevel('high');
696
+ }, []);
697
+ const [isSupported, setIsSupported] = useState(false);
698
+ useEffect(() => {
699
+ isPlatformSupported().then(setIsSupported);
700
+ }, []);
701
+ const [tfLite, setTfLite] = useState();
702
+ useEffect(() => {
703
+ // don't try to load TFLite if the platform is not supported
704
+ if (!isSupported)
705
+ return;
706
+ loadTFLite({ basePath, modelFilePath, tfFilePath })
707
+ .then(setTfLite)
708
+ .catch((err) => console.error('Failed to load TFLite', err));
709
+ }, [basePath, isSupported, modelFilePath, tfFilePath]);
710
+ const handleError = useCallback((error) => {
711
+ getLogger(['filters'])('warn', 'Filter encountered an error and will be disabled');
712
+ disableBackgroundFilter();
713
+ onError?.(error);
714
+ }, [disableBackgroundFilter, onError]);
715
+ return (jsxs(BackgroundFiltersContext.Provider, { value: {
716
+ isSupported,
717
+ isReady: !!tfLite,
718
+ backgroundImage,
719
+ backgroundBlurLevel,
720
+ backgroundFilter,
721
+ disableBackgroundFilter,
722
+ applyBackgroundBlurFilter,
723
+ applyBackgroundImageFilter,
724
+ backgroundImages,
725
+ tfFilePath,
726
+ modelFilePath,
727
+ basePath,
728
+ onError: handleError,
729
+ }, children: [children, tfLite && jsx(BackgroundFilters, { tfLite: tfLite })] }));
730
+ };
731
+ const BackgroundFilters = (props) => {
732
+ const call = useCall();
733
+ const { children, start } = useRenderer(props.tfLite);
734
+ const { backgroundFilter, onError } = useBackgroundFilters();
735
+ const handleErrorRef = useRef(undefined);
736
+ handleErrorRef.current = onError;
737
+ useEffect(() => {
738
+ if (!call || !backgroundFilter)
739
+ return;
740
+ const { unregister } = call.camera.registerFilter((ms) => start(ms, (error) => handleErrorRef.current?.(error)));
741
+ return () => {
742
+ unregister();
743
+ };
744
+ }, [backgroundFilter, call, start]);
745
+ return children;
746
+ };
747
+ const useRenderer = (tfLite) => {
748
+ const { backgroundFilter, backgroundBlurLevel, backgroundImage } = useBackgroundFilters();
749
+ const videoRef = useRef(null);
750
+ const canvasRef = useRef(null);
751
+ const bgImageRef = useRef(null);
752
+ const [videoSize, setVideoSize] = useState({
753
+ width: 1920,
754
+ height: 1080,
755
+ });
756
+ const start = useCallback((ms, onError) => {
757
+ let outputStream;
758
+ let renderer;
759
+ const output = new Promise((resolve, reject) => {
760
+ if (!backgroundFilter) {
761
+ reject(new Error('No filter specified'));
762
+ return;
763
+ }
764
+ const videoEl = videoRef.current;
765
+ const canvasEl = canvasRef.current;
766
+ const bgImageEl = bgImageRef.current;
767
+ if (!videoEl || !canvasEl || (backgroundImage && !bgImageEl)) {
768
+ // You should start renderer in effect or event handlers
769
+ reject(new Error('Renderer started before elements are ready'));
770
+ return;
771
+ }
772
+ videoEl.srcObject = ms;
773
+ videoEl.play().then(() => {
774
+ const [track] = ms.getVideoTracks();
775
+ if (!track) {
776
+ reject(new Error('No video tracks in input media stream'));
777
+ return;
778
+ }
779
+ const trackSettings = track.getSettings();
780
+ flushSync(() => setVideoSize({
781
+ width: trackSettings.width ?? 0,
782
+ height: trackSettings.height ?? 0,
783
+ }));
784
+ renderer = createRenderer(tfLite, videoEl, canvasEl, {
785
+ backgroundFilter,
786
+ backgroundBlurLevel,
787
+ backgroundImage: bgImageEl ?? undefined,
788
+ }, onError);
789
+ outputStream = canvasEl.captureStream();
790
+ resolve(outputStream);
791
+ }, () => {
792
+ reject(new Error('Could not play the source video stream'));
793
+ });
794
+ });
795
+ return {
796
+ output,
797
+ stop: () => {
798
+ renderer?.dispose();
799
+ videoRef.current && (videoRef.current.srcObject = null);
800
+ outputStream && disposeOfMediaStream(outputStream);
801
+ },
802
+ };
803
+ }, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
804
+ const children = (jsxs("div", { className: "str-video__background-filters", children: [jsx("video", { className: clsx('str-video__background-filters__video', videoSize.height > videoSize.width &&
805
+ 'str-video__background-filters__video--tall'), ref: videoRef, playsInline: true, muted: true, controls: false, ...videoSize }), backgroundImage && (jsx("img", { className: "str-video__background-filters__background-image", alt: "Background", ref: bgImageRef, src: backgroundImage, ...videoSize })), jsx("canvas", { className: "str-video__background-filters__target-canvas", ...videoSize, ref: canvasRef })] }));
806
+ return {
807
+ start,
808
+ children,
809
+ };
810
+ };
811
+
557
812
  const IconButton = forwardRef(function IconButton(props, ref) {
558
813
  const { icon, enabled, variant, onClick, className, ...rest } = props;
559
814
  return (jsx("button", { className: clsx('str-video__call-controls__button', className, {
@@ -565,26 +820,7 @@ const IconButton = forwardRef(function IconButton(props, ref) {
565
820
  }, ref: ref, ...rest, children: jsx(Icon, { icon: icon }) }));
566
821
  });
567
822
 
568
- const isComponentType = (elementOrComponent) => {
569
- return elementOrComponent === null
570
- ? false
571
- : !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 = forwardRef(function CompositeButton({ caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
823
+ const CompositeButton = forwardRef(function CompositeButton({ disabled, caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
588
824
  return (jsxs("div", { className: clsx('str-video__composite-button', className, {
589
825
  'str-video__composite-button--caption': caption,
590
826
  'str-video__composite-button--menu': Menu,
@@ -592,10 +828,11 @@ const CompositeButton = forwardRef(function CompositeButton({ caption, children,
592
828
  'str-video__composite-button__button-group--active': active,
593
829
  'str-video__composite-button__button-group--active-primary': active && variant === 'primary',
594
830
  'str-video__composite-button__button-group--active-secondary': active && variant === 'secondary',
831
+ 'str-video__composite-button__button-group--disabled': disabled,
595
832
  }), children: [jsx("button", { type: "button", className: "str-video__composite-button__button", onClick: (e) => {
596
833
  e.preventDefault();
597
834
  onClick?.(e);
598
- }, ...restButtonProps, children: children }), Menu && (jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, onToggle: onMenuToggle, children: isComponentType(Menu) ? jsx(Menu, {}) : Menu }))] }), caption && (jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
835
+ }, disabled: disabled, ...restButtonProps, children: children }), Menu && (jsx(MenuToggle, { offset: menuOffset, placement: menuPlacement, ToggleButton: ToggleMenuButton, onToggle: onMenuToggle, children: isComponentType(Menu) ? jsx(Menu, {}) : Menu }))] }), caption && (jsx("div", { className: "str-video__composite-button__caption", children: caption }))] }));
599
836
  });
600
837
  const DefaultToggleMenuButton = forwardRef(function DefaultToggleMenuButton({ menuShown }, ref) {
601
838
  return (jsx(IconButton, { className: clsx('str-video__menu-toggle-button', {
@@ -966,7 +1203,7 @@ const DropDownSelectOption = (props) => {
966
1203
  'str-video__dropdown-option--selected': selected,
967
1204
  }), ref: ref, ...getItemProps({
968
1205
  onClick: () => handleSelect(index),
969
- }), children: [jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsx("span", { className: "str-video__dropdown-label", children: label })] }));
1206
+ }), children: [icon && jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsx("span", { className: "str-video__dropdown-label", children: label })] }));
970
1207
  };
971
1208
  const DropDownSelect = (props) => {
972
1209
  const { children, icon, handleSelect, defaultSelectedLabel, defaultSelectedIndex, } = props;
@@ -1771,125 +2008,6 @@ const StreamTheme = ({ as: Component = 'div', className, children, ...props }) =
1771
2008
  return (jsx(Component, { ...props, className: clsx('str-video', className), children: children }));
1772
2009
  };
1773
2010
 
1774
- const DefaultVideoPlaceholder = forwardRef(function DefaultVideoPlaceholder({ participant, style }, ref) {
1775
- const { t } = useI18n();
1776
- const [error, setError] = useState(false);
1777
- const name = participant.name || participant.userId;
1778
- return (jsxs("div", { className: "str-video__video-placeholder", style: style, ref: ref, children: [(!participant.image || error) &&
1779
- (name ? (jsx(InitialsFallback, { name: name })) : (jsx("div", { className: "str-video__video-placeholder__no-video-label", children: t('Video is disabled') }))), participant.image && !error && (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 (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 = useCall();
1794
- const [videoElement, setVideoElement] = useState(null);
1795
- // start with true, will flip once the video starts playing
1796
- const [isVideoPaused, setIsVideoPaused] = useState(true);
1797
- const [isWideMode, setIsWideMode] = useState(true);
1798
- const stream = trackType === 'videoTrack'
1799
- ? videoStream
1800
- : trackType === 'screenShareTrack'
1801
- ? screenShareStream
1802
- : undefined;
1803
- 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
- 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
- ? hasVideo(participant)
1841
- : trackType === 'screenShareTrack'
1842
- ? hasScreenShare(participant)
1843
- : false;
1844
- const isInvisible = trackType === 'none' ||
1845
- viewportVisibilityState?.[trackType] === VisibilityState.INVISIBLE;
1846
- const hasNoVideoOrInvisible = !isPublishingTrack || isInvisible;
1847
- const mirrorVideo = isLocalParticipant && trackType === 'videoTrack';
1848
- const isScreenShareTrack = trackType === 'screenShareTrack';
1849
- return (jsxs(Fragment, { children: [!hasNoVideoOrInvisible && (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 && (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 = forwardRef(function BaseVideo({ stream, ...rest }, ref) {
1866
- const [videoElement, setVideoElement] = useState(null);
1867
- useEffect(() => {
1868
- if (!videoElement || !stream)
1869
- return;
1870
- if (stream === videoElement.srcObject)
1871
- return;
1872
- videoElement.srcObject = stream;
1873
- if (Browsers.isSafari() || 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 (jsx("video", { autoPlay: true, playsInline: true, ...rest, ref: (element) => {
1888
- applyElementToRef(ref, element);
1889
- setVideoElement(element);
1890
- } }));
1891
- });
1892
-
1893
2011
  const DefaultDisabledVideoPreview = () => {
1894
2012
  const { t } = useI18n();
1895
2013
  return (jsx("div", { className: "str_video__video-preview__disabled-video-preview", children: t('Video is disabled') }));
@@ -1918,123 +2036,6 @@ const VideoPreview = ({ className, mirror = true, DisabledVideoPreview = Default
1918
2036
  return (jsx("div", { className: clsx('str-video__video-preview-container', className), children: contents }));
1919
2037
  };
1920
2038
 
1921
- const ParticipantActionsContextMenu = () => {
1922
- const { participant, participantViewElement, videoElement } = useParticipantViewContext();
1923
- const [fullscreenModeOn, setFullscreenModeOn] = useState(!!document.fullscreenElement);
1924
- const [pictureInPictureElement, setPictureInPictureElement] = useState(document.pictureInPictureElement);
1925
- const call = useCall();
1926
- const { t } = useI18n();
1927
- const { pin, sessionId, userId } = participant;
1928
- const hasAudioTrack = hasAudio(participant);
1929
- const hasVideoTrack = hasVideo(participant);
1930
- const hasScreenShareTrack = hasScreenShare(participant);
1931
- const hasScreenShareAudioTrack = 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
- 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
- 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 (jsxs(GenericMenu, { onItemClick: close, children: [jsxs(GenericMenuButtonItem, { onClick: toggleParticipantPin, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), pin ? t('Unpin') : t('Pin')] }), jsxs(Restricted, { requiredGrants: [OwnCapability.PIN_FOR_EVERYONE], children: [jsxs(GenericMenuButtonItem, { onClick: pinForEveryone, disabled: pin && !pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Pin for everyone')] }), jsxs(GenericMenuButtonItem, { onClick: unpinForEveryone, disabled: !pin || pin.isLocalPin, children: [jsx(Icon, { icon: "pin" }), t('Unpin for everyone')] })] }), jsx(Restricted, { requiredGrants: [OwnCapability.BLOCK_USERS], children: jsxs(GenericMenuButtonItem, { onClick: blockUser, children: [jsx(Icon, { icon: "not-allowed" }), t('Block')] }) }), jsxs(Restricted, { requiredGrants: [OwnCapability.MUTE_USERS], children: [hasVideoTrack && (jsxs(GenericMenuButtonItem, { onClick: muteVideo, children: [jsx(Icon, { icon: "camera-off-outline" }), t('Turn off video')] })), hasScreenShareTrack && (jsxs(GenericMenuButtonItem, { onClick: muteScreenShare, children: [jsx(Icon, { icon: "screen-share-off" }), t('Turn off screen share')] })), hasAudioTrack && (jsxs(GenericMenuButtonItem, { onClick: muteAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute audio')] })), hasScreenShareAudioTrack && (jsxs(GenericMenuButtonItem, { onClick: muteScreenShareAudio, children: [jsx(Icon, { icon: "no-audio" }), t('Mute screen share audio')] }))] }), participantViewElement && (jsx(GenericMenuButtonItem, { onClick: toggleFullscreenMode, children: t('{{ direction }} fullscreen', {
2017
- direction: fullscreenModeOn ? t('Leave') : t('Enter'),
2018
- }) })), videoElement && document.pictureInPictureEnabled && (jsx(GenericMenuButtonItem, { onClick: togglePictureInPicture, children: t('{{ direction }} picture-in-picture', {
2019
- direction: pictureInPictureElement === videoElement
2020
- ? t('Leave')
2021
- : t('Enter'),
2022
- }) })), jsxs(Restricted, { requiredGrants: [OwnCapability.UPDATE_CALL_PERMISSIONS], children: [jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_AUDIO), children: t('Allow audio') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SEND_VIDEO), children: t('Allow video') }), jsx(GenericMenuButtonItem, { onClick: grantPermission(OwnCapability.SCREENSHARE), children: t('Allow screen sharing') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_AUDIO), children: t('Disable audio') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SEND_VIDEO), children: t('Disable video') }), jsx(GenericMenuButtonItem, { onClick: revokePermission(OwnCapability.SCREENSHARE), children: t('Disable screen sharing') })] })] }));
2023
- };
2024
-
2025
- const useTrackElementVisibility = ({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }) => {
2026
- const call = useCall();
2027
- const manager = propsDynascaleManager ?? call?.dynascaleManager;
2028
- 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
2039
  const ToggleButton = forwardRef(function ToggleButton(props, ref) {
2039
2040
  return jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
2040
2041
  });
@@ -2091,6 +2092,8 @@ const ParticipantView = forwardRef(function ParticipantView({ participant, track
2091
2092
  trackedElement,
2092
2093
  trackType,
2093
2094
  });
2095
+ const { useIncomingVideoSettings } = useCallStateHooks();
2096
+ const { isParticipantVideoEnabled } = useIncomingVideoSettings();
2094
2097
  const participantViewContextValue = useMemo(() => ({
2095
2098
  participant,
2096
2099
  participantViewElement: trackedElement,
@@ -2117,7 +2120,9 @@ const ParticipantView = forwardRef(function ParticipantView({ participant, track
2117
2120
  return (jsx("div", { "data-testid": "participant-view", ref: (element) => {
2118
2121
  applyElementToRef(ref, element);
2119
2122
  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: jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxs(Fragment, { children: [hasAudioTrack && (jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudioTrack && (jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2123
+ }, 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: jsxs(ParticipantViewContext.Provider, { value: participantViewContextValue, children: [!isLocalParticipant && !muteAudio && (jsxs(Fragment, { children: [hasAudioTrack && (jsx(Audio, { participant: participant, trackType: "audioTrack" })), hasScreenShareAudioTrack && (jsx(Audio, { participant: participant, trackType: "screenShareAudioTrack" }))] })), jsx(Video$1, { VideoPlaceholder: VideoPlaceholder, participant: participant, trackType: trackType, refs: videoRefs, enabled: isLocalParticipant ||
2124
+ trackType !== 'videoTrack' ||
2125
+ isParticipantVideoEnabled(participant.sessionId), autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
2121
2126
  });
2122
2127
  ParticipantView.displayName = 'ParticipantView';
2123
2128
 
@@ -2547,7 +2552,7 @@ const LivestreamPlayer = (props) => {
2547
2552
  return (jsx(StreamCall, { call: call, children: jsx(LivestreamLayout, { ...layoutProps }) }));
2548
2553
  };
2549
2554
 
2550
- const [major, minor, patch] = ("1.4.4").split('.');
2555
+ const [major, minor, patch] = ("1.5.0").split('.');
2551
2556
  setSdkInfo({
2552
2557
  type: SfuModels.SdkType.REACT,
2553
2558
  major,