@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/CHANGELOG.md +18 -0
- package/dist/css/styles.css +5 -5
- package/dist/css/styles.css.map +1 -1
- package/dist/index.cjs.js +429 -424
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +431 -426
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/DropdownSelect/DropdownSelect.d.ts +1 -1
- package/dist/src/core/components/Video/Video.d.ts +6 -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/ParticipantView/ParticipantActionsContextMenu.tsx +2 -2
- package/src/core/components/ParticipantView/ParticipantView.tsx +9 -0
- package/src/core/components/Video/Video.tsx +8 -1
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,424 @@ 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, 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 = isLocalParticipant && trackType === 'videoTrack';
|
|
625
|
+
const isScreenShareTrack = trackType === 'screenShareTrack';
|
|
626
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!hasNoVideoOrInvisible && (jsxRuntime.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 && (jsxRuntime.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 = videoReactBindings.useCall();
|
|
640
|
+
const manager = propsDynascaleManager ?? call?.dynascaleManager;
|
|
641
|
+
react.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] = react.useState(false);
|
|
653
|
+
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 }))] }));
|
|
654
|
+
};
|
|
655
|
+
const AvatarFallback = ({ className, names, style, }) => {
|
|
656
|
+
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]] }) }));
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The context for the background filters.
|
|
661
|
+
*/
|
|
662
|
+
const BackgroundFiltersContext = react.createContext(undefined);
|
|
663
|
+
/**
|
|
664
|
+
* A hook to access the background filters context API.
|
|
665
|
+
*/
|
|
666
|
+
const useBackgroundFilters = () => {
|
|
667
|
+
const context = react.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] = react.useState(bgFilterFromProps);
|
|
682
|
+
const [backgroundImage, setBackgroundImage] = react.useState(bgImageFromProps);
|
|
683
|
+
const [backgroundBlurLevel, setBackgroundBlurLevel] = react.useState(bgBlurLevelFromProps);
|
|
684
|
+
const applyBackgroundImageFilter = react.useCallback((imageUrl) => {
|
|
685
|
+
setBackgroundFilter('image');
|
|
686
|
+
setBackgroundImage(imageUrl);
|
|
687
|
+
}, []);
|
|
688
|
+
const applyBackgroundBlurFilter = react.useCallback((blurLevel = 'high') => {
|
|
689
|
+
setBackgroundFilter('blur');
|
|
690
|
+
setBackgroundBlurLevel(blurLevel);
|
|
691
|
+
}, []);
|
|
692
|
+
const disableBackgroundFilter = react.useCallback(() => {
|
|
693
|
+
setBackgroundFilter(undefined);
|
|
694
|
+
setBackgroundImage(undefined);
|
|
695
|
+
setBackgroundBlurLevel('high');
|
|
696
|
+
}, []);
|
|
697
|
+
const [isSupported, setIsSupported] = react.useState(false);
|
|
698
|
+
react.useEffect(() => {
|
|
699
|
+
videoFiltersWeb.isPlatformSupported().then(setIsSupported);
|
|
700
|
+
}, []);
|
|
701
|
+
const [tfLite, setTfLite] = react.useState();
|
|
702
|
+
react.useEffect(() => {
|
|
703
|
+
// don't try to load TFLite if the platform is not supported
|
|
704
|
+
if (!isSupported)
|
|
705
|
+
return;
|
|
706
|
+
videoFiltersWeb.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 = react.useCallback((error) => {
|
|
711
|
+
videoClient.getLogger(['filters'])('warn', 'Filter encountered an error and will be disabled');
|
|
712
|
+
disableBackgroundFilter();
|
|
713
|
+
onError?.(error);
|
|
714
|
+
}, [disableBackgroundFilter, onError]);
|
|
715
|
+
return (jsxRuntime.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 && jsxRuntime.jsx(BackgroundFilters, { tfLite: tfLite })] }));
|
|
730
|
+
};
|
|
731
|
+
const BackgroundFilters = (props) => {
|
|
732
|
+
const call = videoReactBindings.useCall();
|
|
733
|
+
const { children, start } = useRenderer(props.tfLite);
|
|
734
|
+
const { backgroundFilter, onError } = useBackgroundFilters();
|
|
735
|
+
const handleErrorRef = react.useRef(undefined);
|
|
736
|
+
handleErrorRef.current = onError;
|
|
737
|
+
react.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 = react.useRef(null);
|
|
750
|
+
const canvasRef = react.useRef(null);
|
|
751
|
+
const bgImageRef = react.useRef(null);
|
|
752
|
+
const [videoSize, setVideoSize] = react.useState({
|
|
753
|
+
width: 1920,
|
|
754
|
+
height: 1080,
|
|
755
|
+
});
|
|
756
|
+
const start = react.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
|
+
reactDom.flushSync(() => setVideoSize({
|
|
781
|
+
width: trackSettings.width ?? 0,
|
|
782
|
+
height: trackSettings.height ?? 0,
|
|
783
|
+
}));
|
|
784
|
+
renderer = videoFiltersWeb.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 && videoClient.disposeOfMediaStream(outputStream);
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}, [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite]);
|
|
804
|
+
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 &&
|
|
805
|
+
'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 })] }));
|
|
806
|
+
return {
|
|
807
|
+
start,
|
|
808
|
+
children,
|
|
809
|
+
};
|
|
810
|
+
};
|
|
811
|
+
|
|
557
812
|
const IconButton = react.forwardRef(function IconButton(props, ref) {
|
|
558
813
|
const { icon, enabled, variant, onClick, className, ...rest } = props;
|
|
559
814
|
return (jsxRuntime.jsx("button", { className: clsx('str-video__call-controls__button', className, {
|
|
@@ -565,26 +820,7 @@ const IconButton = react.forwardRef(function IconButton(props, ref) {
|
|
|
565
820
|
}, ref: ref, ...rest, children: jsxRuntime.jsx(Icon, { icon: icon }) }));
|
|
566
821
|
});
|
|
567
822
|
|
|
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) {
|
|
823
|
+
const CompositeButton = react.forwardRef(function CompositeButton({ disabled, caption, children, className, active, Menu, menuPlacement, menuOffset, title, ToggleMenuButton = DefaultToggleMenuButton, variant, onClick, onMenuToggle, ...restButtonProps }, ref) {
|
|
588
824
|
return (jsxRuntime.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 = react.forwardRef(function CompositeButton({ caption, chi
|
|
|
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: [jsxRuntime.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 && (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 }))] }));
|
|
835
|
+
}, 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
836
|
});
|
|
600
837
|
const DefaultToggleMenuButton = react.forwardRef(function DefaultToggleMenuButton({ menuShown }, ref) {
|
|
601
838
|
return (jsxRuntime.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: [jsxRuntime.jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsxRuntime.jsx("span", { className: "str-video__dropdown-label", children: label })] }));
|
|
1206
|
+
}), children: [icon && jsxRuntime.jsx(Icon, { className: "str-video__dropdown-icon", icon: icon }), jsxRuntime.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 (jsxRuntime.jsx(Component, { ...props, className: clsx('str-video', className), children: children }));
|
|
1772
2009
|
};
|
|
1773
2010
|
|
|
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
2011
|
const DefaultDisabledVideoPreview = () => {
|
|
1894
2012
|
const { t } = videoReactBindings.useI18n();
|
|
1895
2013
|
return (jsxRuntime.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 (jsxRuntime.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] = 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
2039
|
const ToggleButton = react.forwardRef(function ToggleButton(props, ref) {
|
|
2039
2040
|
return jsxRuntime.jsx(IconButton, { enabled: props.menuShown, icon: "ellipsis", ref: ref });
|
|
2040
2041
|
});
|
|
@@ -2091,6 +2092,8 @@ const ParticipantView = react.forwardRef(function ParticipantView({ participant,
|
|
|
2091
2092
|
trackedElement,
|
|
2092
2093
|
trackType,
|
|
2093
2094
|
});
|
|
2095
|
+
const { useIncomingVideoSettings } = videoReactBindings.useCallStateHooks();
|
|
2096
|
+
const { isParticipantVideoEnabled } = useIncomingVideoSettings();
|
|
2094
2097
|
const participantViewContextValue = react.useMemo(() => ({
|
|
2095
2098
|
participant,
|
|
2096
2099
|
participantViewElement: trackedElement,
|
|
@@ -2117,7 +2120,9 @@ const ParticipantView = react.forwardRef(function ParticipantView({ participant,
|
|
|
2117
2120
|
return (jsxRuntime.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: 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,
|
|
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: 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 ||
|
|
2124
|
+
trackType !== 'videoTrack' ||
|
|
2125
|
+
isParticipantVideoEnabled(participant.sessionId), autoPlay: true }), isComponentType(ParticipantViewUI) ? (jsxRuntime.jsx(ParticipantViewUI, {})) : (ParticipantViewUI)] }) }));
|
|
2121
2126
|
});
|
|
2122
2127
|
ParticipantView.displayName = 'ParticipantView';
|
|
2123
2128
|
|
|
@@ -2547,7 +2552,7 @@ const LivestreamPlayer = (props) => {
|
|
|
2547
2552
|
return (jsxRuntime.jsx(StreamCall, { call: call, children: jsxRuntime.jsx(LivestreamLayout, { ...layoutProps }) }));
|
|
2548
2553
|
};
|
|
2549
2554
|
|
|
2550
|
-
const [major, minor, patch] = ("1.
|
|
2555
|
+
const [major, minor, patch] = ("1.5.0").split('.');
|
|
2551
2556
|
videoClient.setSdkInfo({
|
|
2552
2557
|
type: videoClient.SfuModels.SdkType.REACT,
|
|
2553
2558
|
major,
|