@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/CHANGELOG.md +19 -0
- package/dist/css/styles.css +5 -5
- package/dist/css/styles.css.map +1 -1
- package/dist/index.cjs.js +441 -432
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +443 -434
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +1 -1
- package/dist/src/core/components/CallLayout/LivestreamLayout.d.ts +5 -0
- package/dist/src/core/components/CallLayout/PaginatedGridLayout.d.ts +5 -0
- package/dist/src/core/components/CallLayout/SpeakerLayout.d.ts +6 -1
- package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +10 -0
- package/dist/src/core/components/Video/Video.d.ts +11 -1
- package/package.json +4 -4
- package/src/components/Button/CompositeButton.tsx +3 -0
- package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +2 -4
- package/src/components/DropdownSelect/DropdownSelect.tsx +2 -2
- package/src/core/components/CallLayout/LivestreamLayout.tsx +9 -0
- package/src/core/components/CallLayout/PaginatedGridLayout.tsx +12 -1
- package/src/core/components/CallLayout/SpeakerLayout.tsx +11 -0
- package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +2 -2
- package/src/core/components/ParticipantView/ParticipantView.tsx +17 -0
- package/src/core/components/Video/Video.tsx +17 -2
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
|
|
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,
|
|
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.
|
|
2559
|
+
const [major, minor, patch] = ("1.6.0").split('.');
|
|
2551
2560
|
videoClient.setSdkInfo({
|
|
2552
2561
|
type: videoClient.SfuModels.SdkType.REACT,
|
|
2553
2562
|
major,
|