@stream-io/video-react-sdk 0.3.28 → 0.3.30

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 (44) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +7 -5
  3. package/dist/css/styles.css +100 -0
  4. package/dist/css/styles.css.map +1 -1
  5. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.js +2 -1
  6. package/dist/src/components/CallParticipantsList/CallParticipantListingItem.js.map +1 -1
  7. package/dist/src/core/components/Audio/ParticipantsAudio.d.ts +14 -0
  8. package/dist/src/core/components/Audio/ParticipantsAudio.js +11 -0
  9. package/dist/src/core/components/Audio/ParticipantsAudio.js.map +1 -0
  10. package/dist/src/core/components/Audio/index.d.ts +1 -0
  11. package/dist/src/core/components/Audio/index.js +1 -0
  12. package/dist/src/core/components/Audio/index.js.map +1 -1
  13. package/dist/src/core/components/CallLayout/LivestreamLayout.d.ts +39 -0
  14. package/dist/src/core/components/CallLayout/LivestreamLayout.js +91 -0
  15. package/dist/src/core/components/CallLayout/LivestreamLayout.js.map +1 -0
  16. package/dist/src/core/components/CallLayout/PaginatedGridLayout.js +5 -3
  17. package/dist/src/core/components/CallLayout/PaginatedGridLayout.js.map +1 -1
  18. package/dist/src/core/components/CallLayout/SpeakerLayout.d.ts +7 -2
  19. package/dist/src/core/components/CallLayout/SpeakerLayout.js +32 -37
  20. package/dist/src/core/components/CallLayout/SpeakerLayout.js.map +1 -1
  21. package/dist/src/core/components/CallLayout/hooks.d.ts +3 -0
  22. package/dist/src/core/components/CallLayout/hooks.js +41 -0
  23. package/dist/src/core/components/CallLayout/hooks.js.map +1 -0
  24. package/dist/src/core/components/CallLayout/index.d.ts +1 -0
  25. package/dist/src/core/components/CallLayout/index.js +1 -0
  26. package/dist/src/core/components/CallLayout/index.js.map +1 -1
  27. package/dist/src/core/hooks/useCalculateHardLimit.d.ts +4 -0
  28. package/dist/src/core/hooks/useCalculateHardLimit.js +56 -0
  29. package/dist/src/core/hooks/useCalculateHardLimit.js.map +1 -0
  30. package/dist/src/translations/en.json +1 -0
  31. package/dist/src/translations/index.d.ts +1 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +4 -4
  35. package/src/components/CallParticipantsList/CallParticipantListingItem.tsx +2 -1
  36. package/src/core/components/Audio/ParticipantsAudio.tsx +35 -0
  37. package/src/core/components/Audio/index.ts +1 -0
  38. package/src/core/components/CallLayout/LivestreamLayout.tsx +231 -0
  39. package/src/core/components/CallLayout/PaginatedGridLayout.tsx +33 -36
  40. package/src/core/components/CallLayout/SpeakerLayout.tsx +74 -56
  41. package/src/core/components/CallLayout/hooks.ts +54 -0
  42. package/src/core/components/CallLayout/index.ts +1 -0
  43. package/src/core/hooks/useCalculateHardLimit.ts +72 -0
  44. package/src/translations/en.json +2 -0
@@ -11,9 +11,10 @@ import {
11
11
  ParticipantView,
12
12
  ParticipantViewProps,
13
13
  } from '../ParticipantView';
14
- import { Audio } from '../Audio';
14
+ import { ParticipantsAudio } from '../Audio';
15
15
  import { IconButton } from '../../../components';
16
16
  import { chunk } from '../../../utilities';
17
+ import { usePaginatedLayoutSortPreset } from './hooks';
17
18
 
18
19
  const GROUP_SIZE = 16;
19
20
 
@@ -85,6 +86,8 @@ export const PaginatedGridLayout = ({
85
86
  // used to render audio elements
86
87
  const remoteParticipants = useRemoteParticipants();
87
88
 
89
+ usePaginatedLayoutSortPreset(call);
90
+
88
91
  // only used to render video elements
89
92
  const participantGroups = useMemo(
90
93
  () =>
@@ -109,41 +112,35 @@ export const PaginatedGridLayout = ({
109
112
  if (!call) return null;
110
113
 
111
114
  return (
112
- <>
113
- {remoteParticipants.map((participant) => (
114
- <Audio key={participant.sessionId} participant={participant} />
115
- ))}
116
- <div className="str-video__paginated-grid-layout__wrapper">
117
- <div className="str-video__paginated-grid-layout">
118
- {pageArrowsVisible && pageCount > 1 && (
119
- <IconButton
120
- icon="caret-left"
121
- disabled={page === 0}
122
- onClick={() =>
123
- setPage((currentPage) => Math.max(0, currentPage - 1))
124
- }
125
- />
126
- )}
127
- {selectedGroup && (
128
- <PaginatedGridLayoutGroup
129
- group={participantGroups[page]}
130
- VideoPlaceholder={VideoPlaceholder}
131
- ParticipantViewUI={ParticipantViewUI}
132
- />
133
- )}
134
- {pageArrowsVisible && pageCount > 1 && (
135
- <IconButton
136
- disabled={page === pageCount - 1}
137
- icon="caret-right"
138
- onClick={() =>
139
- setPage((currentPage) =>
140
- Math.min(pageCount - 1, currentPage + 1),
141
- )
142
- }
143
- />
144
- )}
145
- </div>
115
+ <div className="str-video__paginated-grid-layout__wrapper">
116
+ <ParticipantsAudio participants={remoteParticipants} />
117
+ <div className="str-video__paginated-grid-layout">
118
+ {pageArrowsVisible && pageCount > 1 && (
119
+ <IconButton
120
+ icon="caret-left"
121
+ disabled={page === 0}
122
+ onClick={() =>
123
+ setPage((currentPage) => Math.max(0, currentPage - 1))
124
+ }
125
+ />
126
+ )}
127
+ {selectedGroup && (
128
+ <PaginatedGridLayoutGroup
129
+ group={participantGroups[page]}
130
+ VideoPlaceholder={VideoPlaceholder}
131
+ ParticipantViewUI={ParticipantViewUI}
132
+ />
133
+ )}
134
+ {pageArrowsVisible && pageCount > 1 && (
135
+ <IconButton
136
+ disabled={page === pageCount - 1}
137
+ icon="caret-right"
138
+ onClick={() =>
139
+ setPage((currentPage) => Math.min(pageCount - 1, currentPage + 1))
140
+ }
141
+ />
142
+ )}
146
143
  </div>
147
- </>
144
+ </div>
148
145
  );
149
146
  };
@@ -1,15 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
-
3
- import {
4
- CallTypes,
5
- combineComparators,
6
- Comparator,
7
- defaultSortPreset,
8
- screenSharing,
9
- SfuModels,
10
- speakerLayoutSortPreset,
11
- StreamVideoParticipant,
12
- } from '@stream-io/video-client';
2
+ import clsx from 'clsx';
3
+ import { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
13
4
  import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
14
5
 
15
6
  import {
@@ -22,16 +13,23 @@ import {
22
13
  useHorizontalScrollPosition,
23
14
  useVerticalScrollPosition,
24
15
  } from '../../../hooks';
25
- import clsx from 'clsx';
16
+ import { useSpeakerLayoutSortPreset } from './hooks';
17
+ import { useCalculateHardLimit } from '../../hooks/useCalculateHardLimit';
18
+ import { ParticipantsAudio } from '../Audio';
26
19
 
27
20
  export type SpeakerLayoutProps = {
28
21
  ParticipantViewUISpotlight?: ParticipantViewProps['ParticipantViewUI'];
29
22
  ParticipantViewUIBar?: ParticipantViewProps['ParticipantViewUI'];
30
23
  /**
31
- * The position of the particpants who are not in focus.
24
+ * The position of the participants who are not in focus.
32
25
  * Providing `null` will hide the bar.
33
26
  */
34
27
  participantsBarPosition?: 'top' | 'bottom' | 'left' | 'right' | null;
28
+ /**
29
+ * Hard limits the number of the participants rendered in the participants bar.
30
+ * Providing string `dynamic` will calculate hard limit based on screen width/height.
31
+ */
32
+ participantsBarLimit?: 'dynamic' | number;
35
33
  } & Pick<ParticipantViewProps, 'VideoPlaceholder'>;
36
34
 
37
35
  const DefaultParticipantViewUIBar = () => (
@@ -45,45 +43,61 @@ export const SpeakerLayout = ({
45
43
  ParticipantViewUISpotlight = DefaultParticipantViewUISpotlight,
46
44
  VideoPlaceholder,
47
45
  participantsBarPosition = 'bottom',
46
+ participantsBarLimit,
48
47
  }: SpeakerLayoutProps) => {
49
48
  const call = useCall();
50
- const { useParticipants } = useCallStateHooks();
49
+ const { useParticipants, useRemoteParticipants } = useCallStateHooks();
51
50
  const [participantInSpotlight, ...otherParticipants] = useParticipants();
52
- const [scrollWrapper, setScrollWrapper] = useState<HTMLDivElement | null>(
53
- null,
51
+ const remoteParticipants = useRemoteParticipants();
52
+ const [participantsBarWrapperElement, setParticipantsBarWrapperElement] =
53
+ useState<HTMLDivElement | null>(null);
54
+ const [participantsBarElement, setParticipantsBarElement] =
55
+ useState<HTMLDivElement | null>(null);
56
+ const [buttonsWrapperElement, setButtonsWrapperElement] =
57
+ useState<HTMLDivElement | null>(null);
58
+
59
+ const isSpeakerScreenSharing = hasScreenShare(participantInSpotlight);
60
+ const hardLimit = useCalculateHardLimit(
61
+ buttonsWrapperElement,
62
+ participantsBarElement,
63
+ participantsBarLimit,
54
64
  );
55
- const isOneOnOneCall = otherParticipants.length === 1;
65
+
66
+ const isVertical =
67
+ participantsBarPosition === 'left' || participantsBarPosition === 'right';
68
+ const isHorizontal =
69
+ participantsBarPosition === 'top' || participantsBarPosition === 'bottom';
56
70
 
57
71
  useEffect(() => {
58
- if (!scrollWrapper || !call) return;
72
+ if (!participantsBarWrapperElement || !call) return;
59
73
 
60
- const cleanup = call.setViewport(scrollWrapper);
74
+ const cleanup = call.setViewport(participantsBarWrapperElement);
61
75
  return () => cleanup();
62
- }, [scrollWrapper, call]);
76
+ }, [participantsBarWrapperElement, call]);
63
77
 
64
- useEffect(() => {
65
- if (!call) return;
66
- // always show the remote participant in the spotlight
67
- if (isOneOnOneCall) {
68
- call.setSortParticipantsBy(combineComparators(screenSharing, loggedIn));
69
- } else {
70
- call.setSortParticipantsBy(speakerLayoutSortPreset);
71
- }
72
-
73
- return () => {
74
- // reset the sorting to the default for the call type
75
- const callConfig = CallTypes.get(call.type);
76
- call.setSortParticipantsBy(
77
- callConfig.options.sortParticipantsBy || defaultSortPreset,
78
- );
79
- };
80
- }, [call, isOneOnOneCall]);
78
+ const isOneOnOneCall = otherParticipants.length === 1;
79
+ useSpeakerLayoutSortPreset(call, isOneOnOneCall);
80
+
81
+ let participantsWithAppliedLimit = otherParticipants;
82
+
83
+ if (typeof participantsBarLimit !== 'undefined') {
84
+ const hardLimitToApply = isVertical
85
+ ? hardLimit.vertical
86
+ : hardLimit.horizontal;
87
+
88
+ participantsWithAppliedLimit = otherParticipants.slice(
89
+ 0,
90
+ // subtract 1 if speaker is sharing screen as
91
+ // that one is rendered independently from otherParticipants array
92
+ hardLimitToApply - (isSpeakerScreenSharing ? 1 : 0),
93
+ );
94
+ }
81
95
 
82
96
  if (!call) return null;
83
97
 
84
- const isSpeakerScreenSharing = hasScreenShare(participantInSpotlight);
85
98
  return (
86
99
  <div className="str-video__speaker-layout__wrapper">
100
+ <ParticipantsAudio participants={remoteParticipants} />
87
101
  <div
88
102
  className={clsx(
89
103
  'str-video__speaker-layout',
@@ -95,7 +109,7 @@ export const SpeakerLayout = ({
95
109
  {participantInSpotlight && (
96
110
  <ParticipantView
97
111
  participant={participantInSpotlight}
98
- muteAudio={isSpeakerScreenSharing}
112
+ muteAudio={true}
99
113
  trackType={
100
114
  isSpeakerScreenSharing ? 'screenShareTrack' : 'videoTrack'
101
115
  }
@@ -104,13 +118,19 @@ export const SpeakerLayout = ({
104
118
  />
105
119
  )}
106
120
  </div>
107
- {otherParticipants.length > 0 && participantsBarPosition && (
108
- <div className="str-video__speaker-layout__participants-bar-buttons-wrapper">
121
+ {participantsWithAppliedLimit.length > 0 && participantsBarPosition && (
122
+ <div
123
+ ref={setButtonsWrapperElement}
124
+ className="str-video__speaker-layout__participants-bar-buttons-wrapper"
125
+ >
109
126
  <div
110
127
  className="str-video__speaker-layout__participants-bar-wrapper"
111
- ref={setScrollWrapper}
128
+ ref={setParticipantsBarWrapperElement}
112
129
  >
113
- <div className="str-video__speaker-layout__participants-bar">
130
+ <div
131
+ ref={setParticipantsBarElement}
132
+ className="str-video__speaker-layout__participants-bar"
133
+ >
114
134
  {isSpeakerScreenSharing && (
115
135
  <div
116
136
  className="str-video__speaker-layout__participant-tile"
@@ -120,10 +140,11 @@ export const SpeakerLayout = ({
120
140
  participant={participantInSpotlight}
121
141
  ParticipantViewUI={ParticipantViewUIBar}
122
142
  VideoPlaceholder={VideoPlaceholder}
143
+ muteAudio={true}
123
144
  />
124
145
  </div>
125
146
  )}
126
- {otherParticipants.map((participant) => (
147
+ {participantsWithAppliedLimit.map((participant) => (
127
148
  <div
128
149
  className="str-video__speaker-layout__participant-tile"
129
150
  key={participant.sessionId}
@@ -132,18 +153,21 @@ export const SpeakerLayout = ({
132
153
  participant={participant}
133
154
  ParticipantViewUI={ParticipantViewUIBar}
134
155
  VideoPlaceholder={VideoPlaceholder}
156
+ muteAudio={true}
135
157
  />
136
158
  </div>
137
159
  ))}
138
160
  </div>
139
161
  </div>
140
- {(participantsBarPosition === 'left' ||
141
- participantsBarPosition === 'right') && (
142
- <VerticalScrollButtons scrollWrapper={scrollWrapper} />
162
+ {isVertical && (
163
+ <VerticalScrollButtons
164
+ scrollWrapper={participantsBarWrapperElement}
165
+ />
143
166
  )}
144
- {(participantsBarPosition === 'top' ||
145
- participantsBarPosition === 'bottom') && (
146
- <HorizontalScrollButtons scrollWrapper={scrollWrapper} />
167
+ {isHorizontal && (
168
+ <HorizontalScrollButtons
169
+ scrollWrapper={participantsBarWrapperElement}
170
+ />
147
171
  )}
148
172
  </div>
149
173
  )}
@@ -222,9 +246,3 @@ const VerticalScrollButtons = <T extends HTMLElement>({
222
246
 
223
247
  const hasScreenShare = (p?: StreamVideoParticipant) =>
224
248
  !!p?.publishedTracks.includes(SfuModels.TrackType.SCREEN_SHARE);
225
-
226
- const loggedIn: Comparator<StreamVideoParticipant> = (a, b) => {
227
- if (a.isLocalParticipant) return 1;
228
- if (b.isLocalParticipant) return -1;
229
- return 0;
230
- };
@@ -0,0 +1,54 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ Call,
4
+ CallTypes,
5
+ combineComparators,
6
+ Comparator,
7
+ defaultSortPreset,
8
+ paginatedLayoutSortPreset,
9
+ screenSharing,
10
+ speakerLayoutSortPreset,
11
+ StreamVideoParticipant,
12
+ } from '@stream-io/video-client';
13
+
14
+ export const usePaginatedLayoutSortPreset = (call: Call | undefined) => {
15
+ useEffect(() => {
16
+ if (!call) return;
17
+ call.setSortParticipantsBy(paginatedLayoutSortPreset);
18
+ return () => {
19
+ resetSortPreset(call);
20
+ };
21
+ }, [call]);
22
+ };
23
+
24
+ export const useSpeakerLayoutSortPreset = (
25
+ call: Call | undefined,
26
+ isOneOnOneCall: boolean,
27
+ ) => {
28
+ useEffect(() => {
29
+ if (!call) return;
30
+ // always show the remote participant in the spotlight
31
+ if (isOneOnOneCall) {
32
+ call.setSortParticipantsBy(combineComparators(screenSharing, loggedIn));
33
+ } else {
34
+ call.setSortParticipantsBy(speakerLayoutSortPreset);
35
+ }
36
+ return () => {
37
+ resetSortPreset(call);
38
+ };
39
+ }, [call, isOneOnOneCall]);
40
+ };
41
+
42
+ const resetSortPreset = (call: Call) => {
43
+ // reset the sorting to the default for the call type
44
+ const callConfig = CallTypes.get(call.type);
45
+ call.setSortParticipantsBy(
46
+ callConfig.options.sortParticipantsBy || defaultSortPreset,
47
+ );
48
+ };
49
+
50
+ const loggedIn: Comparator<StreamVideoParticipant> = (a, b) => {
51
+ if (a.isLocalParticipant) return 1;
52
+ if (b.isLocalParticipant) return -1;
53
+ return 0;
54
+ };
@@ -1,2 +1,3 @@
1
+ export * from './LivestreamLayout';
1
2
  export * from './PaginatedGridLayout';
2
3
  export * from './SpeakerLayout';
@@ -0,0 +1,72 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useCalculateHardLimit = (
4
+ /**
5
+ * Element that stretches to 100% of the whole layout component
6
+ */
7
+ wrapperElement: HTMLDivElement | null,
8
+ /**
9
+ * Element that directly hosts individual `ParticipantView` (or wrapper) elements
10
+ */
11
+ hostElement: HTMLDivElement | null,
12
+ limit?: 'dynamic' | number,
13
+ ) => {
14
+ const [calculatedLimit, setCalculatedLimit] = useState<{
15
+ vertical: number;
16
+ horizontal: number;
17
+ }>({
18
+ vertical: typeof limit === 'number' ? limit : 1,
19
+ horizontal: typeof limit === 'number' ? limit : 1,
20
+ });
21
+
22
+ useEffect(() => {
23
+ if (
24
+ !hostElement ||
25
+ !wrapperElement ||
26
+ typeof limit === 'number' ||
27
+ typeof limit === 'undefined'
28
+ )
29
+ return;
30
+
31
+ let childWidth: number | null = null;
32
+ let childHeight: number | null = null;
33
+
34
+ const resizeObserver = new ResizeObserver((entries, observer) => {
35
+ // this part should ideally run as little times as possible
36
+ // get child measurements and disconnect
37
+ // does not consider dynamically sized children
38
+ // this hook is for SpeakerLayout use only, where children in the bar are fixed size
39
+ if (entries.length > 1) {
40
+ const child = hostElement.firstChild as HTMLElement | null;
41
+
42
+ if (child) {
43
+ childHeight = child.clientHeight;
44
+ childWidth = child.clientWidth;
45
+ observer.unobserve(hostElement);
46
+ }
47
+ }
48
+
49
+ // keep the state at { vertical: 1, horizontal: 1 }
50
+ // until we get the proper child measurements
51
+ if (childHeight === null || childWidth === null) return;
52
+
53
+ const vertical = Math.floor(wrapperElement.clientHeight / childHeight);
54
+ const horizontal = Math.floor(wrapperElement.clientWidth / childWidth);
55
+
56
+ setCalculatedLimit((pv) => {
57
+ if (pv.vertical !== vertical || pv.horizontal !== horizontal)
58
+ return { vertical, horizontal };
59
+ return pv;
60
+ });
61
+ });
62
+
63
+ resizeObserver.observe(wrapperElement);
64
+ resizeObserver.observe(hostElement);
65
+
66
+ return () => {
67
+ resizeObserver.disconnect();
68
+ };
69
+ }, [hostElement, limit, wrapperElement]);
70
+
71
+ return calculatedLimit;
72
+ };
@@ -12,6 +12,8 @@
12
12
  "Video": "Video",
13
13
  "You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
14
14
 
15
+ "Live": "Live",
16
+
15
17
  "You can now speak.": "You can now speak.",
16
18
  "Awaiting for an approval to speak.": "Awaiting for an approval to speak.",
17
19
  "You can no longer speak.": "You can no longer speak.",