@stream-io/video-react-sdk 1.14.0 → 1.14.2

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.
@@ -39,7 +39,11 @@ export type PaginatedGridLayoutProps = {
39
39
  * @default true
40
40
  */
41
41
  pageArrowsVisible?: boolean;
42
- } & Pick<ParticipantViewProps, 'ParticipantViewUI' | 'VideoPlaceholder'>;
42
+ /**
43
+ * Whether the layout is muted. Defaults to `false`.
44
+ */
45
+ muted?: boolean;
46
+ } & Pick<ParticipantViewProps, 'ParticipantViewUI' | 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
43
47
  export declare const PaginatedGridLayout: {
44
48
  (props: PaginatedGridLayoutProps): import("react/jsx-runtime").JSX.Element | null;
45
49
  displayName: string;
@@ -0,0 +1,43 @@
1
+ import { ParticipantViewProps } from '../ParticipantView';
2
+ import { ParticipantFilter, ParticipantPredicate } from './hooks';
3
+ export type PipLayoutProps = {
4
+ /**
5
+ * Whether to exclude the local participant from the grid.
6
+ * @default false
7
+ */
8
+ excludeLocalParticipant?: boolean;
9
+ /**
10
+ * Predicate to filter call participants or a filter object.
11
+ * @example
12
+ * // With a predicate:
13
+ * <PipLayout
14
+ * filterParticipants={p => p.roles.includes('student')}
15
+ * />
16
+ * @example
17
+ * // With a filter object:
18
+ * <PipLayout
19
+ * filterParticipants={{
20
+ * $or: [
21
+ * { roles: { $contains: 'student' } },
22
+ * { isPinned: true },
23
+ * ],
24
+ * }}
25
+ * />
26
+ */
27
+ filterParticipants?: ParticipantPredicate | ParticipantFilter;
28
+ /**
29
+ * When set to `false` disables mirroring of the local partipant's video.
30
+ * @default true
31
+ */
32
+ mirrorLocalParticipantVideo?: boolean;
33
+ } & Pick<ParticipantViewProps, 'ParticipantViewUI' | 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
34
+ export declare const PipLayout: {
35
+ Pip: {
36
+ (props: PipLayoutProps): import("react/jsx-runtime").JSX.Element | null;
37
+ displayName: string;
38
+ };
39
+ Host: {
40
+ (): import("react/jsx-runtime").JSX.Element;
41
+ displayName: string;
42
+ };
43
+ };
@@ -53,8 +53,12 @@ export type SpeakerLayoutProps = {
53
53
  * @default true
54
54
  */
55
55
  pageArrowsVisible?: boolean;
56
+ /**
57
+ * Whether the layout is muted. Defaults to `false`.
58
+ */
59
+ muted?: boolean;
56
60
  } & Pick<ParticipantViewProps, 'VideoPlaceholder' | 'PictureInPicturePlaceholder'>;
57
61
  export declare const SpeakerLayout: {
58
- ({ ParticipantViewUIBar, ParticipantViewUISpotlight, VideoPlaceholder, PictureInPicturePlaceholder, participantsBarPosition, participantsBarLimit, mirrorLocalParticipantVideo, excludeLocalParticipant, filterParticipants, pageArrowsVisible, }: SpeakerLayoutProps): import("react/jsx-runtime").JSX.Element | null;
62
+ ({ ParticipantViewUIBar, ParticipantViewUISpotlight, VideoPlaceholder, PictureInPicturePlaceholder, participantsBarPosition, participantsBarLimit, mirrorLocalParticipantVideo, excludeLocalParticipant, filterParticipants, pageArrowsVisible, muted, }: SpeakerLayoutProps): import("react/jsx-runtime").JSX.Element | null;
59
63
  displayName: string;
60
64
  };
@@ -2,6 +2,8 @@ import { Call, StreamVideoParticipant } from '@stream-io/video-client';
2
2
  import { Filter } from '../../../utilities/filter';
3
3
  export type FilterableParticipant = Pick<StreamVideoParticipant, 'userId' | 'isSpeaking' | 'isDominantSpeaker' | 'name' | 'roles'> & {
4
4
  isPinned: boolean;
5
+ hasVideo: boolean;
6
+ hasAudio: boolean;
5
7
  };
6
8
  export type ParticipantFilter = Filter<FilterableParticipant>;
7
9
  export type ParticipantPredicate = (paritcipant: StreamVideoParticipant) => boolean;
@@ -1,4 +1,6 @@
1
1
  export * from './LivestreamLayout';
2
2
  export * from './PaginatedGridLayout';
3
3
  export * from './SpeakerLayout';
4
+ export * from './PipLayout';
5
+ export { useFilteredParticipants } from './hooks';
4
6
  export type { FilterableParticipant, ParticipantFilter, ParticipantPredicate, } from './hooks';
@@ -12,12 +12,12 @@ export declare const useFloatingUIPreset: ({ middleware, placement, strategy, of
12
12
  y: number;
13
13
  domReference: Element | null;
14
14
  floating: HTMLElement | null;
15
- strategy: import("@floating-ui/utils").Strategy | undefined;
15
+ strategy: import("@floating-ui/react").Strategy | undefined;
16
16
  context: {
17
17
  x: number;
18
18
  y: number;
19
- placement: import("@floating-ui/utils").Placement;
20
- strategy: import("@floating-ui/utils").Strategy;
19
+ placement: import("@floating-ui/react").Placement;
20
+ strategy: import("@floating-ui/react").Strategy;
21
21
  middlewareData: import("@floating-ui/core").MiddlewareData;
22
22
  isPositioned: boolean;
23
23
  update: () => void;
@@ -27,7 +27,7 @@ export declare const useFloatingUIPreset: ({ middleware, placement, strategy, of
27
27
  events: import("@floating-ui/react").FloatingEvents;
28
28
  dataRef: React.MutableRefObject<import("@floating-ui/react").ContextData>;
29
29
  nodeId: string | undefined;
30
- floatingId: string;
30
+ floatingId: string | undefined;
31
31
  refs: import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
32
32
  elements: import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
33
33
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/index.d.ts",
@@ -29,10 +29,10 @@
29
29
  "*.css"
30
30
  ],
31
31
  "dependencies": {
32
- "@floating-ui/react": "^0.26.24",
33
- "@stream-io/video-client": "1.19.0",
32
+ "@floating-ui/react": "^0.27.6",
33
+ "@stream-io/video-client": "1.19.2",
34
34
  "@stream-io/video-filters-web": "0.1.7",
35
- "@stream-io/video-react-bindings": "1.5.12",
35
+ "@stream-io/video-react-bindings": "1.5.14",
36
36
  "chart.js": "^4.4.4",
37
37
  "clsx": "^2.0.0",
38
38
  "react-chartjs-2": "^5.3.0"
@@ -46,7 +46,7 @@
46
46
  "@rollup/plugin-replace": "^6.0.2",
47
47
  "@rollup/plugin-typescript": "^12.1.2",
48
48
  "@stream-io/audio-filters-web": "^0.2.3",
49
- "@stream-io/video-styling": "^1.1.2",
49
+ "@stream-io/video-styling": "^1.1.3",
50
50
  "@types/react": "^18.3.2",
51
51
  "@types/react-dom": "^18.3.0",
52
52
  "react": "^18.3.1",
@@ -98,6 +98,8 @@ export const MenuToggle = <E extends HTMLElement>({
98
98
  });
99
99
 
100
100
  useEffect(() => {
101
+ const parentDocument = domReference?.ownerDocument;
102
+
101
103
  const handleClick = (event: MouseEvent) => {
102
104
  if (!floating && domReference?.contains(event.target as Node)) {
103
105
  setMenuShown(true);
@@ -119,11 +121,13 @@ export const MenuToggle = <E extends HTMLElement>({
119
121
  toggleHandler.current?.(false);
120
122
  }
121
123
  };
122
- document?.addEventListener('click', handleClick, { capture: true });
123
- document?.addEventListener('keydown', handleKeyDown);
124
+ parentDocument?.addEventListener('click', handleClick, { capture: true });
125
+ parentDocument?.addEventListener('keydown', handleKeyDown);
124
126
  return () => {
125
- document?.removeEventListener('click', handleClick, { capture: true });
126
- document?.removeEventListener('keydown', handleKeyDown);
127
+ parentDocument?.removeEventListener('click', handleClick, {
128
+ capture: true,
129
+ });
130
+ parentDocument?.removeEventListener('keydown', handleKeyDown);
127
131
  };
128
132
  }, [floating, domReference]);
129
133
 
@@ -106,7 +106,15 @@ export type PaginatedGridLayoutProps = {
106
106
  * @default true
107
107
  */
108
108
  pageArrowsVisible?: boolean;
109
- } & Pick<ParticipantViewProps, 'ParticipantViewUI' | 'VideoPlaceholder'>;
109
+
110
+ /**
111
+ * Whether the layout is muted. Defaults to `false`.
112
+ */
113
+ muted?: boolean;
114
+ } & Pick<
115
+ ParticipantViewProps,
116
+ 'ParticipantViewUI' | 'VideoPlaceholder' | 'PictureInPicturePlaceholder'
117
+ >;
110
118
 
111
119
  export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
112
120
  const {
@@ -119,6 +127,8 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
119
127
  pageArrowsVisible = true,
120
128
  VideoPlaceholder,
121
129
  ParticipantViewUI = DefaultParticipantViewUI,
130
+ PictureInPicturePlaceholder,
131
+ muted,
122
132
  } = props;
123
133
  const [page, setPage] = useState(0);
124
134
  const [
@@ -169,7 +179,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
169
179
  className="str-video__paginated-grid-layout__wrapper"
170
180
  ref={setPaginatedGridLayoutWrapperElement}
171
181
  >
172
- <ParticipantsAudio participants={remoteParticipants} />
182
+ {!muted && <ParticipantsAudio participants={remoteParticipants} />}
173
183
  <div className="str-video__paginated-grid-layout">
174
184
  {pageArrowsVisible && pageCount > 1 && (
175
185
  <IconButton
@@ -186,6 +196,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
186
196
  mirror={mirror}
187
197
  VideoPlaceholder={VideoPlaceholder}
188
198
  ParticipantViewUI={ParticipantViewUI}
199
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
189
200
  />
190
201
  )}
191
202
  {pageArrowsVisible && pageCount > 1 && (
@@ -0,0 +1,110 @@
1
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ import { ParticipantsAudio } from '../Audio';
5
+ import {
6
+ DefaultParticipantViewUI,
7
+ ParticipantView,
8
+ ParticipantViewProps,
9
+ } from '../ParticipantView';
10
+ import {
11
+ ParticipantFilter,
12
+ ParticipantPredicate,
13
+ useFilteredParticipants,
14
+ usePaginatedLayoutSortPreset,
15
+ } from './hooks';
16
+
17
+ export type PipLayoutProps = {
18
+ /**
19
+ * Whether to exclude the local participant from the grid.
20
+ * @default false
21
+ */
22
+ excludeLocalParticipant?: boolean;
23
+
24
+ /**
25
+ * Predicate to filter call participants or a filter object.
26
+ * @example
27
+ * // With a predicate:
28
+ * <PipLayout
29
+ * filterParticipants={p => p.roles.includes('student')}
30
+ * />
31
+ * @example
32
+ * // With a filter object:
33
+ * <PipLayout
34
+ * filterParticipants={{
35
+ * $or: [
36
+ * { roles: { $contains: 'student' } },
37
+ * { isPinned: true },
38
+ * ],
39
+ * }}
40
+ * />
41
+ */
42
+ filterParticipants?: ParticipantPredicate | ParticipantFilter;
43
+
44
+ /**
45
+ * When set to `false` disables mirroring of the local partipant's video.
46
+ * @default true
47
+ */
48
+ mirrorLocalParticipantVideo?: boolean;
49
+ } & Pick<
50
+ ParticipantViewProps,
51
+ 'ParticipantViewUI' | 'VideoPlaceholder' | 'PictureInPicturePlaceholder'
52
+ >;
53
+
54
+ const Pip = (props: PipLayoutProps) => {
55
+ const {
56
+ excludeLocalParticipant = false,
57
+ filterParticipants,
58
+ mirrorLocalParticipantVideo = true,
59
+ VideoPlaceholder,
60
+ ParticipantViewUI = DefaultParticipantViewUI,
61
+ PictureInPicturePlaceholder,
62
+ } = props;
63
+ const [layoutWrapperElement, setLayoutWrapperElement] =
64
+ useState<HTMLDivElement | null>(null);
65
+
66
+ const call = useCall();
67
+ const participants = useFilteredParticipants({
68
+ excludeLocalParticipant,
69
+ filterParticipants,
70
+ });
71
+
72
+ usePaginatedLayoutSortPreset(call);
73
+
74
+ useEffect(() => {
75
+ if (!layoutWrapperElement || !call) return;
76
+ return call.setViewport(layoutWrapperElement);
77
+ }, [layoutWrapperElement, call]);
78
+
79
+ const mirror = mirrorLocalParticipantVideo ? undefined : false;
80
+
81
+ if (!call) return null;
82
+
83
+ return (
84
+ <div className="str-video__pip-layout" ref={setLayoutWrapperElement}>
85
+ {participants.map((participant) => (
86
+ <ParticipantView
87
+ key={participant.sessionId}
88
+ participant={participant}
89
+ muteAudio
90
+ mirror={mirror}
91
+ VideoPlaceholder={VideoPlaceholder}
92
+ PictureInPicturePlaceholder={PictureInPicturePlaceholder}
93
+ ParticipantViewUI={ParticipantViewUI}
94
+ />
95
+ ))}
96
+ </div>
97
+ );
98
+ };
99
+
100
+ Pip.displayName = 'PipLayout.Pip';
101
+
102
+ const Host = () => {
103
+ const { useRemoteParticipants } = useCallStateHooks();
104
+ const remoteParticipants = useRemoteParticipants();
105
+ return <ParticipantsAudio participants={remoteParticipants} />;
106
+ };
107
+
108
+ Host.displayName = 'PipLayout.Host';
109
+
110
+ export const PipLayout = { Pip, Host };
@@ -27,25 +27,30 @@ export type SpeakerLayoutProps = {
27
27
  * The UI to be used for the participant in the spotlight.
28
28
  */
29
29
  ParticipantViewUISpotlight?: ParticipantViewProps['ParticipantViewUI'];
30
+
30
31
  /**
31
32
  * The UI to be used for the participants in the participants bar.
32
33
  */
33
34
  ParticipantViewUIBar?: ParticipantViewProps['ParticipantViewUI'];
35
+
34
36
  /**
35
37
  * The position of the participants who are not in focus.
36
38
  * Providing `null` will hide the bar.
37
39
  */
38
40
  participantsBarPosition?: 'top' | 'bottom' | 'left' | 'right' | null;
41
+
39
42
  /**
40
43
  * Hard limits the number of the participants rendered in the participants bar.
41
44
  * Providing string `dynamic` will calculate hard limit based on screen width/height.
42
45
  */
43
46
  participantsBarLimit?: 'dynamic' | number;
47
+
44
48
  /**
45
49
  * When set to `true` will exclude the local participant from layout.
46
50
  * @default false
47
51
  */
48
52
  excludeLocalParticipant?: boolean;
53
+
49
54
  /**
50
55
  * Predicate to filter call participants or a filter object.
51
56
  * @example
@@ -65,16 +70,23 @@ export type SpeakerLayoutProps = {
65
70
  * />
66
71
  */
67
72
  filterParticipants?: ParticipantPredicate | ParticipantFilter;
73
+
68
74
  /**
69
75
  * When set to `false` disables mirroring of the local participant's video.
70
76
  * @default true
71
77
  */
72
78
  mirrorLocalParticipantVideo?: boolean;
79
+
73
80
  /**
74
81
  * Turns on/off the pagination arrows.
75
82
  * @default true
76
83
  */
77
84
  pageArrowsVisible?: boolean;
85
+
86
+ /**
87
+ * Whether the layout is muted. Defaults to `false`.
88
+ */
89
+ muted?: boolean;
78
90
  } & Pick<
79
91
  ParticipantViewProps,
80
92
  'VideoPlaceholder' | 'PictureInPicturePlaceholder'
@@ -95,6 +107,7 @@ export const SpeakerLayout = ({
95
107
  excludeLocalParticipant = false,
96
108
  filterParticipants,
97
109
  pageArrowsVisible = true,
110
+ muted,
98
111
  }: SpeakerLayoutProps) => {
99
112
  const call = useCall();
100
113
  const { useParticipants, useRemoteParticipants } = useCallStateHooks();
@@ -160,7 +173,7 @@ export const SpeakerLayout = ({
160
173
  (participantsWithAppliedLimit.length > 0 || isSpeakerScreenSharing);
161
174
  return (
162
175
  <div className="str-video__speaker-layout__wrapper">
163
- <ParticipantsAudio participants={remoteParticipants} />
176
+ {!muted && <ParticipantsAudio participants={remoteParticipants} />}
164
177
  <div
165
178
  className={clsx(
166
179
  'str-video__speaker-layout',
@@ -5,6 +5,8 @@ import {
5
5
  combineComparators,
6
6
  Comparator,
7
7
  defaultSortPreset,
8
+ hasAudio,
9
+ hasVideo,
8
10
  isPinned,
9
11
  paginatedLayoutSortPreset,
10
12
  screenSharing,
@@ -17,7 +19,11 @@ import { applyFilter, Filter } from '../../../utilities/filter';
17
19
  export type FilterableParticipant = Pick<
18
20
  StreamVideoParticipant,
19
21
  'userId' | 'isSpeaking' | 'isDominantSpeaker' | 'name' | 'roles'
20
- > & { isPinned: boolean };
22
+ > & {
23
+ isPinned: boolean;
24
+ hasVideo: boolean;
25
+ hasAudio: boolean;
26
+ };
21
27
 
22
28
  export type ParticipantFilter = Filter<FilterableParticipant>;
23
29
  export type ParticipantPredicate = (
@@ -66,6 +72,8 @@ export const applyParticipantsFilter = (
66
72
  name: participant.name,
67
73
  roles: participant.roles,
68
74
  isPinned: isPinned(participant),
75
+ hasVideo: hasVideo(participant),
76
+ hasAudio: hasAudio(participant),
69
77
  },
70
78
  filter,
71
79
  );
@@ -1,6 +1,8 @@
1
1
  export * from './LivestreamLayout';
2
2
  export * from './PaginatedGridLayout';
3
3
  export * from './SpeakerLayout';
4
+ export * from './PipLayout';
5
+ export { useFilteredParticipants } from './hooks';
4
6
  export type {
5
7
  FilterableParticipant,
6
8
  ParticipantFilter,