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

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.
package/dist/index.cjs.js CHANGED
@@ -80,75 +80,189 @@ const defaultDevice = 'default';
80
80
  * @param key the key to use for local storage.
81
81
  */
82
82
  const usePersistedDevicePreferences = (key = '@stream-io/device-preferences') => {
83
- const { useCallSettings, useCameraState, useMicrophoneState, useSpeakerState, } = videoReactBindings.useCallStateHooks();
83
+ const { useCallSettings, useCallCallingState, useMicrophoneState, useCameraState, useSpeakerState, } = videoReactBindings.useCallStateHooks();
84
84
  const settings = useCallSettings();
85
- usePersistedDevicePreference(key, 'camera', useCameraState(), settings ? !settings.video.camera_default_on : undefined);
86
- usePersistedDevicePreference(key, 'microphone', useMicrophoneState(), settings ? !settings.audio.mic_default_on : undefined);
87
- usePersistedDevicePreference(key, 'speaker', useSpeakerState(), false);
88
- };
89
- const usePersistedDevicePreference = (key, deviceKey, state, defaultMuted) => {
90
- const { useCallCallingState } = videoReactBindings.useCallStateHooks();
91
85
  const callingState = useCallCallingState();
86
+ const microphoneState = useMicrophoneState();
87
+ const cameraState = useCameraState();
88
+ const speakerState = useSpeakerState();
92
89
  const [applyingState, setApplyingState] = react.useState('idle');
93
- const manager = state[deviceKey];
94
90
  react.useEffect(function apply() {
95
91
  if (callingState === videoClient.CallingState.LEFT ||
96
- !state.devices?.length ||
97
- typeof defaultMuted !== 'boolean' ||
92
+ !microphoneState.devices.length ||
93
+ !cameraState.devices.length ||
94
+ !speakerState.devices ||
95
+ !settings ||
98
96
  applyingState !== 'idle') {
99
97
  return;
100
98
  }
101
- const preferences = parseLocalDevicePreferences(key);
102
- const preference = preferences[deviceKey];
103
99
  setApplyingState('applying');
104
- if (!manager.state.selectedDevice) {
105
- const applyPromise = preference
106
- ? applyLocalDevicePreference(manager, [preference].flat(), state.devices)
107
- : applyMutedState(manager, defaultMuted);
108
- applyPromise
109
- .catch((err) => {
110
- console.warn(`Failed to apply ${deviceKey} device preferences`, err);
111
- })
112
- .finally(() => setApplyingState('applied'));
113
- }
114
- else {
115
- setApplyingState('applied');
116
- }
100
+ (async () => {
101
+ for (const [deviceKey, state, defaultMuted] of [
102
+ ['microphone', microphoneState, !settings.audio.mic_default_on],
103
+ ['camera', cameraState, !settings.video.camera_default_on],
104
+ ['speaker', speakerState, false],
105
+ ]) {
106
+ const preferences = parseLocalDevicePreferences(key);
107
+ const preference = preferences[deviceKey];
108
+ const manager = state[deviceKey];
109
+ if (!manager.state.selectedDevice) {
110
+ const applyPromise = preference
111
+ ? applyLocalDevicePreference(manager, [preference].flat(), state.devices)
112
+ : applyMutedState(manager, defaultMuted);
113
+ await applyPromise.catch((err) => {
114
+ console.warn(`Failed to apply ${deviceKey} device preferences`, err);
115
+ });
116
+ }
117
+ }
118
+ })().finally(() => setApplyingState((state) => (state === 'applying' ? 'applied' : state)));
117
119
  }, [
118
120
  applyingState,
119
121
  callingState,
120
- defaultMuted,
121
- deviceKey,
122
+ cameraState,
123
+ cameraState.devices,
122
124
  key,
123
- manager,
124
- state.devices,
125
+ microphoneState,
126
+ microphoneState.devices,
127
+ settings,
128
+ speakerState,
129
+ speakerState.devices,
125
130
  ]);
126
131
  react.useEffect(function persist() {
127
- if (callingState === videoClient.CallingState.LEFT ||
128
- !state.devices?.length ||
129
- applyingState !== 'applied') {
132
+ if (callingState === videoClient.CallingState.LEFT || applyingState !== 'applied') {
130
133
  return;
131
134
  }
132
- try {
133
- patchLocalDevicePreference(key, deviceKey, {
134
- devices: state.devices,
135
- selectedDevice: state.selectedDevice,
136
- isMute: state.isMute,
137
- });
138
- }
139
- catch (err) {
140
- console.warn(`Failed to save ${deviceKey} device preferences`, err);
135
+ for (const [deviceKey, devices, selectedDevice, isMute] of [
136
+ [
137
+ 'camera',
138
+ cameraState.devices,
139
+ cameraState.selectedDevice,
140
+ cameraState.isMute,
141
+ ],
142
+ [
143
+ 'microphone',
144
+ microphoneState.devices,
145
+ microphoneState.selectedDevice,
146
+ microphoneState.isMute,
147
+ ],
148
+ [
149
+ 'speaker',
150
+ speakerState.devices,
151
+ speakerState.selectedDevice,
152
+ speakerState.isMute,
153
+ ],
154
+ ]) {
155
+ try {
156
+ patchLocalDevicePreference(key, deviceKey, {
157
+ devices,
158
+ selectedDevice,
159
+ isMute,
160
+ });
161
+ }
162
+ catch (err) {
163
+ console.warn(`Failed to save ${deviceKey} device preferences`, err);
164
+ }
141
165
  }
142
166
  }, [
143
167
  applyingState,
144
168
  callingState,
145
- deviceKey,
169
+ cameraState.devices,
170
+ cameraState.isMute,
171
+ cameraState.selectedDevice,
146
172
  key,
147
- state.devices,
148
- state.isMute,
149
- state.selectedDevice,
173
+ microphoneState.devices,
174
+ microphoneState.isMute,
175
+ microphoneState.selectedDevice,
176
+ speakerState.devices,
177
+ speakerState.isMute,
178
+ speakerState.selectedDevice,
150
179
  ]);
151
180
  };
181
+ // const usePersistedDevicePreference = <K extends DeviceKey>(
182
+ // key: string,
183
+ // deviceKey: K,
184
+ // state: DeviceState<K>,
185
+ // defaultMuted?: boolean,
186
+ // ): void => {
187
+ // const { useCallCallingState } = useCallStateHooks();
188
+ // const callingState = useCallCallingState();
189
+ // const [applyingState, setApplyingState] = useState<
190
+ // 'idle' | 'applying' | 'applied'
191
+ // >('idle');
192
+ // const manager = state[deviceKey];
193
+ // useEffect(
194
+ // function apply() {
195
+ // if (
196
+ // callingState === CallingState.LEFT ||
197
+ // !state.devices?.length ||
198
+ // typeof defaultMuted !== 'boolean' ||
199
+ // applyingState !== 'idle'
200
+ // ) {
201
+ // return;
202
+ // }
203
+ // const preferences = parseLocalDevicePreferences(key);
204
+ // const preference = preferences[deviceKey];
205
+ // setApplyingState('applying');
206
+ // if (!manager.state.selectedDevice) {
207
+ // const applyPromise = preference
208
+ // ? applyLocalDevicePreference(
209
+ // manager,
210
+ // [preference].flat(),
211
+ // state.devices,
212
+ // )
213
+ // : applyMutedState(manager, defaultMuted);
214
+ // applyPromise
215
+ // .catch((err) => {
216
+ // console.warn(
217
+ // `Failed to apply ${deviceKey} device preferences`,
218
+ // err,
219
+ // );
220
+ // })
221
+ // .finally(() => setApplyingState('applied'));
222
+ // } else {
223
+ // setApplyingState('applied');
224
+ // }
225
+ // },
226
+ // [
227
+ // applyingState,
228
+ // callingState,
229
+ // defaultMuted,
230
+ // deviceKey,
231
+ // key,
232
+ // manager,
233
+ // state.devices,
234
+ // ],
235
+ // );
236
+ // useEffect(
237
+ // function persist() {
238
+ // if (
239
+ // callingState === CallingState.LEFT ||
240
+ // !state.devices?.length ||
241
+ // applyingState !== 'applied'
242
+ // ) {
243
+ // return;
244
+ // }
245
+ // try {
246
+ // patchLocalDevicePreference(key, deviceKey, {
247
+ // devices: state.devices,
248
+ // selectedDevice: state.selectedDevice,
249
+ // isMute: state.isMute,
250
+ // });
251
+ // } catch (err) {
252
+ // console.warn(`Failed to save ${deviceKey} device preferences`, err);
253
+ // }
254
+ // },
255
+ // [
256
+ // applyingState,
257
+ // callingState,
258
+ // deviceKey,
259
+ // key,
260
+ // state.devices,
261
+ // state.isMute,
262
+ // state.selectedDevice,
263
+ // ],
264
+ // );
265
+ // };
152
266
  const parseLocalDevicePreferences = (key) => {
153
267
  const preferencesStr = window.localStorage.getItem(key);
154
268
  let preferences = {};
@@ -2228,6 +2342,10 @@ var en = {
2228
2342
  Video: Video,
2229
2343
  "You are muted. Unmute to speak.": "You are muted. Unmute to speak.",
2230
2344
  Live: Live,
2345
+ "Livestream starts soon": "Livestream starts soon",
2346
+ "Livestream starts at {{ startsAt }}": "Livestream starts at {{ startsAt, datetime }}",
2347
+ "{{ count }} participants joined early_one": "{{ count }} participant joined early",
2348
+ "{{ count }} participants joined early_other": "{{ count }} participants joined early",
2231
2349
  "You can now speak.": "You can now speak.",
2232
2350
  "Awaiting for an approval to speak.": "Awaiting for an approval to speak.",
2233
2351
  "You can no longer speak.": "You can no longer speak.",
@@ -2445,18 +2563,32 @@ const LivestreamLayout = (props) => {
2445
2563
  ? participants.find(videoClient.hasScreenShare)
2446
2564
  : undefined;
2447
2565
  usePaginatedLayoutSortPreset(call);
2448
- const Overlay = (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showSpeakerName: props.showSpeakerName }));
2566
+ const overlay = (jsxRuntime.jsx(ParticipantOverlay, { showParticipantCount: props.showParticipantCount, showDuration: props.showDuration, showLiveBadge: props.showLiveBadge, showSpeakerName: props.showSpeakerName }));
2449
2567
  const { floatingParticipantProps, muted } = props;
2450
- const FloatingParticipantOverlay = hasOngoingScreenShare && (jsxRuntime.jsx(ParticipantOverlay
2568
+ const floatingParticipantOverlay = hasOngoingScreenShare && (jsxRuntime.jsx(ParticipantOverlay
2451
2569
  // these elements aren't needed for the video feed
2452
2570
  , {
2453
2571
  // these elements aren't needed for the video feed
2454
2572
  showParticipantCount: floatingParticipantProps?.showParticipantCount ?? false, showDuration: floatingParticipantProps?.showDuration ?? false, showLiveBadge: floatingParticipantProps?.showLiveBadge ?? false, showSpeakerName: floatingParticipantProps?.showSpeakerName ?? true }));
2455
- return (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__wrapper", children: [!muted && jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), hasOngoingScreenShare && presenter && (jsxRuntime.jsx(ParticipantView, { className: "str-video__livestream-layout__screen-share", participant: presenter, ParticipantViewUI: Overlay, trackType: "screenShareTrack", muteAudio // audio is rendered by ParticipantsAudio
2573
+ return (jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__wrapper", children: [!muted && jsxRuntime.jsx(ParticipantsAudio, { participants: remoteParticipants }), hasOngoingScreenShare && presenter && (jsxRuntime.jsx(ParticipantView, { className: "str-video__livestream-layout__screen-share", participant: presenter, ParticipantViewUI: overlay, trackType: "screenShareTrack", muteAudio // audio is rendered by ParticipantsAudio
2456
2574
  : true })), currentSpeaker && (jsxRuntime.jsx(ParticipantView, { className: clsx(hasOngoingScreenShare &&
2457
- clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: FloatingParticipantOverlay || Overlay, mirror: props.mirrorLocalParticipantVideo !== false ? undefined : false, muteAudio // audio is rendered by ParticipantsAudio
2575
+ clsx('str-video__livestream-layout__floating-participant', `str-video__livestream-layout__floating-participant--${floatingParticipantProps?.position ?? 'top-right'}`)), participant: currentSpeaker, ParticipantViewUI: floatingParticipantOverlay || overlay, mirror: props.mirrorLocalParticipantVideo !== false ? undefined : false, muteAudio // audio is rendered by ParticipantsAudio
2458
2576
  : true }))] }));
2459
2577
  };
2578
+ LivestreamLayout.displayName = 'LivestreamLayout';
2579
+ const BackstageLayout = (props) => {
2580
+ const { showEarlyParticipantCount = true } = props;
2581
+ const { useParticipantCount, useCallStartsAt } = videoReactBindings.useCallStateHooks();
2582
+ const participantCount = useParticipantCount();
2583
+ const startsAt = useCallStartsAt();
2584
+ const { t } = videoReactBindings.useI18n();
2585
+ return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__wrapper", children: jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__backstage", children: [startsAt && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__starts-at", children: startsAt.getTime() < Date.now()
2586
+ ? t('Livestream starts soon')
2587
+ : t('Livestream starts at {{ startsAt }}', { startsAt }) })), showEarlyParticipantCount && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__early-viewers-count", children: t('{{ count }} participants joined early', {
2588
+ count: participantCount,
2589
+ }) }))] }) }));
2590
+ };
2591
+ BackstageLayout.displayName = 'BackstageLayout';
2460
2592
  const ParticipantOverlay = (props) => {
2461
2593
  const { enableFullScreen = true, showParticipantCount = true, showDuration = true, showLiveBadge = true, showSpeakerName = false, } = props;
2462
2594
  const { participant } = useParticipantViewContext();
@@ -2467,7 +2599,6 @@ const ParticipantOverlay = (props) => {
2467
2599
  const { t } = videoReactBindings.useI18n();
2468
2600
  return (jsxRuntime.jsx("div", { className: "str-video__livestream-layout__overlay", children: jsxRuntime.jsxs("div", { className: "str-video__livestream-layout__overlay__bar", children: [showLiveBadge && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__live-badge", children: t('Live') })), showParticipantCount && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__viewers-count", children: participantCount })), showSpeakerName && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__speaker-name", title: participant.name || participant.userId || '', children: participant.name || participant.userId || '' })), showDuration && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__duration", children: formatDuration(duration) })), enableFullScreen && (jsxRuntime.jsx("span", { className: "str-video__livestream-layout__go-fullscreen", onClick: toggleFullScreen }))] }) }));
2469
2601
  };
2470
- LivestreamLayout.displayName = 'LivestreamLayout';
2471
2602
  const useUpdateCallDuration = () => {
2472
2603
  const { useIsCallLive, useCallSession } = videoReactBindings.useCallStateHooks();
2473
2604
  const isCallLive = useIsCallLive();
@@ -2709,17 +2840,30 @@ const Host = () => {
2709
2840
  Host.displayName = 'PipLayout.Host';
2710
2841
  const PipLayout = { Pip, Host };
2711
2842
 
2843
+ function useEffectEvent(cb) {
2844
+ const cbRef = react.useRef(undefined);
2845
+ react.useLayoutEffect(() => {
2846
+ cbRef.current = cb;
2847
+ }, [cb]);
2848
+ return react.useCallback((...args) => {
2849
+ const callback = cbRef.current;
2850
+ callback?.(...args);
2851
+ }, []);
2852
+ }
2853
+
2712
2854
  const LivestreamPlayer = (props) => {
2713
- const { callType, callId, layoutProps } = props;
2855
+ const { callType, callId, ...restProps } = props;
2714
2856
  const client = videoReactBindings.useStreamVideoClient();
2715
2857
  const [call, setCall] = react.useState();
2858
+ const onError = useEffectEvent(props.onError);
2716
2859
  react.useEffect(() => {
2717
2860
  if (!client)
2718
2861
  return;
2719
2862
  const myCall = client.call(callType, callId);
2720
2863
  setCall(myCall);
2721
- myCall.join().catch((e) => {
2722
- console.error('Failed to join call', e);
2864
+ myCall.get().catch((e) => {
2865
+ console.error('Failed to fetch call', e);
2866
+ onError(e);
2723
2867
  });
2724
2868
  return () => {
2725
2869
  myCall.leave().catch((e) => {
@@ -2727,13 +2871,67 @@ const LivestreamPlayer = (props) => {
2727
2871
  });
2728
2872
  setCall(undefined);
2729
2873
  };
2730
- }, [callId, callType, client]);
2874
+ }, [callId, callType, client, onError]);
2875
+ if (!call) {
2876
+ return null;
2877
+ }
2878
+ return (jsxRuntime.jsx(StreamCall, { call: call, children: jsxRuntime.jsx(LivestreamCall, { ...restProps }) }));
2879
+ };
2880
+ const LivestreamCall = (props) => {
2881
+ const call = useLivestreamCall(props);
2882
+ const { useIsCallLive } = videoReactBindings.useCallStateHooks();
2883
+ const isLive = useIsCallLive();
2731
2884
  if (!call)
2732
2885
  return null;
2733
- return (jsxRuntime.jsx(StreamCall, { call: call, children: jsxRuntime.jsx(LivestreamLayout, { ...layoutProps }) }));
2886
+ if (isLive) {
2887
+ return jsxRuntime.jsx(LivestreamLayout, { ...props.layoutProps });
2888
+ }
2889
+ return jsxRuntime.jsx(BackstageLayout, { ...props.backstageProps });
2890
+ };
2891
+ const useLivestreamCall = (props) => {
2892
+ const call = videoReactBindings.useCall();
2893
+ const { useIsCallLive, useOwnCapabilities } = videoReactBindings.useCallStateHooks();
2894
+ const canJoinLive = useIsCallLive();
2895
+ const canJoinEarly = useCanJoinEearly();
2896
+ const canJoinBackstage = useOwnCapabilities()?.includes('join-backstage') ?? false;
2897
+ const canJoinAsap = canJoinLive || canJoinEarly || canJoinBackstage;
2898
+ const joinBehavior = props.joinBehavior ?? 'asap';
2899
+ const canJoin = (joinBehavior === 'asap' && canJoinAsap) ||
2900
+ (joinBehavior === 'live' && canJoinLive);
2901
+ const onError = useEffectEvent(props.onError);
2902
+ react.useEffect(() => {
2903
+ if (call && call.state.callingState === videoClient.CallingState.IDLE && canJoin) {
2904
+ call.join().catch((e) => {
2905
+ console.error('Failed to join call', e);
2906
+ onError(e);
2907
+ });
2908
+ }
2909
+ }, [call, canJoin, onError]);
2910
+ return call;
2911
+ };
2912
+ const useCanJoinEearly = () => {
2913
+ const { useCallStartsAt, useCallSettings } = videoReactBindings.useCallStateHooks();
2914
+ const startsAt = useCallStartsAt();
2915
+ const settings = useCallSettings();
2916
+ const joinAheadTimeSeconds = settings?.backstage.join_ahead_time_seconds;
2917
+ const [canJoinEarly, setCanJoinEearly] = react.useState(() => checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
2918
+ react.useEffect(() => {
2919
+ if (!canJoinEarly) {
2920
+ const handle = setInterval(() => {
2921
+ setCanJoinEearly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
2922
+ }, 1000);
2923
+ return () => clearInterval(handle);
2924
+ }
2925
+ }, [canJoinEarly, startsAt, joinAheadTimeSeconds]);
2926
+ };
2927
+ const checkCanJoinEarly = (startsAt, joinAheadTimeSeconds) => {
2928
+ if (!startsAt) {
2929
+ return false;
2930
+ }
2931
+ return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
2734
2932
  };
2735
2933
 
2736
- const [major, minor, patch] = ("1.17.1").split('.');
2934
+ const [major, minor, patch] = ("1.18.0").split('.');
2737
2935
  videoClient.setSdkInfo({
2738
2936
  type: videoClient.SfuModels.SdkType.REACT,
2739
2937
  major,
@@ -2746,6 +2944,7 @@ exports.Audio = Audio;
2746
2944
  exports.Avatar = Avatar;
2747
2945
  exports.AvatarFallback = AvatarFallback;
2748
2946
  exports.BackgroundFiltersProvider = BackgroundFiltersProvider;
2947
+ exports.BackstageLayout = BackstageLayout;
2749
2948
  exports.BaseVideo = BaseVideo;
2750
2949
  exports.CallControls = CallControls;
2751
2950
  exports.CallParticipantListing = CallParticipantListing;