@stream-io/video-react-sdk 0.0.1-alpha.33 → 0.0.1-alpha.35

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 (81) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/css/styles.css +134 -145
  3. package/dist/css/styles.css.map +1 -1
  4. package/dist/src/components/Button/CompositeButton.js +2 -4
  5. package/dist/src/components/Button/CompositeButton.js.map +1 -1
  6. package/dist/src/components/StreamCall/CallParticipantsScreenView.js +3 -3
  7. package/dist/src/components/StreamCall/CallParticipantsScreenView.js.map +1 -1
  8. package/dist/src/components/StreamCall/CallParticipantsView.js +2 -3
  9. package/dist/src/components/StreamCall/CallParticipantsView.js.map +1 -1
  10. package/dist/src/core/components/CallLayout/PaginatedGridLayout.d.ts +3 -7
  11. package/dist/src/core/components/CallLayout/PaginatedGridLayout.js +7 -11
  12. package/dist/src/core/components/CallLayout/PaginatedGridLayout.js.map +1 -1
  13. package/dist/src/core/components/CallLayout/SpeakerLayout.d.ts +6 -1
  14. package/dist/src/core/components/CallLayout/SpeakerLayout.js +5 -3
  15. package/dist/src/core/components/CallLayout/SpeakerLayout.js.map +1 -1
  16. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.d.ts +20 -0
  17. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js +33 -0
  18. package/dist/src/core/components/ParticipantView/DefaultParticipantViewUI.js.map +1 -0
  19. package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +82 -0
  20. package/dist/src/core/components/ParticipantView/ParticipantView.js +28 -0
  21. package/dist/src/core/components/ParticipantView/ParticipantView.js.map +1 -0
  22. package/dist/src/core/components/ParticipantView/index.d.ts +2 -0
  23. package/dist/src/core/components/ParticipantView/index.js +3 -0
  24. package/dist/src/core/components/ParticipantView/index.js.map +1 -0
  25. package/dist/src/core/components/Video/BaseVideo.d.ts +3 -3
  26. package/dist/src/core/components/Video/BaseVideo.js +6 -12
  27. package/dist/src/core/components/Video/BaseVideo.js.map +1 -1
  28. package/dist/src/core/components/Video/Video.d.ts +8 -6
  29. package/dist/src/core/components/Video/Video.js +27 -25
  30. package/dist/src/core/components/Video/Video.js.map +1 -1
  31. package/dist/src/core/components/Video/VideoPlaceholder.d.ts +3 -3
  32. package/dist/src/core/components/Video/VideoPlaceholder.js +2 -5
  33. package/dist/src/core/components/Video/VideoPlaceholder.js.map +1 -1
  34. package/dist/src/core/components/index.d.ts +2 -2
  35. package/dist/src/core/components/index.js +1 -1
  36. package/dist/src/core/components/index.js.map +1 -1
  37. package/dist/src/core/hooks/index.d.ts +1 -0
  38. package/dist/src/core/hooks/index.js +1 -0
  39. package/dist/src/core/hooks/index.js.map +1 -1
  40. package/dist/src/core/hooks/useTrackElementVisibility.d.ts +6 -0
  41. package/dist/src/core/hooks/useTrackElementVisibility.js +24 -0
  42. package/dist/src/core/hooks/useTrackElementVisibility.js.map +1 -0
  43. package/dist/src/utilities/applyElementRef.d.ts +2 -0
  44. package/dist/src/utilities/applyElementRef.js +8 -0
  45. package/dist/src/utilities/applyElementRef.js.map +1 -0
  46. package/dist/src/utilities/chunk.d.ts +1 -0
  47. package/dist/src/utilities/chunk.js +5 -0
  48. package/dist/src/utilities/chunk.js.map +1 -0
  49. package/dist/src/utilities/index.d.ts +3 -0
  50. package/dist/src/utilities/index.js +4 -0
  51. package/dist/src/utilities/index.js.map +1 -0
  52. package/dist/src/utilities/isComponentType.d.ts +2 -0
  53. package/dist/src/utilities/isComponentType.js +7 -0
  54. package/dist/src/utilities/isComponentType.js.map +1 -0
  55. package/package.json +5 -5
  56. package/src/components/Button/CompositeButton.tsx +4 -13
  57. package/src/components/StreamCall/CallParticipantsScreenView.tsx +3 -4
  58. package/src/components/StreamCall/CallParticipantsView.tsx +3 -4
  59. package/src/core/components/CallLayout/PaginatedGridLayout.tsx +21 -40
  60. package/src/core/components/CallLayout/SpeakerLayout.tsx +46 -19
  61. package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +165 -0
  62. package/src/core/components/ParticipantView/ParticipantView.tsx +136 -0
  63. package/src/core/components/ParticipantView/index.ts +2 -0
  64. package/src/core/components/Video/BaseVideo.tsx +9 -24
  65. package/src/core/components/Video/Video.tsx +55 -44
  66. package/src/core/components/Video/VideoPlaceholder.tsx +7 -11
  67. package/src/core/components/index.ts +2 -2
  68. package/src/core/hooks/index.ts +1 -0
  69. package/src/core/hooks/useTrackElementVisibility.ts +41 -0
  70. package/src/utilities/applyElementRef.ts +12 -0
  71. package/src/utilities/chunk.ts +8 -0
  72. package/src/utilities/index.ts +3 -0
  73. package/src/utilities/isComponentType.ts +9 -0
  74. package/dist/src/core/components/ParticipantBox/ParticipantBox.d.ts +0 -48
  75. package/dist/src/core/components/ParticipantBox/ParticipantBox.js +0 -58
  76. package/dist/src/core/components/ParticipantBox/ParticipantBox.js.map +0 -1
  77. package/dist/src/core/components/ParticipantBox/index.d.ts +0 -1
  78. package/dist/src/core/components/ParticipantBox/index.js +0 -2
  79. package/dist/src/core/components/ParticipantBox/index.js.map +0 -1
  80. package/src/core/components/ParticipantBox/ParticipantBox.tsx +0 -248
  81. package/src/core/components/ParticipantBox/index.ts +0 -1
@@ -0,0 +1,165 @@
1
+ import { forwardRef } from 'react';
2
+ import { Placement } from '@floating-ui/react';
3
+ import { SfuModels } from '@stream-io/video-client';
4
+ import { useCall } from '@stream-io/video-react-bindings';
5
+ import { clsx } from 'clsx';
6
+
7
+ import {
8
+ IconButton,
9
+ MenuToggle,
10
+ Notification,
11
+ ParticipantActionsContextMenu,
12
+ ToggleMenuButtonProps,
13
+ } from '../../../components';
14
+ import { Reaction } from '../../../components/Reaction';
15
+ import { ParticipantViewProps } from './ParticipantView';
16
+
17
+ import { DebugParticipantPublishQuality } from '../../../components/Debug/DebugParticipantPublishQuality';
18
+ import { DebugStatsView } from '../../../components/Debug/DebugStatsView';
19
+ import { useIsDebugMode } from '../../../components/Debug/useIsDebugMode';
20
+
21
+ export type ParticipantViewUIProps = Pick<ParticipantViewProps, 'participant'>;
22
+
23
+ export type DefaultParticipantViewUIProps = {
24
+ /**
25
+ * Turns on/off the status indicator icons (mute, connection quality, etc...).
26
+ */
27
+ indicatorsVisible?: boolean;
28
+ /**
29
+ * Placement of the context menu component when opened
30
+ */
31
+ menuPlacement?: Placement;
32
+ /**
33
+ * Option to show/hide menu button component
34
+ */
35
+ showMenuButton?: boolean;
36
+ } & ParticipantViewUIProps;
37
+
38
+ const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
39
+ (props, ref) => {
40
+ return <IconButton enabled={props.menuShown} icon="ellipsis" ref={ref} />;
41
+ },
42
+ );
43
+
44
+ export const DefaultParticipantViewUI = ({
45
+ participant,
46
+ indicatorsVisible = true,
47
+ menuPlacement = 'bottom-end',
48
+ showMenuButton = true,
49
+ }: DefaultParticipantViewUIProps) => {
50
+ const call = useCall()!;
51
+ const { reaction, sessionId } = participant;
52
+
53
+ return (
54
+ <>
55
+ {showMenuButton && (
56
+ <MenuToggle
57
+ strategy="fixed"
58
+ placement={menuPlacement}
59
+ ToggleButton={ToggleButton}
60
+ >
61
+ <ParticipantActionsContextMenu participant={participant} />
62
+ </MenuToggle>
63
+ )}
64
+ {reaction && (
65
+ <Reaction reaction={reaction} sessionId={sessionId} call={call} />
66
+ )}
67
+ <ParticipantDetails
68
+ participant={participant}
69
+ indicatorsVisible={indicatorsVisible}
70
+ />
71
+ </>
72
+ );
73
+ };
74
+
75
+ export const ParticipantDetails = ({
76
+ participant,
77
+ indicatorsVisible = true,
78
+ }: Pick<
79
+ DefaultParticipantViewUIProps,
80
+ 'participant' | 'indicatorsVisible'
81
+ >) => {
82
+ const {
83
+ isDominantSpeaker,
84
+ isLoggedInUser,
85
+ connectionQuality,
86
+ publishedTracks,
87
+ pinnedAt,
88
+ sessionId,
89
+ name,
90
+ userId,
91
+ videoStream,
92
+ } = participant;
93
+ const call = useCall()!;
94
+
95
+ const connectionQualityAsString =
96
+ !!connectionQuality &&
97
+ String(SfuModels.ConnectionQuality[connectionQuality]).toLowerCase();
98
+
99
+ const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
100
+ const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
101
+ const isPinned = !!pinnedAt;
102
+
103
+ const isDebugMode = useIsDebugMode();
104
+
105
+ return (
106
+ <div className="str-video__participant-details">
107
+ <span className="str-video__participant-details__name">
108
+ {name || userId}
109
+ {indicatorsVisible && isDominantSpeaker && (
110
+ <span
111
+ className="str-video__participant-details__name--dominant_speaker"
112
+ title="Dominant speaker"
113
+ />
114
+ )}
115
+ {indicatorsVisible && (
116
+ <Notification
117
+ isVisible={
118
+ isLoggedInUser &&
119
+ connectionQuality === SfuModels.ConnectionQuality.POOR
120
+ }
121
+ message="Poor connection quality. Please check your internet connection."
122
+ >
123
+ {connectionQualityAsString && (
124
+ <span
125
+ className={clsx(
126
+ 'str-video__participant-details__connection-quality',
127
+ `str-video__participant-details__connection-quality--${connectionQualityAsString}`,
128
+ )}
129
+ title={connectionQualityAsString}
130
+ />
131
+ )}
132
+ </Notification>
133
+ )}
134
+ {indicatorsVisible && !hasAudio && (
135
+ <span className="str-video__participant-details__name--audio-muted" />
136
+ )}
137
+ {indicatorsVisible && !hasVideo && (
138
+ <span className="str-video__participant-details__name--video-muted" />
139
+ )}
140
+ {indicatorsVisible && isPinned && (
141
+ // TODO: remove this monstrosity once we have a proper design
142
+ <span
143
+ title="Unpin"
144
+ onClick={() => call?.setParticipantPinnedAt(sessionId)}
145
+ style={{ cursor: 'pointer' }}
146
+ className="str-video__participant-details__name--pinned"
147
+ />
148
+ )}
149
+ </span>
150
+ {isDebugMode && (
151
+ <>
152
+ <DebugParticipantPublishQuality
153
+ participant={participant}
154
+ call={call}
155
+ />
156
+ <DebugStatsView
157
+ call={call}
158
+ kind={isLoggedInUser ? 'publisher' : 'subscriber'}
159
+ mediaStream={videoStream}
160
+ />
161
+ </>
162
+ )}
163
+ </div>
164
+ );
165
+ };
@@ -0,0 +1,136 @@
1
+ import { forwardRef, ComponentType, useState, ReactElement } from 'react';
2
+ import clsx from 'clsx';
3
+ import {
4
+ SfuModels,
5
+ StreamVideoLocalParticipant,
6
+ StreamVideoParticipant,
7
+ } from '@stream-io/video-client';
8
+
9
+ import { Audio } from '../Audio';
10
+ import { Video, VideoProps } from '../Video';
11
+ import { useTrackElementVisibility } from '../../hooks';
12
+ import {
13
+ DefaultParticipantViewUI,
14
+ ParticipantViewUIProps,
15
+ } from './DefaultParticipantViewUI';
16
+ import { isComponentType, applyElementRef } from '../../../utilities';
17
+
18
+ export type ParticipantViewProps = {
19
+ /**
20
+ * The participant bound to this component.
21
+ */
22
+ participant: StreamVideoParticipant | StreamVideoLocalParticipant;
23
+
24
+ /**
25
+ * Component used to render user interface elements (details, network status...),
26
+ * pass `null` if you wish to not render anything
27
+ * @default DefaultParticipantViewUI
28
+ */
29
+ ParticipantViewUI?:
30
+ | ComponentType<ParticipantViewUIProps>
31
+ | ReactElement
32
+ | null;
33
+
34
+ /**
35
+ * In supported browsers, this sets the default audio output.
36
+ * The value of this prop should be a valid Audio Output `deviceId`.
37
+ */
38
+ sinkId?: string;
39
+
40
+ /**
41
+ * The kind of video stream to play for the given participant.
42
+ */
43
+ videoKind?: 'video' | 'screen';
44
+
45
+ /**
46
+ * Turns on/off the audio for the participant.
47
+ */
48
+ muteAudio?: boolean;
49
+
50
+ /**
51
+ * A function meant for exposing the "native" element ref to the integrators.
52
+ * The element can either be:
53
+ * - `<video />` for participants with enabled video.
54
+ * - `<div />` for participants with disabled video. This ref would point to
55
+ * the VideoPlaceholder component.
56
+ *
57
+ * @param element the element ref.
58
+ */
59
+ setVideoElementRef?: (element: HTMLElement | null) => void;
60
+
61
+ /**
62
+ * Custom class applied to the root DOM element.
63
+ */
64
+ className?: string;
65
+ } & Pick<VideoProps, 'VideoPlaceholder'>;
66
+
67
+ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
68
+ (
69
+ {
70
+ participant,
71
+ sinkId,
72
+ videoKind = 'video',
73
+ muteAudio,
74
+ setVideoElementRef,
75
+ className,
76
+ VideoPlaceholder,
77
+ ParticipantViewUI = DefaultParticipantViewUI as ComponentType<ParticipantViewUIProps>,
78
+ },
79
+ ref,
80
+ ) => {
81
+ const {
82
+ audioStream,
83
+ isLoggedInUser,
84
+ isSpeaking,
85
+ publishedTracks,
86
+ sessionId,
87
+ } = participant;
88
+
89
+ const hasAudio = publishedTracks.includes(SfuModels.TrackType.AUDIO);
90
+ const hasVideo = publishedTracks.includes(SfuModels.TrackType.VIDEO);
91
+
92
+ const [trackedElement, setTrackedElement] = useState<HTMLDivElement | null>(
93
+ null,
94
+ );
95
+
96
+ useTrackElementVisibility({
97
+ sessionId,
98
+ trackedElement,
99
+ });
100
+
101
+ return (
102
+ <div
103
+ ref={(element) => {
104
+ applyElementRef(ref, element);
105
+ setTrackedElement(element);
106
+ }}
107
+ className={clsx(
108
+ 'str-video__participant-view',
109
+ isSpeaking && 'str-video__participant-view--speaking',
110
+ !hasVideo && 'str-video__participant-view--no-video',
111
+ !hasAudio && 'str-video__participant-view--no-audio',
112
+ className,
113
+ )}
114
+ >
115
+ <Audio
116
+ // mute the local participant, as we don't want to hear ourselves
117
+ muted={isLoggedInUser || muteAudio}
118
+ sinkId={sinkId}
119
+ audioStream={audioStream}
120
+ />
121
+ <Video
122
+ VideoPlaceholder={VideoPlaceholder}
123
+ participant={participant}
124
+ kind={videoKind}
125
+ setVideoElementRef={setVideoElementRef}
126
+ autoPlay
127
+ />
128
+ {isComponentType(ParticipantViewUI) ? (
129
+ <ParticipantViewUI participant={participant} />
130
+ ) : (
131
+ ParticipantViewUI
132
+ )}
133
+ </div>
134
+ );
135
+ },
136
+ );
@@ -0,0 +1,2 @@
1
+ export * from './ParticipantView';
2
+ export * from './DefaultParticipantViewUI';
@@ -1,18 +1,9 @@
1
- import {
2
- DetailedHTMLProps,
3
- ForwardedRef,
4
- forwardRef,
5
- useEffect,
6
- useState,
7
- VideoHTMLAttributes,
8
- } from 'react';
9
- import clsx from 'clsx';
1
+ import { ComponentPropsWithRef, forwardRef, useEffect, useState } from 'react';
10
2
  import { Browsers } from '@stream-io/video-client';
11
3
 
12
- export type VideoProps = DetailedHTMLProps<
13
- VideoHTMLAttributes<HTMLVideoElement>,
14
- HTMLVideoElement
15
- > & {
4
+ import { applyElementRef } from '../../../utilities';
5
+
6
+ export type BaseVideoProps = ComponentPropsWithRef<'video'> & {
16
7
  stream?: MediaStream;
17
8
  };
18
9
 
@@ -20,19 +11,11 @@ export type VideoProps = DetailedHTMLProps<
20
11
  * @description Extends video element with `stream` property
21
12
  * (`srcObject`) to reactively handle stream changes
22
13
  */
23
- export const BaseVideo = forwardRef<HTMLVideoElement, VideoProps>(
14
+ export const BaseVideo = forwardRef<HTMLVideoElement, BaseVideoProps>(
24
15
  ({ stream, ...rest }, ref) => {
25
16
  const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
26
17
  null,
27
18
  );
28
- const setRef: ForwardedRef<HTMLVideoElement> = (instance) => {
29
- setVideoElement(instance);
30
- if (typeof ref === 'function') {
31
- (ref as (instance: HTMLVideoElement | null) => void)(instance);
32
- } else if (ref) {
33
- ref.current = instance;
34
- }
35
- };
36
19
 
37
20
  useEffect(() => {
38
21
  if (!videoElement || !stream) return;
@@ -60,8 +43,10 @@ export const BaseVideo = forwardRef<HTMLVideoElement, VideoProps>(
60
43
  autoPlay
61
44
  playsInline
62
45
  {...rest}
63
- className={clsx('str-video__base-video', rest.className)}
64
- ref={setRef}
46
+ ref={(element) => {
47
+ applyElementRef(ref, element);
48
+ setVideoElement(element);
49
+ }}
65
50
  />
66
51
  );
67
52
  },
@@ -1,45 +1,60 @@
1
1
  import {
2
- DetailedHTMLProps,
2
+ ComponentType,
3
3
  useCallback,
4
4
  useEffect,
5
5
  useRef,
6
6
  useState,
7
- VideoHTMLAttributes,
7
+ ComponentPropsWithoutRef,
8
8
  } from 'react';
9
9
  import {
10
- Call,
11
10
  DebounceType,
12
11
  SfuModels,
13
12
  StreamVideoParticipant,
14
13
  VisibilityState,
15
14
  } from '@stream-io/video-client';
16
15
  import clsx from 'clsx';
17
- import { VideoPlaceholder } from './VideoPlaceholder';
16
+ import {
17
+ VideoPlaceholder as DefaultVideoPlaceholder,
18
+ VideoPlaceholderProps,
19
+ } from './VideoPlaceholder';
18
20
  import { BaseVideo } from './BaseVideo';
21
+ import { useCall } from '@stream-io/video-react-bindings';
22
+
23
+ export type VideoProps = ComponentPropsWithoutRef<'video'> & {
24
+ kind: 'video' | 'screen';
25
+ participant: StreamVideoParticipant;
26
+ setVideoElementRef?: (element: HTMLElement | null) => void;
27
+ VideoPlaceholder?: ComponentType<VideoPlaceholderProps>;
28
+ };
19
29
 
20
- export const Video = (
21
- props: DetailedHTMLProps<
22
- VideoHTMLAttributes<HTMLVideoElement>,
23
- HTMLVideoElement
24
- > & {
25
- call: Call;
26
- kind: 'video' | 'screen';
27
- participant: StreamVideoParticipant;
28
- setVideoElementRef?: (element: HTMLElement | null) => void;
29
- },
30
- ) => {
31
- const { call, kind, participant, className, setVideoElementRef, ...rest } =
32
- props;
33
- const { sessionId, videoStream, screenShareStream, publishedTracks } =
34
- participant;
30
+ export const Video = ({
31
+ kind,
32
+ participant,
33
+ className,
34
+ setVideoElementRef,
35
+ VideoPlaceholder = DefaultVideoPlaceholder,
36
+ ...rest
37
+ }: VideoProps) => {
38
+ const {
39
+ sessionId,
40
+ videoStream,
41
+ screenShareStream,
42
+ publishedTracks,
43
+ viewportVisibilityState,
44
+ isLoggedInUser,
45
+ userId,
46
+ } = participant;
47
+
48
+ const call = useCall();
35
49
 
36
50
  const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
37
51
  null,
38
52
  );
53
+
39
54
  // const [videoTrackMuted, setVideoTrackMuted] = useState(false);
40
55
  const [videoPlaying, setVideoPlaying] = useState(false);
41
56
  const viewportVisibilityRef = useRef<VisibilityState | undefined>(
42
- participant.viewportVisibilityState,
57
+ viewportVisibilityState,
43
58
  );
44
59
 
45
60
  const stream = kind === 'video' ? videoStream : screenShareStream;
@@ -75,7 +90,7 @@ export const Video = (
75
90
 
76
91
  const displayPlaceholder =
77
92
  !isPublishingTrack ||
78
- (participant.viewportVisibilityState === VisibilityState.INVISIBLE &&
93
+ (viewportVisibilityState === VisibilityState.INVISIBLE &&
79
94
  !screenShareStream) ||
80
95
  !videoPlaying;
81
96
 
@@ -85,6 +100,8 @@ export const Video = (
85
100
  dimension?: SfuModels.VideoDimension,
86
101
  type: DebounceType = DebounceType.SLOW,
87
102
  ) => {
103
+ if (!call) return;
104
+
88
105
  call.updateSubscriptionsPartial(
89
106
  kind,
90
107
  {
@@ -98,17 +115,14 @@ export const Video = (
98
115
  [call, kind, sessionId],
99
116
  );
100
117
 
101
- // handle generic subscription updates
102
-
103
118
  // handle visibility subscription updates
104
119
  useEffect(() => {
105
- viewportVisibilityRef.current = participant.viewportVisibilityState;
120
+ viewportVisibilityRef.current = viewportVisibilityState;
106
121
 
107
- if (!videoElement || !isPublishingTrack || participant.isLoggedInUser)
108
- return;
122
+ if (!videoElement || !isPublishingTrack || isLoggedInUser) return;
109
123
 
110
124
  const isInvisibleVVS =
111
- participant.viewportVisibilityState === VisibilityState.INVISIBLE;
125
+ viewportVisibilityState === VisibilityState.INVISIBLE;
112
126
 
113
127
  updateSubscription(
114
128
  isInvisibleVVS
@@ -121,16 +135,15 @@ export const Video = (
121
135
  );
122
136
  }, [
123
137
  updateSubscription,
124
- participant.viewportVisibilityState,
138
+ viewportVisibilityState,
125
139
  videoElement,
126
140
  isPublishingTrack,
127
- participant.isLoggedInUser,
141
+ isLoggedInUser,
128
142
  ]);
129
143
 
130
144
  // handle resize subscription updates
131
145
  useEffect(() => {
132
- if (!videoElement || !isPublishingTrack || participant.isLoggedInUser)
133
- return;
146
+ if (!videoElement || !isPublishingTrack || isLoggedInUser) return;
134
147
 
135
148
  const resizeObserver = new ResizeObserver(() => {
136
149
  const currentDimensions = `${videoElement.clientWidth},${videoElement.clientHeight}`;
@@ -163,14 +176,14 @@ export const Video = (
163
176
  }, [
164
177
  updateSubscription,
165
178
  videoElement,
166
- participant.viewportVisibilityState,
179
+ viewportVisibilityState,
167
180
  isPublishingTrack,
168
- participant.isLoggedInUser,
181
+ isLoggedInUser,
169
182
  ]);
170
183
 
184
+ // handle generic subscription updates
171
185
  useEffect(() => {
172
- if (!isPublishingTrack || !videoElement || participant.isLoggedInUser)
173
- return;
186
+ if (!isPublishingTrack || !videoElement || isLoggedInUser) return;
174
187
 
175
188
  updateSubscription(
176
189
  {
@@ -183,12 +196,7 @@ export const Video = (
183
196
  return () => {
184
197
  updateSubscription(undefined, DebounceType.FAST);
185
198
  };
186
- }, [
187
- updateSubscription,
188
- videoElement,
189
- isPublishingTrack,
190
- participant.isLoggedInUser,
191
- ]);
199
+ }, [updateSubscription, videoElement, isPublishingTrack, isLoggedInUser]);
192
200
 
193
201
  const [isWideMode, setIsWideMode] = useState(true);
194
202
  useEffect(() => {
@@ -210,16 +218,19 @@ export const Video = (
210
218
  };
211
219
  }, [stream, videoElement]);
212
220
 
221
+ if (!call) return null;
222
+
213
223
  return (
214
224
  <>
215
225
  <BaseVideo
216
226
  {...rest}
217
227
  stream={stream}
218
- className={clsx(className, {
219
- 'str-video__video--wide': isWideMode,
228
+ className={clsx(className, 'str-video__video', {
220
229
  'str-video__video--tall': !isWideMode,
230
+ 'str-video__video--mirror': isLoggedInUser && kind === 'video',
231
+ 'str-video__video--screen-share': kind === 'screen',
221
232
  })}
222
- data-user-id={participant.userId}
233
+ data-user-id={userId}
223
234
  data-session-id={sessionId}
224
235
  ref={(ref) => {
225
236
  setVideoElement(ref);
@@ -1,10 +1,9 @@
1
- import { ComponentProps, forwardRef, useState } from 'react';
2
- import { clsx } from 'clsx';
3
- import { StreamVideoParticipant } from '@stream-io/video-client';
1
+ import { ComponentPropsWithRef, forwardRef, useState } from 'react';
2
+ import type { StreamVideoParticipant } from '@stream-io/video-client';
4
3
 
5
4
  export type VideoPlaceholderProps = {
6
5
  participant: StreamVideoParticipant;
7
- } & ComponentProps<'div'>;
6
+ } & ComponentPropsWithRef<'div'>;
8
7
 
9
8
  export const VideoPlaceholder = forwardRef<
10
9
  HTMLDivElement,
@@ -15,10 +14,10 @@ export const VideoPlaceholder = forwardRef<
15
14
  const name = participant?.name || participant?.userId;
16
15
 
17
16
  return (
18
- <div className="str-video__participant-placeholder" style={style} ref={ref}>
17
+ <div className="str-video__video-placeholder" style={style} ref={ref}>
19
18
  {(!participant.image || error) &&
20
19
  (name ? (
21
- <div className="str-video__participant-placeholder--initials-fallback">
20
+ <div className="str-video__video-placeholder__initials-fallback">
22
21
  <div>{name[0]}</div>
23
22
  </div>
24
23
  ) : (
@@ -27,11 +26,8 @@ export const VideoPlaceholder = forwardRef<
27
26
  {participant.image && !error && (
28
27
  <img
29
28
  onError={() => setError(true)}
30
- alt="participant-placeholder"
31
- className={clsx('str-video__participant-placeholder--avatar', {
32
- 'str-video__participant-placeholder--avatar-speaking':
33
- participant.isSpeaking,
34
- })}
29
+ alt="video-placeholder"
30
+ className="str-video__video-placeholder__avatar"
35
31
  src={participant.image}
36
32
  />
37
33
  )}
@@ -1,7 +1,7 @@
1
1
  export * from './Audio';
2
- export * from './ParticipantBox';
2
+ export * from './ParticipantView';
3
3
 
4
4
  export { Video } from './Video';
5
- export type { VideoProps } from './Video';
5
+ export type { BaseVideoProps, VideoProps } from './Video';
6
6
 
7
7
  export * from './CallLayout';
@@ -1,2 +1,3 @@
1
1
  export * from './useAudioPublisher';
2
2
  export * from './useVideoPublisher';
3
+ export * from './useTrackElementVisibility';
@@ -0,0 +1,41 @@
1
+ import { useEffect } from 'react';
2
+ import { ViewportTracker, VisibilityState } from '@stream-io/video-client';
3
+ import { useCall } from '@stream-io/video-react-bindings';
4
+
5
+ export const useTrackElementVisibility = <T extends HTMLElement>({
6
+ trackedElement,
7
+ viewportTracker: propsViewportTracker,
8
+ sessionId,
9
+ }: {
10
+ trackedElement: T | null;
11
+ sessionId: string;
12
+ viewportTracker?: ViewportTracker;
13
+ }) => {
14
+ const call = useCall();
15
+
16
+ const viewportTracker = propsViewportTracker ?? call?.viewportTracker;
17
+
18
+ useEffect(() => {
19
+ if (!trackedElement || !viewportTracker || !call) return;
20
+
21
+ const unobserve = viewportTracker.observe(trackedElement, (entry) => {
22
+ call.state.updateParticipant(sessionId, (p) => ({
23
+ ...p,
24
+ viewportVisibilityState: entry.isIntersecting
25
+ ? VisibilityState.VISIBLE
26
+ : VisibilityState.INVISIBLE,
27
+ }));
28
+ });
29
+
30
+ return () => {
31
+ unobserve();
32
+ // reset visibility state to UNKNOWN upon cleanup
33
+ // so that the layouts that are not actively observed
34
+ // can still function normally (runtime layout switching)
35
+ call.state.updateParticipant(sessionId, (p) => ({
36
+ ...p,
37
+ viewportVisibilityState: VisibilityState.UNKNOWN,
38
+ }));
39
+ };
40
+ }, [trackedElement, viewportTracker, call, sessionId]);
41
+ };
@@ -0,0 +1,12 @@
1
+ import type { ForwardedRef } from 'react';
2
+
3
+ export const applyElementRef = <T extends HTMLElement | null>(
4
+ ref: ForwardedRef<T>,
5
+ element: T,
6
+ ) => {
7
+ if (!ref) return;
8
+
9
+ if (typeof ref === 'function') return ref(element);
10
+
11
+ ref.current = element;
12
+ };
@@ -0,0 +1,8 @@
1
+ export const chunk = <T extends unknown[]>(array: T, size: number) => {
2
+ const chunkCount = Math.ceil(array.length / size);
3
+
4
+ return Array.from(
5
+ { length: chunkCount },
6
+ (_, index) => array.slice(size * index, size * index + size) as T,
7
+ );
8
+ };
@@ -0,0 +1,3 @@
1
+ export * from './isComponentType';
2
+ export * from './chunk';
3
+ export * from './applyElementRef';