@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/css/styles.css +3 -3
  3. package/dist/index.cjs.js +173 -123
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.es.js +174 -124
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/src/components/Button/CompositeButton.d.ts +1 -0
  8. package/dist/src/components/CallControls/CancelCallButton.d.ts +2 -1
  9. package/dist/src/components/CallControls/ScreenShareButton.d.ts +3 -2
  10. package/dist/src/components/CallControls/ToggleAudioButton.d.ts +3 -2
  11. package/dist/src/components/CallControls/ToggleVideoButton.d.ts +3 -2
  12. package/dist/src/components/Menu/MenuToggle.d.ts +2 -1
  13. package/dist/src/components/Tooltip/WithTooltip.d.ts +4 -2
  14. package/dist/src/utilities/callControlHandler.d.ts +16 -0
  15. package/package.json +4 -4
  16. package/src/components/Button/CompositeButton.tsx +3 -0
  17. package/src/components/CallControls/CallControls.tsx +3 -3
  18. package/src/components/CallControls/CancelCallButton.tsx +12 -8
  19. package/src/components/CallControls/ReactionsButton.tsx +14 -9
  20. package/src/components/CallControls/RecordCallButton.tsx +21 -15
  21. package/src/components/CallControls/ScreenShareButton.tsx +34 -26
  22. package/src/components/CallControls/ToggleAudioButton.tsx +84 -56
  23. package/src/components/CallControls/ToggleVideoButton.tsx +87 -59
  24. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +10 -9
  25. package/src/components/DeviceSettings/DeviceSelector.tsx +4 -0
  26. package/src/components/Menu/MenuToggle.tsx +9 -0
  27. package/src/components/Tooltip/WithTooltip.tsx +7 -2
  28. package/src/core/components/Audio/ParticipantsAudio.tsx +10 -13
  29. package/src/core/components/CallLayout/LivestreamLayout.tsx +5 -7
  30. package/src/core/components/CallLayout/SpeakerLayout.tsx +3 -5
  31. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +12 -12
  32. package/src/core/components/ParticipantView/ParticipantActionsContextMenu.tsx +16 -14
  33. package/src/core/components/ParticipantView/ParticipantView.tsx +12 -17
  34. package/src/core/components/Video/Video.tsx +4 -4
  35. 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 = Pick<
15
- IconButtonWithMenuProps,
16
- 'caption' | 'Menu' | 'menuPlacement'
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
- <CompositeButton
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 || t('Video')
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
- onClick={() => camera.toggle()}
49
- disabled={!hasBrowserPermission}
50
- Menu={Menu}
51
- menuPlacement={menuPlacement}
52
- {...restCompositeButtonProps}
49
+ tooltipDisabled={tooltipDisabled}
53
50
  >
54
- <Icon icon={!optimisticIsMute ? 'camera' : 'camera-off'} />
55
- {!hasBrowserPermission && (
56
- <span
57
- className="str-video__no-media-permission"
58
- title={t('Check your browser video permissions')}
59
- children="!"
60
- />
61
- )}
62
- </CompositeButton>
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 = Pick<
67
- IconButtonWithMenuProps,
68
- 'caption' | 'Menu' | 'menuPlacement'
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
- <CompositeButton
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
- disabled={
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
- <Icon icon={optimisticIsMute ? 'camera-off' : 'camera'} />
133
- {(!hasBrowserPermission ||
134
- !hasPermission ||
135
- !isPublishingVideoAllowed) && (
136
- <span className="str-video__no-media-permission">!</span>
137
- )}
138
- </CompositeButton>
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 { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
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.publishedTracks.includes(
25
- SfuModels.TrackType.AUDIO,
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
- {isPinned && (
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={tooltipVisible}
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 { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
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 hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
31
- const audioTrackElement = hasAudio && audioStream && (
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 hasScreenShareAudio = publishedTracks.includes(
40
- SfuModels.TrackType.SCREEN_SHARE_AUDIO,
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 { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
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 [currentSpeaker, ...otherParticipants] = useParticipants();
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(currentSpeaker) && currentSpeaker
67
- : otherParticipants.find(hasScreenShare);
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 { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
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 = hasScreenShare(participantInSpotlight);
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 { SfuModels } from '@stream-io/video-client';
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 { publishedTracks } = participant;
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
- hasScreenShare &&
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 hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
131
- const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
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 && !hasAudio && (
140
+ {indicatorsVisible && !hasAudioTrack && (
141
141
  <span className="str-video__participant-details__name--audio-muted" />
142
142
  )}
143
- {indicatorsVisible && !hasVideo && (
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 { OwnCapability, SfuModels } from '@stream-io/video-client';
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, publishedTracks, sessionId, userId } = participant;
30
+ const { pin, sessionId, userId } = participant;
25
31
 
26
- const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
27
- const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
28
- const hasScreenShare = publishedTracks.includes(
29
- SfuModels.TrackType.SCREEN_SHARE,
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
- {hasVideo && (
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
- {hasScreenShare && (
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
- {hasAudio && (
178
+ {hasAudioTrack && (
177
179
  <GenericMenuButtonItem onClick={muteAudio}>
178
180
  <Icon icon="no-audio" />
179
181
  {t('Mute audio')}
180
182
  </GenericMenuButtonItem>
181
183
  )}
182
- {hasScreenShareAudio && (
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
- SfuModels,
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
- isLocalParticipant,
79
- isSpeaking,
80
- isDominantSpeaker,
81
- publishedTracks,
82
- sessionId,
83
- } = participant;
79
+ const { isLocalParticipant, isSpeaking, isDominantSpeaker, sessionId } =
80
+ participant;
84
81
 
85
- const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
86
- const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
87
- const hasScreenShareAudio = publishedTracks.includes(
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
- !hasVideo && 'str-video__participant-view--no-video',
151
- !hasAudio && 'str-video__participant-view--no-audio',
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
- {hasAudio && (
154
+ {hasAudioTrack && (
160
155
  <Audio participant={participant} trackType="audioTrack" />
161
156
  )}
162
- {hasScreenShareAudio && (
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
- SfuModels,
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
- ? publishedTracks.includes(SfuModels.TrackType.VIDEO)
133
+ ? hasVideo(participant)
134
134
  : trackType === 'screenShareTrack'
135
- ? publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE)
135
+ ? hasScreenShare(participant)
136
136
  : false;
137
137
 
138
138
  const isInvisible =