@stream-io/video-react-sdk 1.0.6 → 1.0.8
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 +21 -0
- package/dist/css/styles.css +3 -3
- package/dist/index.cjs.js +173 -123
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +174 -124
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/Button/CompositeButton.d.ts +1 -0
- package/dist/src/components/CallControls/CancelCallButton.d.ts +2 -1
- package/dist/src/components/CallControls/ScreenShareButton.d.ts +3 -2
- package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -2
- package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -2
- package/dist/src/components/Menu/MenuToggle.d.ts +2 -1
- package/dist/src/components/Tooltip/WithTooltip.d.ts +4 -2
- package/dist/src/utilities/callControlHandler.d.ts +16 -0
- package/package.json +4 -4
- package/src/components/Button/CompositeButton.tsx +3 -0
- package/src/components/CallControls/CallControls.tsx +3 -3
- package/src/components/CallControls/CancelCallButton.tsx +12 -8
- package/src/components/CallControls/ReactionsButton.tsx +14 -9
- package/src/components/CallControls/RecordCallButton.tsx +21 -15
- package/src/components/CallControls/ScreenShareButton.tsx +34 -26
- package/src/components/CallControls/ToggleAudioButton.tsx +84 -56
- package/src/components/CallControls/ToggleVideoButton.tsx +87 -59
- package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +10 -9
- package/src/components/DeviceSettings/DeviceSelector.tsx +4 -0
- package/src/components/Menu/MenuToggle.tsx +9 -0
- package/src/components/Tooltip/WithTooltip.tsx +7 -2
- package/src/core/components/Audio/ParticipantsAudio.tsx +10 -13
- package/src/core/components/CallLayout/LivestreamLayout.tsx +5 -7
- package/src/core/components/CallLayout/SpeakerLayout.tsx +3 -5
- package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +12 -12
- package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +16 -14
- package/src/core/components/ParticipantView/ParticipantView.tsx +12 -17
- package/src/core/components/Video/Video.tsx +4 -4
- package/src/utilities/callControlHandler.ts +43 -0
|
@@ -10,10 +10,18 @@ import { DeviceSelectorVideo } from '../DeviceSettings';
|
|
|
10
10
|
import { PermissionNotification } from '../Notification';
|
|
11
11
|
import { useRequestPermission } from '../../hooks';
|
|
12
12
|
import { Icon } from '../Icon';
|
|
13
|
+
import { WithTooltip } from '../Tooltip';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
PropsWithErrorHandler,
|
|
17
|
+
createCallControlHandler,
|
|
18
|
+
} from '../../utilities/callControlHandler';
|
|
13
19
|
|
|
14
|
-
export type ToggleVideoPreviewButtonProps =
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
export type ToggleVideoPreviewButtonProps = PropsWithErrorHandler<
|
|
21
|
+
Pick<
|
|
22
|
+
IconButtonWithMenuProps,
|
|
23
|
+
'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'
|
|
24
|
+
>
|
|
17
25
|
>;
|
|
18
26
|
|
|
19
27
|
export const ToggleVideoPreviewButton = (
|
|
@@ -28,44 +36,55 @@ export const ToggleVideoPreviewButton = (
|
|
|
28
36
|
const { t } = useI18n();
|
|
29
37
|
const { useCameraState } = useCallStateHooks();
|
|
30
38
|
const { camera, optimisticIsMute, hasBrowserPermission } = useCameraState();
|
|
39
|
+
const [tooltipDisabled, setTooltipDisabled] = useState(false);
|
|
40
|
+
const handleClick = createCallControlHandler(props, () => camera.toggle());
|
|
31
41
|
|
|
32
42
|
return (
|
|
33
|
-
<
|
|
34
|
-
active={optimisticIsMute}
|
|
35
|
-
caption={caption}
|
|
36
|
-
className={clsx(!hasBrowserPermission && 'str-video__device-unavailable')}
|
|
43
|
+
<WithTooltip
|
|
37
44
|
title={
|
|
38
45
|
!hasBrowserPermission
|
|
39
46
|
? t('Check your browser video permissions')
|
|
40
|
-
: caption
|
|
41
|
-
}
|
|
42
|
-
variant="secondary"
|
|
43
|
-
data-testid={
|
|
44
|
-
optimisticIsMute
|
|
45
|
-
? 'preview-video-unmute-button'
|
|
46
|
-
: 'preview-video-mute-button'
|
|
47
|
+
: caption ?? t('Video')
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
disabled={!hasBrowserPermission}
|
|
50
|
-
Menu={Menu}
|
|
51
|
-
menuPlacement={menuPlacement}
|
|
52
|
-
{...restCompositeButtonProps}
|
|
49
|
+
tooltipDisabled={tooltipDisabled}
|
|
53
50
|
>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
<CompositeButton
|
|
52
|
+
active={optimisticIsMute}
|
|
53
|
+
caption={caption}
|
|
54
|
+
className={clsx(
|
|
55
|
+
!hasBrowserPermission && 'str-video__device-unavailable',
|
|
56
|
+
)}
|
|
57
|
+
variant="secondary"
|
|
58
|
+
data-testid={
|
|
59
|
+
optimisticIsMute
|
|
60
|
+
? 'preview-video-unmute-button'
|
|
61
|
+
: 'preview-video-mute-button'
|
|
62
|
+
}
|
|
63
|
+
onClick={handleClick}
|
|
64
|
+
disabled={!hasBrowserPermission}
|
|
65
|
+
Menu={Menu}
|
|
66
|
+
menuPlacement={menuPlacement}
|
|
67
|
+
onMenuToggle={(shown) => setTooltipDisabled(shown)}
|
|
68
|
+
{...restCompositeButtonProps}
|
|
69
|
+
>
|
|
70
|
+
<Icon icon={!optimisticIsMute ? 'camera' : 'camera-off'} />
|
|
71
|
+
{!hasBrowserPermission && (
|
|
72
|
+
<span
|
|
73
|
+
className="str-video__no-media-permission"
|
|
74
|
+
title={t('Check your browser video permissions')}
|
|
75
|
+
children="!"
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</CompositeButton>
|
|
79
|
+
</WithTooltip>
|
|
63
80
|
);
|
|
64
81
|
};
|
|
65
82
|
|
|
66
|
-
type ToggleVideoPublishingButtonProps =
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
type ToggleVideoPublishingButtonProps = PropsWithErrorHandler<
|
|
84
|
+
Pick<
|
|
85
|
+
IconButtonWithMenuProps,
|
|
86
|
+
'caption' | 'Menu' | 'menuPlacement' | 'onMenuToggle'
|
|
87
|
+
>
|
|
69
88
|
>;
|
|
70
89
|
|
|
71
90
|
export const ToggleVideoPublishingButton = (
|
|
@@ -86,6 +105,14 @@ export const ToggleVideoPublishingButton = (
|
|
|
86
105
|
const { camera, optimisticIsMute, hasBrowserPermission } = useCameraState();
|
|
87
106
|
const callSettings = useCallSettings();
|
|
88
107
|
const isPublishingVideoAllowed = callSettings?.video.enabled;
|
|
108
|
+
const [tooltipDisabled, setTooltipDisabled] = useState(false);
|
|
109
|
+
const handleClick = createCallControlHandler(props, async () => {
|
|
110
|
+
if (!hasPermission) {
|
|
111
|
+
await requestPermission();
|
|
112
|
+
} else {
|
|
113
|
+
await camera.toggle();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
89
116
|
|
|
90
117
|
return (
|
|
91
118
|
<Restricted requiredGrants={[OwnCapability.SEND_VIDEO]}>
|
|
@@ -98,10 +125,7 @@ export const ToggleVideoPublishingButton = (
|
|
|
98
125
|
)}
|
|
99
126
|
messageRevoked={t('You can no longer share your video.')}
|
|
100
127
|
>
|
|
101
|
-
<
|
|
102
|
-
active={optimisticIsMute}
|
|
103
|
-
caption={caption}
|
|
104
|
-
variant="secondary"
|
|
128
|
+
<WithTooltip
|
|
105
129
|
title={
|
|
106
130
|
!hasPermission
|
|
107
131
|
? t('You have no permission to share your video')
|
|
@@ -111,31 +135,35 @@ export const ToggleVideoPublishingButton = (
|
|
|
111
135
|
? t('Video publishing is disabled by the system')
|
|
112
136
|
: caption || t('Video')
|
|
113
137
|
}
|
|
114
|
-
|
|
115
|
-
!hasBrowserPermission || !hasPermission || !isPublishingVideoAllowed
|
|
116
|
-
}
|
|
117
|
-
data-testid={
|
|
118
|
-
optimisticIsMute ? 'video-unmute-button' : 'video-mute-button'
|
|
119
|
-
}
|
|
120
|
-
onClick={async () => {
|
|
121
|
-
if (!hasPermission) {
|
|
122
|
-
await requestPermission();
|
|
123
|
-
} else {
|
|
124
|
-
await camera.toggle();
|
|
125
|
-
}
|
|
126
|
-
}}
|
|
127
|
-
Menu={Menu}
|
|
128
|
-
menuPlacement={menuPlacement}
|
|
129
|
-
menuOffset={16}
|
|
130
|
-
{...restCompositeButtonProps}
|
|
138
|
+
tooltipDisabled={tooltipDisabled}
|
|
131
139
|
>
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
<CompositeButton
|
|
141
|
+
active={optimisticIsMute}
|
|
142
|
+
caption={caption}
|
|
143
|
+
variant="secondary"
|
|
144
|
+
disabled={
|
|
145
|
+
!hasBrowserPermission ||
|
|
146
|
+
!hasPermission ||
|
|
147
|
+
!isPublishingVideoAllowed
|
|
148
|
+
}
|
|
149
|
+
data-testid={
|
|
150
|
+
optimisticIsMute ? 'video-unmute-button' : 'video-mute-button'
|
|
151
|
+
}
|
|
152
|
+
onClick={handleClick}
|
|
153
|
+
Menu={Menu}
|
|
154
|
+
menuPlacement={menuPlacement}
|
|
155
|
+
menuOffset={16}
|
|
156
|
+
onMenuToggle={(shown) => setTooltipDisabled(shown)}
|
|
157
|
+
{...restCompositeButtonProps}
|
|
158
|
+
>
|
|
159
|
+
<Icon icon={optimisticIsMute ? 'camera-off' : 'camera'} />
|
|
160
|
+
{(!hasBrowserPermission ||
|
|
161
|
+
!hasPermission ||
|
|
162
|
+
!isPublishingVideoAllowed) && (
|
|
163
|
+
<span className="str-video__no-media-permission">!</span>
|
|
164
|
+
)}
|
|
165
|
+
</CompositeButton>
|
|
166
|
+
</WithTooltip>
|
|
139
167
|
</PermissionNotification>
|
|
140
168
|
</Restricted>
|
|
141
169
|
);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { ComponentProps, ComponentType, forwardRef } from 'react';
|
|
3
3
|
import { useConnectedUser, useI18n } from '@stream-io/video-react-bindings';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
hasAudio,
|
|
6
|
+
hasVideo,
|
|
7
|
+
isPinned,
|
|
8
|
+
StreamVideoParticipant,
|
|
9
|
+
} from '@stream-io/video-client';
|
|
5
10
|
import { IconButton } from '../Button';
|
|
6
11
|
import { MenuToggle, ToggleMenuButtonProps } from '../Menu';
|
|
7
12
|
import { WithTooltip } from '../Tooltip';
|
|
@@ -21,13 +26,9 @@ export const CallParticipantListingItem = ({
|
|
|
21
26
|
participant,
|
|
22
27
|
DisplayName = DefaultDisplayName,
|
|
23
28
|
}: CallParticipantListingItemProps) => {
|
|
24
|
-
const isAudioOn = participant
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
const isVideoOn = participant.publishedTracks.includes(
|
|
28
|
-
SfuModels.TrackType.VIDEO,
|
|
29
|
-
);
|
|
30
|
-
const isPinned = !!participant.pin;
|
|
29
|
+
const isAudioOn = hasAudio(participant);
|
|
30
|
+
const isVideoOn = hasVideo(participant);
|
|
31
|
+
const isPinnedOn = isPinned(participant);
|
|
31
32
|
|
|
32
33
|
const { t } = useI18n();
|
|
33
34
|
|
|
@@ -54,7 +55,7 @@ export const CallParticipantListingItem = ({
|
|
|
54
55
|
}`,
|
|
55
56
|
)}
|
|
56
57
|
/>
|
|
57
|
-
{
|
|
58
|
+
{isPinnedOn && (
|
|
58
59
|
<MediaIndicator
|
|
59
60
|
title={t('Pinned')}
|
|
60
61
|
className={clsx(
|
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import { ChangeEventHandler, useCallback } from 'react';
|
|
3
3
|
|
|
4
4
|
import { DropDownSelect, DropDownSelectOption } from '../DropdownSelect';
|
|
5
|
+
import { useMenuContext } from '../Menu';
|
|
5
6
|
|
|
6
7
|
type DeviceSelectorOptionProps = {
|
|
7
8
|
id: string;
|
|
@@ -64,6 +65,8 @@ const DeviceSelectorList = (props: {
|
|
|
64
65
|
onChange,
|
|
65
66
|
} = props;
|
|
66
67
|
|
|
68
|
+
const { close } = useMenuContext();
|
|
69
|
+
|
|
67
70
|
// sometimes the browser (Chrome) will report the system-default device
|
|
68
71
|
// with an id of 'default'. In case when it doesn't, we'll select the first
|
|
69
72
|
// available device.
|
|
@@ -100,6 +103,7 @@ const DeviceSelectorList = (props: {
|
|
|
100
103
|
key={device.deviceId}
|
|
101
104
|
onChange={(e) => {
|
|
102
105
|
onChange?.(e.target.value);
|
|
106
|
+
close?.();
|
|
103
107
|
}}
|
|
104
108
|
name={type}
|
|
105
109
|
selected={
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useEffect,
|
|
8
8
|
useMemo,
|
|
9
|
+
useRef,
|
|
9
10
|
useState,
|
|
10
11
|
} from 'react';
|
|
11
12
|
import {
|
|
@@ -34,6 +35,7 @@ export type MenuToggleProps<E extends HTMLElement> = PropsWithChildren<{
|
|
|
34
35
|
strategy?: Strategy;
|
|
35
36
|
offset?: number;
|
|
36
37
|
visualType?: MenuVisualType;
|
|
38
|
+
onToggle?: (menuShown: boolean) => void;
|
|
37
39
|
}>;
|
|
38
40
|
|
|
39
41
|
export type MenuContextValue = {
|
|
@@ -84,8 +86,11 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
84
86
|
offset,
|
|
85
87
|
visualType = MenuVisualType.MENU,
|
|
86
88
|
children,
|
|
89
|
+
onToggle,
|
|
87
90
|
}: MenuToggleProps<E>) => {
|
|
88
91
|
const [menuShown, setMenuShown] = useState(false);
|
|
92
|
+
const toggleHandler = useRef(onToggle);
|
|
93
|
+
toggleHandler.current = onToggle;
|
|
89
94
|
|
|
90
95
|
const { floating, domReference, refs, x, y } = useFloatingUIPreset({
|
|
91
96
|
placement,
|
|
@@ -97,8 +102,10 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
97
102
|
const handleClick = (event: MouseEvent) => {
|
|
98
103
|
if (!floating && domReference?.contains(event.target as Node)) {
|
|
99
104
|
setMenuShown(true);
|
|
105
|
+
toggleHandler.current?.(true);
|
|
100
106
|
} else if (floating && !floating?.contains(event.target as Node)) {
|
|
101
107
|
setMenuShown(false);
|
|
108
|
+
toggleHandler.current?.(false);
|
|
102
109
|
}
|
|
103
110
|
};
|
|
104
111
|
|
|
@@ -109,6 +116,7 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
109
116
|
!event.ctrlKey
|
|
110
117
|
) {
|
|
111
118
|
setMenuShown(false);
|
|
119
|
+
toggleHandler.current?.(false);
|
|
112
120
|
}
|
|
113
121
|
};
|
|
114
122
|
document?.addEventListener('click', handleClick, { capture: true });
|
|
@@ -135,6 +143,7 @@ export const MenuToggle = <E extends HTMLElement>({
|
|
|
135
143
|
left: x ?? 0,
|
|
136
144
|
overflowY: 'auto',
|
|
137
145
|
}}
|
|
146
|
+
role="menu"
|
|
138
147
|
children={children}
|
|
139
148
|
/>
|
|
140
149
|
) : null}
|
|
@@ -3,13 +3,16 @@ import { Tooltip, TooltipProps } from './Tooltip';
|
|
|
3
3
|
import { useEnterLeaveHandlers } from './hooks';
|
|
4
4
|
|
|
5
5
|
type WithPopupProps = ComponentProps<'div'> &
|
|
6
|
-
Omit<TooltipProps<HTMLDivElement>, 'referenceElement'
|
|
6
|
+
Omit<TooltipProps<HTMLDivElement>, 'referenceElement' | 'children'> & {
|
|
7
|
+
tooltipDisabled?: boolean;
|
|
8
|
+
};
|
|
7
9
|
|
|
8
10
|
// todo: duplicate of CallParticipantList.tsx#MediaIndicator - refactor to a single component
|
|
9
11
|
export const WithTooltip = ({
|
|
10
12
|
title,
|
|
11
13
|
tooltipClassName,
|
|
12
14
|
tooltipPlacement,
|
|
15
|
+
tooltipDisabled,
|
|
13
16
|
...props
|
|
14
17
|
}: WithPopupProps) => {
|
|
15
18
|
const { handleMouseEnter, handleMouseLeave, tooltipVisible } =
|
|
@@ -17,12 +20,14 @@ export const WithTooltip = ({
|
|
|
17
20
|
const [tooltipAnchor, setTooltipAnchor] = useState<HTMLDivElement | null>(
|
|
18
21
|
null,
|
|
19
22
|
);
|
|
23
|
+
const tooltipActuallyVisible =
|
|
24
|
+
!tooltipDisabled && Boolean(title) && tooltipVisible;
|
|
20
25
|
|
|
21
26
|
return (
|
|
22
27
|
<>
|
|
23
28
|
<Tooltip
|
|
24
29
|
referenceElement={tooltipAnchor}
|
|
25
|
-
visible={
|
|
30
|
+
visible={tooltipActuallyVisible}
|
|
26
31
|
tooltipClassName={tooltipClassName}
|
|
27
32
|
tooltipPlacement={tooltipPlacement}
|
|
28
33
|
>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ComponentProps, Fragment } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
hasAudio,
|
|
4
|
+
hasScreenShareAudio,
|
|
5
|
+
StreamVideoParticipant,
|
|
6
|
+
} from '@stream-io/video-client';
|
|
3
7
|
import { Audio } from './Audio';
|
|
4
8
|
|
|
5
9
|
export type ParticipantsAudioProps = {
|
|
@@ -20,15 +24,10 @@ export const ParticipantsAudio = (props: ParticipantsAudioProps) => {
|
|
|
20
24
|
<>
|
|
21
25
|
{participants.map((participant) => {
|
|
22
26
|
if (participant.isLocalParticipant) return null;
|
|
23
|
-
const {
|
|
24
|
-
publishedTracks,
|
|
25
|
-
audioStream,
|
|
26
|
-
screenShareAudioStream,
|
|
27
|
-
sessionId,
|
|
28
|
-
} = participant;
|
|
27
|
+
const { audioStream, screenShareAudioStream, sessionId } = participant;
|
|
29
28
|
|
|
30
|
-
const
|
|
31
|
-
const audioTrackElement =
|
|
29
|
+
const hasAudioTrack = hasAudio(participant);
|
|
30
|
+
const audioTrackElement = hasAudioTrack && audioStream && (
|
|
32
31
|
<Audio
|
|
33
32
|
{...audioProps}
|
|
34
33
|
trackType="audioTrack"
|
|
@@ -36,10 +35,8 @@ export const ParticipantsAudio = (props: ParticipantsAudioProps) => {
|
|
|
36
35
|
/>
|
|
37
36
|
);
|
|
38
37
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
);
|
|
42
|
-
const screenShareAudioTrackElement = hasScreenShareAudio &&
|
|
38
|
+
const hasScreenShareAudioTrack = hasScreenShareAudio(participant);
|
|
39
|
+
const screenShareAudioTrackElement = hasScreenShareAudioTrack &&
|
|
43
40
|
screenShareAudioStream && (
|
|
44
41
|
<Audio
|
|
45
42
|
{...audioProps}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useCallStateHooks,
|
|
6
6
|
useI18n,
|
|
7
7
|
} from '@stream-io/video-react-bindings';
|
|
8
|
-
import {
|
|
8
|
+
import { hasScreenShare } from '@stream-io/video-client';
|
|
9
9
|
import { ParticipantView, useParticipantViewContext } from '../ParticipantView';
|
|
10
10
|
import { ParticipantsAudio } from '../Audio';
|
|
11
11
|
import { usePaginatedLayoutSortPreset } from './hooks';
|
|
@@ -59,12 +59,13 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
|
|
|
59
59
|
const { useParticipants, useRemoteParticipants, useHasOngoingScreenShare } =
|
|
60
60
|
useCallStateHooks();
|
|
61
61
|
const call = useCall();
|
|
62
|
-
const
|
|
62
|
+
const participants = useParticipants();
|
|
63
|
+
const [currentSpeaker] = participants;
|
|
63
64
|
const remoteParticipants = useRemoteParticipants();
|
|
64
65
|
const hasOngoingScreenShare = useHasOngoingScreenShare();
|
|
65
66
|
const presenter = hasOngoingScreenShare
|
|
66
|
-
? hasScreenShare
|
|
67
|
-
:
|
|
67
|
+
? participants.find(hasScreenShare)
|
|
68
|
+
: undefined;
|
|
68
69
|
|
|
69
70
|
usePaginatedLayoutSortPreset(call);
|
|
70
71
|
|
|
@@ -122,9 +123,6 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
|
|
|
122
123
|
);
|
|
123
124
|
};
|
|
124
125
|
|
|
125
|
-
const hasScreenShare = (p?: StreamVideoParticipant) =>
|
|
126
|
-
!!p?.publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
|
|
127
|
-
|
|
128
126
|
const ParticipantOverlay = (props: {
|
|
129
127
|
enableFullScreen?: boolean;
|
|
130
128
|
showParticipantCount?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import {
|
|
3
|
+
import { hasScreenShare } from '@stream-io/video-client';
|
|
4
4
|
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -56,7 +56,8 @@ export const SpeakerLayout = ({
|
|
|
56
56
|
const [buttonsWrapperElement, setButtonsWrapperElement] =
|
|
57
57
|
useState<HTMLDivElement | null>(null);
|
|
58
58
|
|
|
59
|
-
const isSpeakerScreenSharing =
|
|
59
|
+
const isSpeakerScreenSharing =
|
|
60
|
+
participantInSpotlight && hasScreenShare(participantInSpotlight);
|
|
60
61
|
const hardLimit = useCalculateHardLimit(
|
|
61
62
|
buttonsWrapperElement,
|
|
62
63
|
participantsBarElement,
|
|
@@ -247,6 +248,3 @@ const VerticalScrollButtons = <T extends HTMLElement>({
|
|
|
247
248
|
</>
|
|
248
249
|
);
|
|
249
250
|
};
|
|
250
|
-
|
|
251
|
-
const hasScreenShare = (p?: StreamVideoParticipant) =>
|
|
252
|
-
!!p?.publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { ComponentType, forwardRef } from 'react';
|
|
2
2
|
import { Placement } from '@floating-ui/react';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
hasAudio,
|
|
5
|
+
hasScreenShare,
|
|
6
|
+
hasVideo,
|
|
7
|
+
SfuModels,
|
|
8
|
+
} from '@stream-io/video-client';
|
|
4
9
|
import { useCall, useI18n } from '@stream-io/video-react-bindings';
|
|
5
10
|
import clsx from 'clsx';
|
|
6
11
|
|
|
@@ -71,15 +76,11 @@ export const DefaultParticipantViewUI = ({
|
|
|
71
76
|
ParticipantActionsContextMenu = DefaultParticipantActionsContextMenu,
|
|
72
77
|
}: DefaultParticipantViewUIProps) => {
|
|
73
78
|
const { participant, trackType } = useParticipantViewContext();
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const hasScreenShare = publishedTracks.includes(
|
|
77
|
-
SfuModels.TrackType.SCREEN_SHARE,
|
|
78
|
-
);
|
|
79
|
+
const isScreenSharing = hasScreenShare(participant);
|
|
79
80
|
|
|
80
81
|
if (
|
|
81
82
|
participant.isLocalParticipant &&
|
|
82
|
-
|
|
83
|
+
isScreenSharing &&
|
|
83
84
|
trackType === 'screenShareTrack'
|
|
84
85
|
) {
|
|
85
86
|
return (
|
|
@@ -114,7 +115,6 @@ export const ParticipantDetails = ({
|
|
|
114
115
|
const {
|
|
115
116
|
isLocalParticipant,
|
|
116
117
|
connectionQuality,
|
|
117
|
-
publishedTracks,
|
|
118
118
|
pin,
|
|
119
119
|
sessionId,
|
|
120
120
|
name,
|
|
@@ -127,8 +127,8 @@ export const ParticipantDetails = ({
|
|
|
127
127
|
!!connectionQuality &&
|
|
128
128
|
SfuModels.ConnectionQuality[connectionQuality].toLowerCase();
|
|
129
129
|
|
|
130
|
-
const
|
|
131
|
-
const
|
|
130
|
+
const hasAudioTrack = hasAudio(participant);
|
|
131
|
+
const hasVideoTrack = hasVideo(participant);
|
|
132
132
|
const canUnpin = !!pin && pin.isLocalPin;
|
|
133
133
|
|
|
134
134
|
return (
|
|
@@ -137,10 +137,10 @@ export const ParticipantDetails = ({
|
|
|
137
137
|
<span className="str-video__participant-details__name">
|
|
138
138
|
{name || userId}
|
|
139
139
|
|
|
140
|
-
{indicatorsVisible && !
|
|
140
|
+
{indicatorsVisible && !hasAudioTrack && (
|
|
141
141
|
<span className="str-video__participant-details__name--audio-muted" />
|
|
142
142
|
)}
|
|
143
|
-
{indicatorsVisible && !
|
|
143
|
+
{indicatorsVisible && !hasVideoTrack && (
|
|
144
144
|
<span className="str-video__participant-details__name--video-muted" />
|
|
145
145
|
)}
|
|
146
146
|
{indicatorsVisible && canUnpin && (
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { Restricted, useCall, useI18n } from '@stream-io/video-react-bindings';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
hasAudio,
|
|
5
|
+
hasScreenShare,
|
|
6
|
+
hasScreenShareAudio,
|
|
7
|
+
hasVideo,
|
|
8
|
+
OwnCapability,
|
|
9
|
+
} from '@stream-io/video-client';
|
|
4
10
|
import { useParticipantViewContext } from './ParticipantViewContext';
|
|
5
11
|
import {
|
|
6
12
|
GenericMenu,
|
|
@@ -21,16 +27,12 @@ export const ParticipantActionsContextMenu = () => {
|
|
|
21
27
|
const call = useCall();
|
|
22
28
|
const { t } = useI18n();
|
|
23
29
|
|
|
24
|
-
const { pin,
|
|
30
|
+
const { pin, sessionId, userId } = participant;
|
|
25
31
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
const hasScreenShareAudio = publishedTracks.includes(
|
|
32
|
-
SfuModels.TrackType.SCREEN_SHARE_AUDIO,
|
|
33
|
-
);
|
|
32
|
+
const hasAudioTrack = hasAudio(participant);
|
|
33
|
+
const hasVideoTrack = hasVideo(participant);
|
|
34
|
+
const hasScreenShareTrack = hasScreenShare(participant);
|
|
35
|
+
const hasScreenShareAudioTrack = hasScreenShareAudio(participant);
|
|
34
36
|
|
|
35
37
|
const blockUser = () => call?.blockUser(userId);
|
|
36
38
|
const muteAudio = () => call?.muteUser(userId, 'audio');
|
|
@@ -161,25 +163,25 @@ export const ParticipantActionsContextMenu = () => {
|
|
|
161
163
|
</GenericMenuButtonItem>
|
|
162
164
|
</Restricted>
|
|
163
165
|
<Restricted requiredGrants={[OwnCapability.MUTE_USERS]}>
|
|
164
|
-
{
|
|
166
|
+
{hasVideoTrack && (
|
|
165
167
|
<GenericMenuButtonItem onClick={muteVideo}>
|
|
166
168
|
<Icon icon="camera-off-outline" />
|
|
167
169
|
{t('Turn off video')}
|
|
168
170
|
</GenericMenuButtonItem>
|
|
169
171
|
)}
|
|
170
|
-
{
|
|
172
|
+
{hasScreenShareTrack && (
|
|
171
173
|
<GenericMenuButtonItem onClick={muteScreenShare}>
|
|
172
174
|
<Icon icon="screen-share-off" />
|
|
173
175
|
{t('Turn off screen share')}
|
|
174
176
|
</GenericMenuButtonItem>
|
|
175
177
|
)}
|
|
176
|
-
{
|
|
178
|
+
{hasAudioTrack && (
|
|
177
179
|
<GenericMenuButtonItem onClick={muteAudio}>
|
|
178
180
|
<Icon icon="no-audio" />
|
|
179
181
|
{t('Mute audio')}
|
|
180
182
|
</GenericMenuButtonItem>
|
|
181
183
|
)}
|
|
182
|
-
{
|
|
184
|
+
{hasScreenShareAudioTrack && (
|
|
183
185
|
<GenericMenuButtonItem onClick={muteScreenShareAudio}>
|
|
184
186
|
<Icon icon="no-audio" />
|
|
185
187
|
{t('Mute screen share audio')}
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
} from 'react';
|
|
8
8
|
import clsx from 'clsx';
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
hasAudio,
|
|
11
|
+
hasScreenShareAudio,
|
|
12
|
+
hasVideo,
|
|
11
13
|
StreamVideoParticipant,
|
|
12
14
|
VideoTrackType,
|
|
13
15
|
} from '@stream-io/video-client';
|
|
@@ -74,19 +76,12 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
|
|
|
74
76
|
},
|
|
75
77
|
ref,
|
|
76
78
|
) {
|
|
77
|
-
const {
|
|
78
|
-
|
|
79
|
-
isSpeaking,
|
|
80
|
-
isDominantSpeaker,
|
|
81
|
-
publishedTracks,
|
|
82
|
-
sessionId,
|
|
83
|
-
} = participant;
|
|
79
|
+
const { isLocalParticipant, isSpeaking, isDominantSpeaker, sessionId } =
|
|
80
|
+
participant;
|
|
84
81
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
SfuModels.TrackType.SCREEN_SHARE_AUDIO,
|
|
89
|
-
);
|
|
82
|
+
const hasAudioTrack = hasAudio(participant);
|
|
83
|
+
const hasVideoTrack = hasVideo(participant);
|
|
84
|
+
const hasScreenShareAudioTrack = hasScreenShareAudio(participant);
|
|
90
85
|
|
|
91
86
|
const [trackedElement, setTrackedElement] = useState<HTMLDivElement | null>(
|
|
92
87
|
null,
|
|
@@ -147,8 +142,8 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
|
|
|
147
142
|
'str-video__participant-view',
|
|
148
143
|
isDominantSpeaker && 'str-video__participant-view--dominant-speaker',
|
|
149
144
|
isSpeaking && 'str-video__participant-view--speaking',
|
|
150
|
-
!
|
|
151
|
-
!
|
|
145
|
+
!hasVideoTrack && 'str-video__participant-view--no-video',
|
|
146
|
+
!hasAudioTrack && 'str-video__participant-view--no-audio',
|
|
152
147
|
className,
|
|
153
148
|
)}
|
|
154
149
|
>
|
|
@@ -156,10 +151,10 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
|
|
|
156
151
|
{/* mute the local participant, as we don't want to hear ourselves */}
|
|
157
152
|
{!isLocalParticipant && !muteAudio && (
|
|
158
153
|
<>
|
|
159
|
-
{
|
|
154
|
+
{hasAudioTrack && (
|
|
160
155
|
<Audio participant={participant} trackType="audioTrack" />
|
|
161
156
|
)}
|
|
162
|
-
{
|
|
157
|
+
{hasScreenShareAudioTrack && (
|
|
163
158
|
<Audio
|
|
164
159
|
participant={participant}
|
|
165
160
|
trackType="screenShareAudioTrack"
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
hasScreenShare,
|
|
10
|
+
hasVideo,
|
|
10
11
|
StreamVideoParticipant,
|
|
11
12
|
VideoTrackType,
|
|
12
13
|
VisibilityState,
|
|
@@ -64,7 +65,6 @@ export const Video = ({
|
|
|
64
65
|
sessionId,
|
|
65
66
|
videoStream,
|
|
66
67
|
screenShareStream,
|
|
67
|
-
publishedTracks,
|
|
68
68
|
viewportVisibilityState,
|
|
69
69
|
isLocalParticipant,
|
|
70
70
|
userId,
|
|
@@ -130,9 +130,9 @@ export const Video = ({
|
|
|
130
130
|
|
|
131
131
|
const isPublishingTrack =
|
|
132
132
|
trackType === 'videoTrack'
|
|
133
|
-
?
|
|
133
|
+
? hasVideo(participant)
|
|
134
134
|
: trackType === 'screenShareTrack'
|
|
135
|
-
?
|
|
135
|
+
? hasScreenShare(participant)
|
|
136
136
|
: false;
|
|
137
137
|
|
|
138
138
|
const isInvisible =
|