@stream-io/video-react-sdk 1.43.4 → 1.44.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.
@@ -0,0 +1,15 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { Placement } from '@floating-ui/react';
3
+ export type CameraUnavailableNotificationProps = {
4
+ placement?: Placement;
5
+ className?: string;
6
+ };
7
+ /**
8
+ * Shows when the last attempt to start the camera failed (e.g. the camera is
9
+ * in use by another app). Reads the error from the camera state, so it shows
10
+ * failures that happened before it was mounted.
11
+ *
12
+ * For error-specific copy, build your own notification on top of
13
+ * `useCameraState().error`.
14
+ */
15
+ export declare const CameraUnavailableNotification: ({ children, placement, className, }: PropsWithChildren<CameraUnavailableNotificationProps>) => import("react").JSX.Element;
@@ -3,3 +3,4 @@ export * from './PermissionNotification';
3
3
  export * from './SpeakingWhileMutedNotification';
4
4
  export * from './RecordingInProgressNotification';
5
5
  export * from './MicCaptureErrorNotification';
6
+ export * from './CameraUnavailableNotification';
@@ -62,6 +62,8 @@ export declare const translations: {
62
62
  "Camera on": string;
63
63
  "Camera off": string;
64
64
  "Camera is paused by your system": string;
65
+ "Camera unavailable": string;
66
+ "Close other apps that might be using your camera and try again.": string;
65
67
  "No camera found": string;
66
68
  "Video is disabled": string;
67
69
  Pinned: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.43.4",
3
+ "version": "1.44.0",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/esm/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -49,9 +49,9 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@floating-ui/react": "^0.27.20",
52
- "@stream-io/video-client": "1.61.2",
52
+ "@stream-io/video-client": "1.62.0",
53
53
  "@stream-io/video-filters-web": "0.10.0",
54
- "@stream-io/video-react-bindings": "1.20.6",
54
+ "@stream-io/video-react-bindings": "1.21.0",
55
55
  "chart.js": "^4.5.1",
56
56
  "clsx": "^2.1.1",
57
57
  "react-chartjs-2": "^5.3.1"
@@ -66,7 +66,7 @@
66
66
  "@rollup/plugin-typescript": "^12.3.0",
67
67
  "@stream-io/audio-filters-web": "^0.10.4",
68
68
  "@stream-io/typescript-config": "^0.0.0",
69
- "@stream-io/video-styling": "^1.15.1",
69
+ "@stream-io/video-styling": "^1.16.0",
70
70
  "@types/node": "^25.9.5",
71
71
  "@types/react": "~19.2.18",
72
72
  "@types/react-dom": "~19.2.4",
@@ -12,7 +12,10 @@ import {
12
12
  } from '@stream-io/video-client';
13
13
  import { CompositeButton, IconButtonWithMenuProps } from '../Button/';
14
14
  import { DeviceSelectorVideo } from '../DeviceSettings';
15
- import { PermissionNotification } from '../Notification';
15
+ import {
16
+ CameraUnavailableNotification,
17
+ PermissionNotification,
18
+ } from '../Notification';
16
19
  import { useRequestPermission } from '../../hooks';
17
20
  import { Icon } from '../Icon';
18
21
  import { WithTooltip } from '../Tooltip';
@@ -58,64 +61,66 @@ export const ToggleVideoPreviewButton = (
58
61
  const handleClick = createCallControlHandler(props, () => camera.toggle());
59
62
 
60
63
  return (
61
- <WithTooltip
62
- title={
63
- !hasBrowserPermission
64
- ? t('Check your browser video permissions')
65
- : isSystemMuted
66
- ? t('Camera is paused by your system')
67
- : (caption ?? t('Video'))
68
- }
69
- tooltipDisabled={tooltipDisabled}
70
- >
71
- <CompositeButton
72
- active={optionsAwareIsMute}
73
- caption={caption}
74
- className={clsx(
75
- !hasBrowserPermission && 'str-video__device-unavailable',
76
- )}
77
- variant="secondary"
78
- data-testid={
79
- optionsAwareIsMute
80
- ? 'preview-video-unmute-button'
81
- : 'preview-video-mute-button'
64
+ <CameraUnavailableNotification>
65
+ <WithTooltip
66
+ title={
67
+ !hasBrowserPermission
68
+ ? t('Check your browser video permissions')
69
+ : isSystemMuted
70
+ ? t('Camera is paused by your system')
71
+ : (caption ?? t('Video'))
82
72
  }
83
- onClick={handleClick}
84
- disabled={
85
- !hasBrowserPermission || (!optimisticUpdates && isTogglePending)
86
- }
87
- Menu={Menu}
88
- menuPlacement={menuPlacement}
89
- {...restCompositeButtonProps}
90
- onMenuToggle={(shown) => {
91
- setTooltipDisabled(shown);
92
- onMenuToggle?.(shown);
93
- }}
73
+ tooltipDisabled={tooltipDisabled}
94
74
  >
95
- <Icon icon={!optionsAwareIsMute ? 'camera' : 'camera-off'} />
96
- {!hasBrowserPermission && (
97
- <span
98
- className="str-video__no-media-permission"
99
- title={t('Check your browser video permissions')}
100
- children="!"
101
- />
102
- )}
103
- {isPromptingPermission && (
104
- <span
105
- className="str-video__pending-permission"
106
- title={t('Waiting for permission')}
107
- children="?"
108
- />
109
- )}
110
- {isSystemMuted && hasBrowserPermission && (
111
- <span
112
- className="str-video__system-muted"
113
- title={t('Camera is paused by your system')}
114
- children="!"
115
- />
116
- )}
117
- </CompositeButton>
118
- </WithTooltip>
75
+ <CompositeButton
76
+ active={optionsAwareIsMute}
77
+ caption={caption}
78
+ className={clsx(
79
+ !hasBrowserPermission && 'str-video__device-unavailable',
80
+ )}
81
+ variant="secondary"
82
+ data-testid={
83
+ optionsAwareIsMute
84
+ ? 'preview-video-unmute-button'
85
+ : 'preview-video-mute-button'
86
+ }
87
+ onClick={handleClick}
88
+ disabled={
89
+ !hasBrowserPermission || (!optimisticUpdates && isTogglePending)
90
+ }
91
+ Menu={Menu}
92
+ menuPlacement={menuPlacement}
93
+ {...restCompositeButtonProps}
94
+ onMenuToggle={(shown) => {
95
+ setTooltipDisabled(shown);
96
+ onMenuToggle?.(shown);
97
+ }}
98
+ >
99
+ <Icon icon={!optionsAwareIsMute ? 'camera' : 'camera-off'} />
100
+ {!hasBrowserPermission && (
101
+ <span
102
+ className="str-video__no-media-permission"
103
+ title={t('Check your browser video permissions')}
104
+ children="!"
105
+ />
106
+ )}
107
+ {isPromptingPermission && (
108
+ <span
109
+ className="str-video__pending-permission"
110
+ title={t('Waiting for permission')}
111
+ children="?"
112
+ />
113
+ )}
114
+ {isSystemMuted && hasBrowserPermission && (
115
+ <span
116
+ className="str-video__system-muted"
117
+ title={t('Camera is paused by your system')}
118
+ children="!"
119
+ />
120
+ )}
121
+ </CompositeButton>
122
+ </WithTooltip>
123
+ </CameraUnavailableNotification>
119
124
  );
120
125
  };
121
126
 
@@ -178,70 +183,72 @@ export const ToggleVideoPublishingButton = (
178
183
  )}
179
184
  messageRevoked={t('You can no longer share your video.')}
180
185
  >
181
- <WithTooltip
182
- title={
183
- !hasPermission
184
- ? t('You have no permission to share your video')
185
- : !hasBrowserPermission
186
- ? t('Check your browser video permissions')
187
- : !isPublishingVideoAllowed
188
- ? t('Video publishing is disabled by the system')
189
- : isSystemMuted
190
- ? t('Camera is paused by your system')
191
- : caption || t('Video')
192
- }
193
- tooltipDisabled={tooltipDisabled}
194
- >
195
- <CompositeButton
196
- active={optionsAwareIsMute}
197
- caption={caption}
198
- variant="secondary"
199
- disabled={
200
- !hasBrowserPermission ||
201
- !hasPermission ||
202
- !isPublishingVideoAllowed ||
203
- (!optimisticUpdates && isTogglePending)
204
- }
205
- data-testid={
206
- optionsAwareIsMute ? 'video-unmute-button' : 'video-mute-button'
186
+ <CameraUnavailableNotification>
187
+ <WithTooltip
188
+ title={
189
+ !hasPermission
190
+ ? t('You have no permission to share your video')
191
+ : !hasBrowserPermission
192
+ ? t('Check your browser video permissions')
193
+ : !isPublishingVideoAllowed
194
+ ? t('Video publishing is disabled by the system')
195
+ : isSystemMuted
196
+ ? t('Camera is paused by your system')
197
+ : caption || t('Video')
207
198
  }
208
- onClick={handleClick}
209
- Menu={Menu}
210
- menuPlacement={menuPlacement}
211
- menuOffset={16}
212
- {...restCompositeButtonProps}
213
- onMenuToggle={(shown) => {
214
- setTooltipDisabled(shown);
215
- onMenuToggle?.(shown);
216
- }}
199
+ tooltipDisabled={tooltipDisabled}
217
200
  >
218
- <Icon icon={optionsAwareIsMute ? 'camera-off' : 'camera'} />
219
- {(!hasBrowserPermission ||
220
- !hasPermission ||
221
- !isPublishingVideoAllowed) && (
222
- <span className="str-video__no-media-permission">!</span>
223
- )}
224
- {isPromptingPermission && (
225
- <span
226
- className="str-video__pending-permission"
227
- title={t('Waiting for permission')}
228
- >
229
- ?
230
- </span>
231
- )}
232
- {isSystemMuted &&
233
- hasBrowserPermission &&
234
- hasPermission &&
235
- isPublishingVideoAllowed && (
201
+ <CompositeButton
202
+ active={optionsAwareIsMute}
203
+ caption={caption}
204
+ variant="secondary"
205
+ disabled={
206
+ !hasBrowserPermission ||
207
+ !hasPermission ||
208
+ !isPublishingVideoAllowed ||
209
+ (!optimisticUpdates && isTogglePending)
210
+ }
211
+ data-testid={
212
+ optionsAwareIsMute ? 'video-unmute-button' : 'video-mute-button'
213
+ }
214
+ onClick={handleClick}
215
+ Menu={Menu}
216
+ menuPlacement={menuPlacement}
217
+ menuOffset={16}
218
+ {...restCompositeButtonProps}
219
+ onMenuToggle={(shown) => {
220
+ setTooltipDisabled(shown);
221
+ onMenuToggle?.(shown);
222
+ }}
223
+ >
224
+ <Icon icon={optionsAwareIsMute ? 'camera-off' : 'camera'} />
225
+ {(!hasBrowserPermission ||
226
+ !hasPermission ||
227
+ !isPublishingVideoAllowed) && (
228
+ <span className="str-video__no-media-permission">!</span>
229
+ )}
230
+ {isPromptingPermission && (
236
231
  <span
237
- className="str-video__system-muted"
238
- title={t('Camera is paused by your system')}
232
+ className="str-video__pending-permission"
233
+ title={t('Waiting for permission')}
239
234
  >
240
- !
235
+ ?
241
236
  </span>
242
237
  )}
243
- </CompositeButton>
244
- </WithTooltip>
238
+ {isSystemMuted &&
239
+ hasBrowserPermission &&
240
+ hasPermission &&
241
+ isPublishingVideoAllowed && (
242
+ <span
243
+ className="str-video__system-muted"
244
+ title={t('Camera is paused by your system')}
245
+ >
246
+ !
247
+ </span>
248
+ )}
249
+ </CompositeButton>
250
+ </WithTooltip>
251
+ </CameraUnavailableNotification>
245
252
  </PermissionNotification>
246
253
  </Restricted>
247
254
  );
@@ -0,0 +1,61 @@
1
+ import { PropsWithChildren, useEffect, useState } from 'react';
2
+ import { Placement } from '@floating-ui/react';
3
+ import clsx from 'clsx';
4
+ import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
5
+ import { Notification } from './Notification';
6
+
7
+ export type CameraUnavailableNotificationProps = {
8
+ placement?: Placement;
9
+ className?: string;
10
+ };
11
+
12
+ const UNAVAILABLE_ERRORS = ['NotReadableError', 'AbortError'];
13
+
14
+ const isCameraUnavailable = (error: Error | undefined) =>
15
+ !!error?.name && UNAVAILABLE_ERRORS.includes(error.name);
16
+
17
+ /**
18
+ * Shows when the last attempt to start the camera failed (e.g. the camera is
19
+ * in use by another app). Reads the error from the camera state, so it shows
20
+ * failures that happened before it was mounted.
21
+ *
22
+ * For error-specific copy, build your own notification on top of
23
+ * `useCameraState().error`.
24
+ */
25
+ export const CameraUnavailableNotification = ({
26
+ children,
27
+ placement,
28
+ className,
29
+ }: PropsWithChildren<CameraUnavailableNotificationProps>) => {
30
+ const { useCameraState } = useCallStateHooks();
31
+ const { error } = useCameraState();
32
+ const { t } = useI18n();
33
+ const [isVisible, setIsVisible] = useState(() => isCameraUnavailable(error));
34
+
35
+ useEffect(() => {
36
+ setIsVisible(isCameraUnavailable(error));
37
+ }, [error]);
38
+
39
+ const message = (
40
+ <>
41
+ <span className="str-video__notification__line str-video__notification__title">
42
+ {t('Camera unavailable')}
43
+ </span>
44
+ <span className="str-video__notification__line">
45
+ {t('Close other apps that might be using your camera and try again.')}
46
+ </span>
47
+ </>
48
+ );
49
+
50
+ return (
51
+ <Notification
52
+ message={message}
53
+ isVisible={isVisible}
54
+ placement={placement}
55
+ className={clsx('str-video__camera-unavailable-notification', className)}
56
+ close={() => setIsVisible(false)}
57
+ >
58
+ {children}
59
+ </Notification>
60
+ );
61
+ };
@@ -3,3 +3,4 @@ export * from './PermissionNotification';
3
3
  export * from './SpeakingWhileMutedNotification';
4
4
  export * from './RecordingInProgressNotification';
5
5
  export * from './MicCaptureErrorNotification';
6
+ export * from './CameraUnavailableNotification';
@@ -67,6 +67,8 @@
67
67
  "Camera on": "Camera on",
68
68
  "Camera off": "Camera off",
69
69
  "Camera is paused by your system": "Camera is paused by your system",
70
+ "Camera unavailable": "Camera unavailable",
71
+ "Close other apps that might be using your camera and try again.": "Close other apps that might be using your camera and try again.",
70
72
  "No camera found": "No camera found",
71
73
  "Video is disabled": "Video is disabled",
72
74
  "Pinned": "Pinned",