@stream-io/video-react-sdk 1.17.1 → 1.18.1

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.
@@ -45,3 +45,17 @@ export declare const LivestreamLayout: {
45
45
  (props: LivestreamLayoutProps): import("react/jsx-runtime").JSX.Element;
46
46
  displayName: string;
47
47
  };
48
+ /**
49
+ * The props for the {@link LivestreamLayout} component.
50
+ */
51
+ export type BackstageLayoutProps = {
52
+ /**
53
+ * Whether to show the counter for participants that joined before
54
+ * the livestream went live. Defaults to `true`.
55
+ */
56
+ showEarlyParticipantCount?: boolean;
57
+ };
58
+ export declare const BackstageLayout: {
59
+ (props: BackstageLayoutProps): import("react/jsx-runtime").JSX.Element;
60
+ displayName: string;
61
+ };
@@ -0,0 +1 @@
1
+ export declare function useEffectEvent<P extends unknown[]>(cb: ((...args: P) => void) | undefined): (...args: P) => void;
@@ -13,6 +13,10 @@ export declare const translations: {
13
13
  Video: string;
14
14
  "You are muted. Unmute to speak.": string;
15
15
  Live: string;
16
+ "Livestream starts soon": string;
17
+ "Livestream starts at {{ startsAt }}": string;
18
+ "{{ count }} participants joined early_one": string;
19
+ "{{ count }} participants joined early_other": string;
16
20
  "You can now speak.": string;
17
21
  "Awaiting for an approval to speak.": string;
18
22
  "You can no longer speak.": string;
@@ -1,4 +1,4 @@
1
- import { LivestreamLayoutProps } from '../../core';
1
+ import { BackstageLayoutProps, LivestreamLayoutProps } from '../../core';
2
2
  export type LivestreamPlayerProps = {
3
3
  /**
4
4
  * The call type. Usually `livestream`.
@@ -8,9 +8,29 @@ export type LivestreamPlayerProps = {
8
8
  * The call ID.
9
9
  */
10
10
  callId: string;
11
+ /**
12
+ * Determines when the viewer joins the call.
13
+ *
14
+ * `"asap"` behavior means joining the call as soon as it is possible
15
+ * (either the `join_ahead_time_seconds` setting allows it, or the user
16
+ * has a the capability to join backstage).
17
+ *
18
+ * `"live"` behavior means joining the call when it goes live.
19
+ *
20
+ * @default "asap"
21
+ */
22
+ joinBehavior?: 'asap' | 'live';
11
23
  /**
12
24
  * The props for the {@link LivestreamLayout} component.
13
25
  */
14
26
  layoutProps?: LivestreamLayoutProps;
27
+ /**
28
+ * The props for the {@link BackstageLayout} component.
29
+ */
30
+ backstageProps?: BackstageLayoutProps;
31
+ /**
32
+ * Callback to handle errors while fetching or joining livestream.
33
+ */
34
+ onError?: (error: any) => void;
15
35
  };
16
36
  export declare const LivestreamPlayer: (props: LivestreamPlayerProps) => import("react/jsx-runtime").JSX.Element | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.17.1",
3
+ "version": "1.18.1",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/index.d.ts",
@@ -30,9 +30,9 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@floating-ui/react": "^0.27.6",
33
- "@stream-io/video-client": "1.23.1",
33
+ "@stream-io/video-client": "1.23.2",
34
34
  "@stream-io/video-filters-web": "0.2.1",
35
- "@stream-io/video-react-bindings": "1.6.4",
35
+ "@stream-io/video-react-bindings": "1.6.5",
36
36
  "chart.js": "^4.4.4",
37
37
  "clsx": "^2.0.0",
38
38
  "react-chartjs-2": "^5.3.0"
@@ -45,8 +45,8 @@
45
45
  "@rollup/plugin-json": "^6.1.0",
46
46
  "@rollup/plugin-replace": "^6.0.2",
47
47
  "@rollup/plugin-typescript": "^12.1.2",
48
- "@stream-io/audio-filters-web": "^0.4.0",
49
- "@stream-io/video-styling": "^1.1.5",
48
+ "@stream-io/audio-filters-web": "^0.4.1",
49
+ "@stream-io/video-styling": "^1.2.0",
50
50
  "@types/react": "^19.1.3",
51
51
  "@types/react-dom": "^19.1.3",
52
52
  "react": "19.0.0",
@@ -111,7 +111,7 @@ export const NoiseCancellationProvider = (
111
111
  noiseCancellation.isEnabled().then((e) => setIsEnabled(e));
112
112
  const unsubscribe = noiseCancellation.on('change', (v) => setIsEnabled(v));
113
113
  const init = (deinit.current || Promise.resolve())
114
- .then(() => noiseCancellation.init())
114
+ .then(() => noiseCancellation.init({ tracer: call.tracer }))
115
115
  .then(() => call.microphone.enableNoiseCancellation(noiseCancellation))
116
116
  .catch((e) => console.error(`Can't initialize noise cancellation`, e));
117
117
 
@@ -75,7 +75,7 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
75
75
 
76
76
  usePaginatedLayoutSortPreset(call);
77
77
 
78
- const Overlay = (
78
+ const overlay = (
79
79
  <ParticipantOverlay
80
80
  showParticipantCount={props.showParticipantCount}
81
81
  showDuration={props.showDuration}
@@ -85,7 +85,7 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
85
85
  );
86
86
 
87
87
  const { floatingParticipantProps, muted } = props;
88
- const FloatingParticipantOverlay = hasOngoingScreenShare && (
88
+ const floatingParticipantOverlay = hasOngoingScreenShare && (
89
89
  <ParticipantOverlay
90
90
  // these elements aren't needed for the video feed
91
91
  showParticipantCount={
@@ -104,7 +104,7 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
104
104
  <ParticipantView
105
105
  className="str-video__livestream-layout__screen-share"
106
106
  participant={presenter}
107
- ParticipantViewUI={Overlay}
107
+ ParticipantViewUI={overlay}
108
108
  trackType="screenShareTrack"
109
109
  muteAudio // audio is rendered by ParticipantsAudio
110
110
  />
@@ -121,7 +121,7 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
121
121
  ),
122
122
  )}
123
123
  participant={currentSpeaker}
124
- ParticipantViewUI={FloatingParticipantOverlay || Overlay}
124
+ ParticipantViewUI={floatingParticipantOverlay || overlay}
125
125
  mirror={
126
126
  props.mirrorLocalParticipantVideo !== false ? undefined : false
127
127
  }
@@ -132,6 +132,50 @@ export const LivestreamLayout = (props: LivestreamLayoutProps) => {
132
132
  );
133
133
  };
134
134
 
135
+ LivestreamLayout.displayName = 'LivestreamLayout';
136
+
137
+ /**
138
+ * The props for the {@link LivestreamLayout} component.
139
+ */
140
+ export type BackstageLayoutProps = {
141
+ /**
142
+ * Whether to show the counter for participants that joined before
143
+ * the livestream went live. Defaults to `true`.
144
+ */
145
+ showEarlyParticipantCount?: boolean;
146
+ };
147
+
148
+ export const BackstageLayout = (props: BackstageLayoutProps) => {
149
+ const { showEarlyParticipantCount = true } = props;
150
+ const { useParticipantCount, useCallStartsAt } = useCallStateHooks();
151
+ const participantCount = useParticipantCount();
152
+ const startsAt = useCallStartsAt();
153
+ const { t } = useI18n();
154
+
155
+ return (
156
+ <div className="str-video__livestream-layout__wrapper">
157
+ <div className="str-video__livestream-layout__backstage">
158
+ {startsAt && (
159
+ <span className="str-video__livestream-layout__starts-at">
160
+ {startsAt.getTime() < Date.now()
161
+ ? t('Livestream starts soon')
162
+ : t('Livestream starts at {{ startsAt }}', { startsAt })}
163
+ </span>
164
+ )}
165
+ {showEarlyParticipantCount && (
166
+ <span className="str-video__livestream-layout__early-viewers-count">
167
+ {t('{{ count }} participants joined early', {
168
+ count: participantCount,
169
+ })}
170
+ </span>
171
+ )}
172
+ </div>
173
+ </div>
174
+ );
175
+ };
176
+
177
+ BackstageLayout.displayName = 'BackstageLayout';
178
+
135
179
  const ParticipantOverlay = (props: {
136
180
  enableFullScreen?: boolean;
137
181
  showParticipantCount?: boolean;
@@ -189,8 +233,6 @@ const ParticipantOverlay = (props: {
189
233
  );
190
234
  };
191
235
 
192
- LivestreamLayout.displayName = 'LivestreamLayout';
193
-
194
236
  const useUpdateCallDuration = () => {
195
237
  const { useIsCallLive, useCallSession } = useCallStateHooks();
196
238
  const isCallLive = useIsCallLive();
@@ -0,0 +1,16 @@
1
+ import { useCallback, useLayoutEffect, useRef } from 'react';
2
+
3
+ export function useEffectEvent<P extends unknown[]>(
4
+ cb: ((...args: P) => void) | undefined,
5
+ ): (...args: P) => void {
6
+ const cbRef = useRef<((...args: P) => void) | undefined>(undefined);
7
+
8
+ useLayoutEffect(() => {
9
+ cbRef.current = cb;
10
+ }, [cb]);
11
+
12
+ return useCallback((...args: P) => {
13
+ const callback = cbRef.current;
14
+ callback?.(...args);
15
+ }, []);
16
+ }
@@ -43,122 +43,231 @@ export const usePersistedDevicePreferences = (
43
43
  ): void => {
44
44
  const {
45
45
  useCallSettings,
46
- useCameraState,
46
+ useCallCallingState,
47
47
  useMicrophoneState,
48
+ useCameraState,
48
49
  useSpeakerState,
49
50
  } = useCallStateHooks();
50
51
  const settings = useCallSettings();
52
+ const callingState = useCallCallingState();
51
53
 
52
- usePersistedDevicePreference(
53
- key,
54
- 'camera',
55
- useCameraState(),
56
- settings ? !settings.video.camera_default_on : undefined,
57
- );
58
-
59
- usePersistedDevicePreference(
60
- key,
61
- 'microphone',
62
- useMicrophoneState(),
63
- settings ? !settings.audio.mic_default_on : undefined,
64
- );
65
-
66
- usePersistedDevicePreference(key, 'speaker', useSpeakerState(), false);
67
- };
54
+ const microphoneState: DeviceState<'microphone'> = useMicrophoneState();
55
+ const cameraState: DeviceState<'camera'> = useCameraState();
56
+ const speakerState: DeviceState<'speaker'> = useSpeakerState();
68
57
 
69
- const usePersistedDevicePreference = <K extends DeviceKey>(
70
- key: string,
71
- deviceKey: K,
72
- state: DeviceState<K>,
73
- defaultMuted?: boolean,
74
- ): void => {
75
- const { useCallCallingState } = useCallStateHooks();
76
- const callingState = useCallCallingState();
77
58
  const [applyingState, setApplyingState] = useState<
78
59
  'idle' | 'applying' | 'applied'
79
60
  >('idle');
80
- const manager = state[deviceKey];
81
61
 
82
62
  useEffect(
83
63
  function apply() {
84
64
  if (
85
65
  callingState === CallingState.LEFT ||
86
- !state.devices?.length ||
87
- typeof defaultMuted !== 'boolean' ||
66
+ !microphoneState.devices.length ||
67
+ !cameraState.devices.length ||
68
+ !speakerState.devices ||
69
+ !settings ||
88
70
  applyingState !== 'idle'
89
71
  ) {
90
72
  return;
91
73
  }
92
74
 
93
- const preferences = parseLocalDevicePreferences(key);
94
- const preference = preferences[deviceKey];
95
-
96
75
  setApplyingState('applying');
97
76
 
98
- if (!manager.state.selectedDevice) {
99
- const applyPromise = preference
100
- ? applyLocalDevicePreference(
101
- manager,
102
- [preference].flat(),
103
- state.devices,
104
- )
105
- : applyMutedState(manager, defaultMuted);
106
-
107
- applyPromise
108
- .catch((err) => {
109
- console.warn(
110
- `Failed to apply ${deviceKey} device preferences`,
111
- err,
112
- );
113
- })
114
- .finally(() => setApplyingState('applied'));
115
- } else {
116
- setApplyingState('applied');
117
- }
77
+ (async () => {
78
+ for (const [deviceKey, state, defaultMuted] of [
79
+ ['microphone', microphoneState, !settings.audio.mic_default_on],
80
+ ['camera', cameraState, !settings.video.camera_default_on],
81
+ ['speaker', speakerState, false],
82
+ ] as const) {
83
+ const preferences = parseLocalDevicePreferences(key);
84
+ const preference = preferences[deviceKey];
85
+ const manager = (
86
+ state as DeviceState<'camera' | 'microphone' | 'speaker'>
87
+ )[deviceKey];
88
+
89
+ if (!manager.state.selectedDevice) {
90
+ const applyPromise = preference
91
+ ? applyLocalDevicePreference(
92
+ manager,
93
+ [preference].flat(),
94
+ state.devices,
95
+ )
96
+ : applyMutedState(manager, defaultMuted);
97
+
98
+ await applyPromise.catch((err) => {
99
+ console.warn(
100
+ `Failed to apply ${deviceKey} device preferences`,
101
+ err,
102
+ );
103
+ });
104
+ }
105
+ }
106
+ })().finally(() =>
107
+ setApplyingState((state) => (state === 'applying' ? 'applied' : state)),
108
+ );
118
109
  },
119
110
  [
120
111
  applyingState,
121
112
  callingState,
122
- defaultMuted,
123
- deviceKey,
113
+ cameraState,
114
+ cameraState.devices,
124
115
  key,
125
- manager,
126
- state.devices,
116
+ microphoneState,
117
+ microphoneState.devices,
118
+ settings,
119
+ speakerState,
120
+ speakerState.devices,
127
121
  ],
128
122
  );
129
123
 
130
124
  useEffect(
131
125
  function persist() {
132
- if (
133
- callingState === CallingState.LEFT ||
134
- !state.devices?.length ||
135
- applyingState !== 'applied'
136
- ) {
126
+ if (callingState === CallingState.LEFT || applyingState !== 'applied') {
137
127
  return;
138
128
  }
139
129
 
140
- try {
141
- patchLocalDevicePreference(key, deviceKey, {
142
- devices: state.devices,
143
- selectedDevice: state.selectedDevice,
144
- isMute: state.isMute,
145
- });
146
- } catch (err) {
147
- console.warn(`Failed to save ${deviceKey} device preferences`, err);
130
+ for (const [deviceKey, devices, selectedDevice, isMute] of [
131
+ [
132
+ 'camera',
133
+ cameraState.devices,
134
+ cameraState.selectedDevice,
135
+ cameraState.isMute,
136
+ ],
137
+ [
138
+ 'microphone',
139
+ microphoneState.devices,
140
+ microphoneState.selectedDevice,
141
+ microphoneState.isMute,
142
+ ],
143
+ [
144
+ 'speaker',
145
+ speakerState.devices,
146
+ speakerState.selectedDevice,
147
+ speakerState.isMute,
148
+ ],
149
+ ] as const) {
150
+ try {
151
+ patchLocalDevicePreference(key, deviceKey, {
152
+ devices,
153
+ selectedDevice,
154
+ isMute,
155
+ });
156
+ } catch (err) {
157
+ console.warn(`Failed to save ${deviceKey} device preferences`, err);
158
+ }
148
159
  }
149
160
  },
150
161
  [
151
162
  applyingState,
152
163
  callingState,
153
- deviceKey,
164
+ cameraState.devices,
165
+ cameraState.isMute,
166
+ cameraState.selectedDevice,
154
167
  key,
155
- state.devices,
156
- state.isMute,
157
- state.selectedDevice,
168
+ microphoneState.devices,
169
+ microphoneState.isMute,
170
+ microphoneState.selectedDevice,
171
+ speakerState.devices,
172
+ speakerState.isMute,
173
+ speakerState.selectedDevice,
158
174
  ],
159
175
  );
160
176
  };
161
177
 
178
+ // const usePersistedDevicePreference = <K extends DeviceKey>(
179
+ // key: string,
180
+ // deviceKey: K,
181
+ // state: DeviceState<K>,
182
+ // defaultMuted?: boolean,
183
+ // ): void => {
184
+ // const { useCallCallingState } = useCallStateHooks();
185
+ // const callingState = useCallCallingState();
186
+ // const [applyingState, setApplyingState] = useState<
187
+ // 'idle' | 'applying' | 'applied'
188
+ // >('idle');
189
+ // const manager = state[deviceKey];
190
+
191
+ // useEffect(
192
+ // function apply() {
193
+ // if (
194
+ // callingState === CallingState.LEFT ||
195
+ // !state.devices?.length ||
196
+ // typeof defaultMuted !== 'boolean' ||
197
+ // applyingState !== 'idle'
198
+ // ) {
199
+ // return;
200
+ // }
201
+
202
+ // const preferences = parseLocalDevicePreferences(key);
203
+ // const preference = preferences[deviceKey];
204
+
205
+ // setApplyingState('applying');
206
+
207
+ // if (!manager.state.selectedDevice) {
208
+ // const applyPromise = preference
209
+ // ? applyLocalDevicePreference(
210
+ // manager,
211
+ // [preference].flat(),
212
+ // state.devices,
213
+ // )
214
+ // : applyMutedState(manager, defaultMuted);
215
+
216
+ // applyPromise
217
+ // .catch((err) => {
218
+ // console.warn(
219
+ // `Failed to apply ${deviceKey} device preferences`,
220
+ // err,
221
+ // );
222
+ // })
223
+ // .finally(() => setApplyingState('applied'));
224
+ // } else {
225
+ // setApplyingState('applied');
226
+ // }
227
+ // },
228
+ // [
229
+ // applyingState,
230
+ // callingState,
231
+ // defaultMuted,
232
+ // deviceKey,
233
+ // key,
234
+ // manager,
235
+ // state.devices,
236
+ // ],
237
+ // );
238
+
239
+ // useEffect(
240
+ // function persist() {
241
+ // if (
242
+ // callingState === CallingState.LEFT ||
243
+ // !state.devices?.length ||
244
+ // applyingState !== 'applied'
245
+ // ) {
246
+ // return;
247
+ // }
248
+
249
+ // try {
250
+ // patchLocalDevicePreference(key, deviceKey, {
251
+ // devices: state.devices,
252
+ // selectedDevice: state.selectedDevice,
253
+ // isMute: state.isMute,
254
+ // });
255
+ // } catch (err) {
256
+ // console.warn(`Failed to save ${deviceKey} device preferences`, err);
257
+ // }
258
+ // },
259
+ // [
260
+ // applyingState,
261
+ // callingState,
262
+ // deviceKey,
263
+ // key,
264
+ // state.devices,
265
+ // state.isMute,
266
+ // state.selectedDevice,
267
+ // ],
268
+ // );
269
+ // };
270
+
162
271
  const parseLocalDevicePreferences = (key: string): LocalDevicePreferences => {
163
272
  const preferencesStr = window.localStorage.getItem(key);
164
273
  let preferences: LocalDevicePreferences = {};
@@ -13,6 +13,10 @@
13
13
  "You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
14
14
 
15
15
  "Live": "Live",
16
+ "Livestream starts soon": "Livestream starts soon",
17
+ "Livestream starts at {{ startsAt }}": "Livestream starts at {{ startsAt, datetime }}",
18
+ "{{ count }} participants joined early_one": "{{ count }} participant joined early",
19
+ "{{ count }} participants joined early_other": "{{ count }} participants joined early",
16
20
 
17
21
  "You can now speak.": "You can now speak.",
18
22
  "Awaiting for an approval to speak.": "Awaiting for an approval to speak.",