@stream-io/video-react-sdk 1.36.0 → 1.37.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{embedded-BackgroundFilters-g6ozIvlN.es.js → background-filters-D2U5UBbH.es.js} +95 -32
  3. package/dist/background-filters-D2U5UBbH.es.js.map +1 -0
  4. package/dist/{background-filters-DdQqSx4T.cjs.js → background-filters-folwWrgs.cjs.js} +95 -32
  5. package/dist/background-filters-folwWrgs.cjs.js.map +1 -0
  6. package/dist/css/embedded.css +6 -0
  7. package/dist/css/embedded.css.map +1 -1
  8. package/dist/css/styles.css +6 -0
  9. package/dist/css/styles.css.map +1 -1
  10. package/dist/{background-filters-g6ozIvlN.es.js → embedded-BackgroundFilters-D2U5UBbH.es.js} +95 -32
  11. package/dist/embedded-BackgroundFilters-D2U5UBbH.es.js.map +1 -0
  12. package/dist/{embedded-BackgroundFilters-DdQqSx4T.cjs.js → embedded-BackgroundFilters-folwWrgs.cjs.js} +95 -32
  13. package/dist/embedded-BackgroundFilters-folwWrgs.cjs.js.map +1 -0
  14. package/dist/embedded.cjs.js +41 -25
  15. package/dist/embedded.cjs.js.map +1 -1
  16. package/dist/embedded.es.js +42 -26
  17. package/dist/embedded.es.js.map +1 -1
  18. package/dist/index.cjs.js +42 -26
  19. package/dist/index.cjs.js.map +1 -1
  20. package/dist/index.es.js +42 -26
  21. package/dist/index.es.js.map +1 -1
  22. package/dist/src/components/BackgroundFilters/types.d.ts +5 -4
  23. package/dist/src/core/hooks/useTrackElementVisibility.d.ts +2 -3
  24. package/dist/src/translations/index.d.ts +2 -0
  25. package/package.json +5 -5
  26. package/src/components/BackgroundFilters/BackgroundFilters.tsx +145 -53
  27. package/src/components/BackgroundFilters/types.ts +5 -4
  28. package/src/components/CallControls/ToggleAudioButton.tsx +32 -5
  29. package/src/components/CallControls/ToggleVideoButton.tsx +36 -5
  30. package/src/core/components/CallLayout/PaginatedGridLayout.tsx +1 -1
  31. package/src/core/components/CallLayout/SpeakerLayout.tsx +1 -1
  32. package/src/core/components/ParticipantView/ParticipantView.tsx +0 -1
  33. package/src/core/hooks/useTrackElementVisibility.ts +4 -14
  34. package/src/translations/en.json +2 -0
  35. package/dist/background-filters-DdQqSx4T.cjs.js.map +0 -1
  36. package/dist/background-filters-g6ozIvlN.es.js.map +0 -1
  37. package/dist/embedded-BackgroundFilters-DdQqSx4T.cjs.js.map +0 -1
  38. package/dist/embedded-BackgroundFilters-g6ozIvlN.es.js.map +0 -1
@@ -74,8 +74,9 @@ export type BackgroundFiltersProps = PlatformSupportFlags & {
74
74
  */
75
75
  onError?: (error: any) => void;
76
76
  /**
77
- * Configuration for performance metric thresholds.
78
- * Use this to customize when performance warnings are triggered.
77
+ * `@deprecated` Performance tuning is now handled internally by the SDK, so
78
+ * this prop is ignored. Remove this prop; there is no replacement
79
+ * configuration. Degradation is derived from the processed/source FPS ratio.
79
80
  */
80
81
  performanceThresholds?: BackgroundFiltersPerformanceThresholds;
81
82
  /**
@@ -91,8 +92,8 @@ export type PerformanceDegradationReason = 'frame-drop' | 'cpu-throttling';
91
92
  /**
92
93
  * Performance degradation information for background filters.
93
94
  *
94
- * Performance is calculated using an Exponential Moving Average (EMA) of FPS values
95
- * to smooth out quick spikes and provide stable performance warnings.
95
+ * Performance is calculated from a smoothed processed/source FPS ratio to
96
+ * provide stable performance warnings.
96
97
  */
97
98
  export type BackgroundFiltersPerformance = {
98
99
  /**
@@ -1,7 +1,6 @@
1
- import { DynascaleManager, VideoTrackType } from '@stream-io/video-client';
2
- export declare const useTrackElementVisibility: <T extends HTMLElement>({ trackedElement, dynascaleManager: propsDynascaleManager, sessionId, trackType, }: {
1
+ import { VideoTrackType } from '@stream-io/video-client';
2
+ export declare const useTrackElementVisibility: <T extends HTMLElement>({ trackedElement, sessionId, trackType, }: {
3
3
  trackedElement: T | null;
4
4
  sessionId: string;
5
- dynascaleManager?: DynascaleManager;
6
5
  trackType: VideoTrackType | "none";
7
6
  }) => void;
@@ -56,8 +56,10 @@ export declare const translations: {
56
56
  Dismiss: string;
57
57
  "Microphone on": string;
58
58
  "Microphone off": string;
59
+ "Microphone is paused by your system": string;
59
60
  "Camera on": string;
60
61
  "Camera off": string;
62
+ "Camera is paused by your system": string;
61
63
  "No camera found": string;
62
64
  "Video is disabled": string;
63
65
  Pinned: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/index.d.ts",
@@ -45,9 +45,9 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@floating-ui/react": "^0.27.6",
48
- "@stream-io/video-client": "1.48.0",
48
+ "@stream-io/video-client": "1.50.0",
49
49
  "@stream-io/video-filters-web": "0.7.4",
50
- "@stream-io/video-react-bindings": "1.15.0",
50
+ "@stream-io/video-react-bindings": "1.16.0",
51
51
  "chart.js": "^4.4.4",
52
52
  "clsx": "^2.0.0",
53
53
  "react-chartjs-2": "^5.3.0"
@@ -60,8 +60,8 @@
60
60
  "@rollup/plugin-json": "^6.1.0",
61
61
  "@rollup/plugin-replace": "^6.0.2",
62
62
  "@rollup/plugin-typescript": "^12.1.4",
63
- "@stream-io/audio-filters-web": "^0.7.3",
64
- "@stream-io/video-styling": "^1.13.0",
63
+ "@stream-io/audio-filters-web": "^0.8.0",
64
+ "@stream-io/video-styling": "^1.14.0",
65
65
  "@types/react": "~19.1.17",
66
66
  "@types/react-dom": "~19.1.11",
67
67
  "react": "19.1.0",
@@ -30,15 +30,26 @@ import type {
30
30
  PerformanceDegradationReason,
31
31
  } from './types';
32
32
 
33
+ type MediaStreamTrackVideoStats = {
34
+ deliveredFrames: number;
35
+ discardedFrames: number;
36
+ totalFrames: number;
37
+ };
38
+
39
+ type MediaStreamTrackWithStats = MediaStreamTrack & {
40
+ stats?: MediaStreamTrackVideoStats;
41
+ };
42
+
33
43
  /**
34
44
  * Constants for FPS warning calculation.
35
- * Smooths out quick spikes using an EMA, ignores brief outliers,
36
- * and uses two thresholds to avoid flickering near the limit.
45
+ * Smooths the processed-to-source FPS ratio and uses hysteresis
46
+ * so the degradation warning doesn't flicker near the limit.
37
47
  */
38
48
  const ALPHA = 0.2;
39
- const DEFAULT_FPS = 30;
40
- const DEVIATION_LIMIT = 0.5;
41
- const OUTLIER_PERSISTENCE = 5;
49
+ const FPS_RATIO_WARNING_THRESHOLD_LOWER = 0.75;
50
+ const FPS_RATIO_WARNING_THRESHOLD_UPPER = 0.85;
51
+
52
+ const EMPTY_BACKGROUND_IMAGES: string[] = [];
42
53
 
43
54
  /**
44
55
  * Represents the available background filter processing engines.
@@ -76,6 +87,76 @@ const determineEngine = async (
76
87
  return isMediaPipeSupported ? FilterEngine.MEDIA_PIPE : FilterEngine.NONE;
77
88
  };
78
89
 
90
+ /**
91
+ * Samples the raw camera track's frame stats (W3C MediaCapture Extensions) at a
92
+ * fixed interval and returns a live source fps estimate. Returns `undefined`
93
+ * while warming up, when the camera is off, or when the browser doesn't expose
94
+ * `MediaStreamTrack.stats` (currently Chromium-only).
95
+ */
96
+ const useTrackFramesPerSecond = (call: Call | undefined) => {
97
+ const { useCameraState } = useCallStateHooks();
98
+ const { rootMediaStream } = useCameraState();
99
+ const [fps, setFps] = useState<number | undefined>(undefined);
100
+ const trackId = rootMediaStream?.getVideoTracks()[0]?.id;
101
+
102
+ useEffect(() => {
103
+ if (!call) {
104
+ setFps(undefined);
105
+ return;
106
+ }
107
+
108
+ let previousSnapshot:
109
+ | (MediaStreamTrackVideoStats & { capturedAt: number })
110
+ | undefined;
111
+ let previousTrackId: string | undefined;
112
+
113
+ const intervalId = setInterval(() => {
114
+ const track = rootMediaStream?.getVideoTracks()[0] as
115
+ | MediaStreamTrackWithStats
116
+ | undefined;
117
+
118
+ const stats = track?.stats;
119
+ if (!track || !stats) {
120
+ previousSnapshot = undefined;
121
+ setFps(undefined);
122
+ return;
123
+ }
124
+
125
+ if (track.id !== previousTrackId) {
126
+ previousTrackId = track.id;
127
+ previousSnapshot = undefined;
128
+ setFps(undefined);
129
+ }
130
+
131
+ const currentSnapshot = {
132
+ deliveredFrames: stats.deliveredFrames,
133
+ discardedFrames: stats.discardedFrames,
134
+ totalFrames: stats.totalFrames,
135
+ capturedAt: performance.now(),
136
+ };
137
+
138
+ if (previousSnapshot) {
139
+ const elapsedSec =
140
+ (currentSnapshot.capturedAt - previousSnapshot.capturedAt) / 1000;
141
+
142
+ const delivered =
143
+ currentSnapshot.deliveredFrames - previousSnapshot.deliveredFrames;
144
+
145
+ const nextFps = Math.round(delivered / elapsedSec);
146
+ setFps(nextFps);
147
+ }
148
+ previousSnapshot = currentSnapshot;
149
+ }, 1000);
150
+
151
+ return () => {
152
+ clearInterval(intervalId);
153
+ setFps(undefined);
154
+ };
155
+ }, [call, rootMediaStream, trackId]);
156
+
157
+ return fps;
158
+ };
159
+
79
160
  /**
80
161
  * A provider component that enables the use of background filters in your app.
81
162
  *
@@ -93,7 +174,7 @@ export const BackgroundFiltersProvider = (
93
174
  const {
94
175
  ContextProvider,
95
176
  children,
96
- backgroundImages = [],
177
+ backgroundImages = EMPTY_BACKGROUND_IMAGES,
97
178
  backgroundFilter: bgFilterFromProps = undefined,
98
179
  backgroundImage: bgImageFromProps = undefined,
99
180
  backgroundBlurLevel: bgBlurLevelFromProps = undefined,
@@ -102,7 +183,6 @@ export const BackgroundFiltersProvider = (
102
183
  useLegacyFilter,
103
184
  basePath,
104
185
  onError,
105
- performanceThresholds,
106
186
  forceSafariSupport,
107
187
  forceMobileSupport,
108
188
  segmentationOptions,
@@ -120,46 +200,37 @@ export const BackgroundFiltersProvider = (
120
200
  const [showLowFpsWarning, setShowLowFpsWarning] = useState<boolean>(false);
121
201
  const [isLoading, setIsLoading] = useState<boolean>(false);
122
202
 
203
+ const rootFps = useTrackFramesPerSecond(call);
123
204
  const cameraFrameRate = callStatsReport?.publisherStats?.camera?.frameRate;
124
205
 
125
- const sourceFps =
126
- performanceThresholds?.defaultFps || cameraFrameRate || DEFAULT_FPS;
127
-
128
- const fpsWarningThresholdLower =
129
- performanceThresholds?.fpsWarningThresholdLower ?? sourceFps * 0.75;
130
- const fpsWarningThresholdUpper =
131
- performanceThresholds?.fpsWarningThresholdUpper ?? sourceFps * 0.85;
132
-
133
- const emaRef = useRef<number>(sourceFps);
134
- const outlierStreakRef = useRef<number>(0);
206
+ const sourceFps = rootFps ?? cameraFrameRate;
207
+ const fpsRatioEmaRef = useRef<number | undefined>(undefined);
135
208
 
136
209
  const handleStats = useCallback(
137
210
  (stats: PerformanceStats) => {
138
211
  const fps = stats?.fps;
139
- if (fps === undefined || fps === null) {
140
- emaRef.current = sourceFps;
141
- outlierStreakRef.current = 0;
212
+ if (fps == null || !sourceFps) {
213
+ fpsRatioEmaRef.current = undefined;
142
214
  setShowLowFpsWarning(false);
143
215
  return;
144
216
  }
145
217
 
146
- const prevEma = emaRef.current;
147
- const deviation = Math.abs(fps - prevEma) / prevEma;
148
-
149
- const isOutlier = fps < prevEma && deviation > DEVIATION_LIMIT;
150
- outlierStreakRef.current = isOutlier ? outlierStreakRef.current + 1 : 0;
151
- if (isOutlier && outlierStreakRef.current < OUTLIER_PERSISTENCE) return;
218
+ const ratio = fps / sourceFps;
219
+ const prevRatioEma = fpsRatioEmaRef.current ?? ratio;
152
220
 
153
- emaRef.current = ALPHA * fps + (1 - ALPHA) * prevEma;
221
+ const nextFpsRatioEma = ALPHA * ratio + (1 - ALPHA) * prevRatioEma;
222
+ fpsRatioEmaRef.current = nextFpsRatioEma;
154
223
 
155
224
  setShowLowFpsWarning((prev) => {
156
- if (prev && emaRef.current > fpsWarningThresholdUpper) return false;
157
- if (!prev && emaRef.current < fpsWarningThresholdLower) return true;
225
+ if (prev && nextFpsRatioEma > FPS_RATIO_WARNING_THRESHOLD_UPPER)
226
+ return false;
227
+ if (!prev && nextFpsRatioEma < FPS_RATIO_WARNING_THRESHOLD_LOWER)
228
+ return true;
158
229
 
159
230
  return prev;
160
231
  });
161
232
  },
162
- [fpsWarningThresholdLower, fpsWarningThresholdUpper, sourceFps],
233
+ [sourceFps],
163
234
  );
164
235
 
165
236
  const performance: BackgroundFiltersPerformance = useMemo(() => {
@@ -207,16 +278,17 @@ export const BackgroundFiltersProvider = (
207
278
  call?.tracer.trace('backgroundFilters.performance', {
208
279
  degraded: currentDegraded,
209
280
  reason: performance?.reason,
210
- fps: emaRef.current,
281
+ ratio: fpsRatioEmaRef.current,
282
+ sourceFps,
211
283
  });
212
284
  }
213
285
  prevDegradedRef.current = currentDegraded;
214
286
  }, [
215
- performanceThresholds,
216
287
  performance.degraded,
217
288
  performance.reason,
218
289
  backgroundFilter,
219
290
  call?.tracer,
291
+ sourceFps,
220
292
  ]);
221
293
 
222
294
  const applyBackgroundImageFilter = useCallback((imageUrl: string) => {
@@ -237,10 +309,9 @@ export const BackgroundFiltersProvider = (
237
309
  setBackgroundImage(undefined);
238
310
  setBackgroundBlurLevel(undefined);
239
311
 
240
- emaRef.current = sourceFps;
241
- outlierStreakRef.current = 0;
312
+ fpsRatioEmaRef.current = undefined;
242
313
  setShowLowFpsWarning(false);
243
- }, [sourceFps]);
314
+ }, []);
244
315
 
245
316
  const [engine, setEngine] = useState<FilterEngine>(FilterEngine.NONE);
246
317
  const [isSupported, setIsSupported] = useState(false);
@@ -288,24 +359,45 @@ export const BackgroundFiltersProvider = (
288
359
  );
289
360
 
290
361
  const isReady = useLegacyFilter ? !!tfLite : !!mediaPipe;
291
- const contextValue: BackgroundFiltersContextValue = {
292
- isSupported,
293
- performance,
294
- isReady,
295
- isLoading,
296
- backgroundImage,
297
- backgroundBlurLevel,
298
- backgroundFilter,
299
- disableBackgroundFilter,
300
- applyBackgroundBlurFilter,
301
- applyBackgroundImageFilter,
302
- backgroundImages,
303
- tfFilePath,
304
- modelFilePath,
305
- basePath,
306
- onError: handleError,
307
- segmentationOptions,
308
- };
362
+
363
+ const contextValue = useMemo<BackgroundFiltersContextValue>(
364
+ () => ({
365
+ isSupported,
366
+ performance,
367
+ isReady,
368
+ isLoading,
369
+ backgroundImage,
370
+ backgroundBlurLevel,
371
+ backgroundFilter,
372
+ disableBackgroundFilter,
373
+ applyBackgroundBlurFilter,
374
+ applyBackgroundImageFilter,
375
+ backgroundImages,
376
+ tfFilePath,
377
+ modelFilePath,
378
+ basePath,
379
+ onError: handleError,
380
+ segmentationOptions,
381
+ }),
382
+ [
383
+ isSupported,
384
+ performance,
385
+ isReady,
386
+ isLoading,
387
+ backgroundImage,
388
+ backgroundBlurLevel,
389
+ backgroundFilter,
390
+ disableBackgroundFilter,
391
+ applyBackgroundBlurFilter,
392
+ applyBackgroundImageFilter,
393
+ backgroundImages,
394
+ tfFilePath,
395
+ modelFilePath,
396
+ basePath,
397
+ handleError,
398
+ segmentationOptions,
399
+ ],
400
+ );
309
401
  return (
310
402
  <ContextProvider.Provider value={contextValue}>
311
403
  {children}
@@ -92,8 +92,9 @@ export type BackgroundFiltersProps = PlatformSupportFlags & {
92
92
  onError?: (error: any) => void;
93
93
 
94
94
  /**
95
- * Configuration for performance metric thresholds.
96
- * Use this to customize when performance warnings are triggered.
95
+ * `@deprecated` Performance tuning is now handled internally by the SDK, so
96
+ * this prop is ignored. Remove this prop; there is no replacement
97
+ * configuration. Degradation is derived from the processed/source FPS ratio.
97
98
  */
98
99
  performanceThresholds?: BackgroundFiltersPerformanceThresholds;
99
100
 
@@ -112,8 +113,8 @@ export type PerformanceDegradationReason = 'frame-drop' | 'cpu-throttling';
112
113
  /**
113
114
  * Performance degradation information for background filters.
114
115
  *
115
- * Performance is calculated using an Exponential Moving Average (EMA) of FPS values
116
- * to smooth out quick spikes and provide stable performance warnings.
116
+ * Performance is calculated from a smoothed processed/source FPS ratio to
117
+ * provide stable performance warnings.
117
118
  */
118
119
  export type BackgroundFiltersPerformance = {
119
120
  /**
@@ -1,4 +1,4 @@
1
- import { OwnCapability } from '@stream-io/video-client';
1
+ import { OwnCapability, SfuModels } from '@stream-io/video-client';
2
2
  import {
3
3
  Restricted,
4
4
  useCallStateHooks,
@@ -38,7 +38,7 @@ export const ToggleAudioPreviewButton = (
38
38
  ...restCompositeButtonProps
39
39
  } = props;
40
40
  const { t } = useI18n();
41
- const { useMicrophoneState } = useCallStateHooks();
41
+ const { useMicrophoneState, useLocalParticipant } = useCallStateHooks();
42
42
  const {
43
43
  microphone,
44
44
  hasBrowserPermission,
@@ -46,6 +46,10 @@ export const ToggleAudioPreviewButton = (
46
46
  optionsAwareIsMute,
47
47
  isTogglePending,
48
48
  } = useMicrophoneState({ optimisticUpdates });
49
+ const localParticipant = useLocalParticipant();
50
+ const isSystemMuted = !!localParticipant?.interruptedTracks?.includes(
51
+ SfuModels.TrackType.AUDIO,
52
+ );
49
53
  const [tooltipDisabled, setTooltipDisabled] = useState(false);
50
54
  const handleClick = createCallControlHandler(props, () =>
51
55
  microphone.toggle(),
@@ -56,7 +60,9 @@ export const ToggleAudioPreviewButton = (
56
60
  title={
57
61
  !hasBrowserPermission
58
62
  ? t('Check your browser audio permissions')
59
- : (caption ?? t('Mic'))
63
+ : isSystemMuted
64
+ ? t('Microphone is paused by your system')
65
+ : (caption ?? t('Mic'))
60
66
  }
61
67
  tooltipDisabled={tooltipDisabled}
62
68
  >
@@ -99,6 +105,13 @@ export const ToggleAudioPreviewButton = (
99
105
  children="?"
100
106
  />
101
107
  )}
108
+ {isSystemMuted && hasBrowserPermission && (
109
+ <span
110
+ className="str-video__system-muted"
111
+ title={t('Microphone is paused by your system')}
112
+ children="!"
113
+ />
114
+ )}
102
115
  </CompositeButton>
103
116
  </WithTooltip>
104
117
  );
@@ -128,7 +141,7 @@ export const ToggleAudioPublishingButton = (
128
141
  const { hasPermission, requestPermission, isAwaitingPermission } =
129
142
  useRequestPermission(OwnCapability.SEND_AUDIO);
130
143
 
131
- const { useMicrophoneState } = useCallStateHooks();
144
+ const { useMicrophoneState, useLocalParticipant } = useCallStateHooks();
132
145
  const {
133
146
  microphone,
134
147
  hasBrowserPermission,
@@ -136,6 +149,10 @@ export const ToggleAudioPublishingButton = (
136
149
  isTogglePending,
137
150
  optionsAwareIsMute,
138
151
  } = useMicrophoneState({ optimisticUpdates });
152
+ const localParticipant = useLocalParticipant();
153
+ const isSystemMuted = !!localParticipant?.interruptedTracks?.includes(
154
+ SfuModels.TrackType.AUDIO,
155
+ );
139
156
 
140
157
  const [tooltipDisabled, setTooltipDisabled] = useState(false);
141
158
  const handleClick = createCallControlHandler(props, async () => {
@@ -161,7 +178,9 @@ export const ToggleAudioPublishingButton = (
161
178
  ? t('You have no permission to share your audio')
162
179
  : !hasBrowserPermission
163
180
  ? t('Check your browser mic permissions')
164
- : (caption ?? t('Mic'))
181
+ : isSystemMuted
182
+ ? t('Microphone is paused by your system')
183
+ : (caption ?? t('Mic'))
165
184
  }
166
185
  tooltipDisabled={tooltipDisabled}
167
186
  >
@@ -200,6 +219,14 @@ export const ToggleAudioPublishingButton = (
200
219
  ?
201
220
  </span>
202
221
  )}
222
+ {isSystemMuted && hasBrowserPermission && hasPermission && (
223
+ <span
224
+ className="str-video__system-muted"
225
+ title={t('Microphone is paused by your system')}
226
+ >
227
+ !
228
+ </span>
229
+ )}
203
230
  </CompositeButton>
204
231
  </WithTooltip>
205
232
  </PermissionNotification>
@@ -5,7 +5,7 @@ import {
5
5
  UseInputMediaDeviceOptions,
6
6
  } from '@stream-io/video-react-bindings';
7
7
  import clsx from 'clsx';
8
- import { OwnCapability } from '@stream-io/video-client';
8
+ import { OwnCapability, SfuModels } from '@stream-io/video-client';
9
9
  import { CompositeButton, IconButtonWithMenuProps } from '../Button/';
10
10
  import { DeviceSelectorVideo } from '../DeviceSettings';
11
11
  import { PermissionNotification } from '../Notification';
@@ -38,7 +38,7 @@ export const ToggleVideoPreviewButton = (
38
38
  ...restCompositeButtonProps
39
39
  } = props;
40
40
  const { t } = useI18n();
41
- const { useCameraState } = useCallStateHooks();
41
+ const { useCameraState, useLocalParticipant } = useCallStateHooks();
42
42
  const {
43
43
  camera,
44
44
  hasBrowserPermission,
@@ -46,6 +46,10 @@ export const ToggleVideoPreviewButton = (
46
46
  isTogglePending,
47
47
  optionsAwareIsMute,
48
48
  } = useCameraState({ optimisticUpdates });
49
+ const localParticipant = useLocalParticipant();
50
+ const isSystemMuted = !!localParticipant?.interruptedTracks?.includes(
51
+ SfuModels.TrackType.VIDEO,
52
+ );
49
53
  const [tooltipDisabled, setTooltipDisabled] = useState(false);
50
54
  const handleClick = createCallControlHandler(props, () => camera.toggle());
51
55
 
@@ -54,7 +58,9 @@ export const ToggleVideoPreviewButton = (
54
58
  title={
55
59
  !hasBrowserPermission
56
60
  ? t('Check your browser video permissions')
57
- : (caption ?? t('Video'))
61
+ : isSystemMuted
62
+ ? t('Camera is paused by your system')
63
+ : (caption ?? t('Video'))
58
64
  }
59
65
  tooltipDisabled={tooltipDisabled}
60
66
  >
@@ -97,6 +103,13 @@ export const ToggleVideoPreviewButton = (
97
103
  children="?"
98
104
  />
99
105
  )}
106
+ {isSystemMuted && hasBrowserPermission && (
107
+ <span
108
+ className="str-video__system-muted"
109
+ title={t('Camera is paused by your system')}
110
+ children="!"
111
+ />
112
+ )}
100
113
  </CompositeButton>
101
114
  </WithTooltip>
102
115
  );
@@ -126,7 +139,8 @@ export const ToggleVideoPublishingButton = (
126
139
  const { hasPermission, requestPermission, isAwaitingPermission } =
127
140
  useRequestPermission(OwnCapability.SEND_VIDEO);
128
141
 
129
- const { useCameraState, useCallSettings } = useCallStateHooks();
142
+ const { useCameraState, useCallSettings, useLocalParticipant } =
143
+ useCallStateHooks();
130
144
  const {
131
145
  camera,
132
146
  optionsAwareIsMute,
@@ -134,6 +148,10 @@ export const ToggleVideoPublishingButton = (
134
148
  isPromptingPermission,
135
149
  isTogglePending,
136
150
  } = useCameraState({ optimisticUpdates });
151
+ const localParticipant = useLocalParticipant();
152
+ const isSystemMuted = !!localParticipant?.interruptedTracks?.includes(
153
+ SfuModels.TrackType.VIDEO,
154
+ );
137
155
  const callSettings = useCallSettings();
138
156
  const isPublishingVideoAllowed = callSettings?.video.enabled;
139
157
  const [tooltipDisabled, setTooltipDisabled] = useState(false);
@@ -164,7 +182,9 @@ export const ToggleVideoPublishingButton = (
164
182
  ? t('Check your browser video permissions')
165
183
  : !isPublishingVideoAllowed
166
184
  ? t('Video publishing is disabled by the system')
167
- : caption || t('Video')
185
+ : isSystemMuted
186
+ ? t('Camera is paused by your system')
187
+ : caption || t('Video')
168
188
  }
169
189
  tooltipDisabled={tooltipDisabled}
170
190
  >
@@ -205,6 +225,17 @@ export const ToggleVideoPublishingButton = (
205
225
  ?
206
226
  </span>
207
227
  )}
228
+ {isSystemMuted &&
229
+ hasBrowserPermission &&
230
+ hasPermission &&
231
+ isPublishingVideoAllowed && (
232
+ <span
233
+ className="str-video__system-muted"
234
+ title={t('Camera is paused by your system')}
235
+ >
236
+ !
237
+ </span>
238
+ )}
208
239
  </CompositeButton>
209
240
  </WithTooltip>
210
241
  </PermissionNotification>
@@ -152,7 +152,7 @@ export const PaginatedGridLayout = (props: PaginatedGridLayoutProps) => {
152
152
 
153
153
  const cleanup = call.setViewport(paginatedGridLayoutWrapperElement);
154
154
 
155
- return () => cleanup();
155
+ return () => cleanup?.();
156
156
  }, [paginatedGridLayoutWrapperElement, call]);
157
157
 
158
158
  // only used to render video elements
@@ -149,7 +149,7 @@ export const SpeakerLayout = ({
149
149
 
150
150
  const cleanup = call.setViewport(participantsBarWrapperElement);
151
151
 
152
- return () => cleanup();
152
+ return () => cleanup?.();
153
153
  }, [participantsBarWrapperElement, call]);
154
154
 
155
155
  const isOneOnOneCall = allParticipants.length === 2;
@@ -102,7 +102,6 @@ export const ParticipantView = forwardRef<HTMLDivElement, ParticipantViewProps>(
102
102
  const [contextVideoPlaceholderElement, setContextVideoPlaceholderElement] =
103
103
  useState<HTMLDivElement | null>(null);
104
104
 
105
- // TODO: allow to pass custom ViewportTracker instance from props
106
105
  useTrackElementVisibility({
107
106
  sessionId,
108
107
  trackedElement,
@@ -1,29 +1,19 @@
1
1
  import { useEffect } from 'react';
2
- import { DynascaleManager, VideoTrackType } from '@stream-io/video-client';
2
+ import { VideoTrackType } from '@stream-io/video-client';
3
3
  import { useCall } from '@stream-io/video-react-bindings';
4
4
 
5
5
  export const useTrackElementVisibility = <T extends HTMLElement>({
6
6
  trackedElement,
7
- dynascaleManager: propsDynascaleManager,
8
7
  sessionId,
9
8
  trackType,
10
9
  }: {
11
10
  trackedElement: T | null;
12
11
  sessionId: string;
13
- dynascaleManager?: DynascaleManager;
14
12
  trackType: VideoTrackType | 'none';
15
13
  }) => {
16
14
  const call = useCall();
17
- const manager = propsDynascaleManager ?? call?.dynascaleManager;
18
15
  useEffect(() => {
19
- if (!trackedElement || !manager || !call || trackType === 'none') return;
20
- const unobserve = manager.trackElementVisibility(
21
- trackedElement,
22
- sessionId,
23
- trackType,
24
- );
25
- return () => {
26
- unobserve();
27
- };
28
- }, [trackedElement, manager, call, sessionId, trackType]);
16
+ if (!trackedElement || !call || trackType === 'none') return;
17
+ return call.trackElementVisibility(trackedElement, sessionId, trackType);
18
+ }, [trackedElement, call, sessionId, trackType]);
29
19
  };
@@ -60,8 +60,10 @@
60
60
 
61
61
  "Microphone on": "Microphone on",
62
62
  "Microphone off": "Microphone off",
63
+ "Microphone is paused by your system": "Microphone is paused by your system",
63
64
  "Camera on": "Camera on",
64
65
  "Camera off": "Camera off",
66
+ "Camera is paused by your system": "Camera is paused by your system",
65
67
  "No camera found": "No camera found",
66
68
  "Video is disabled": "Video is disabled",
67
69
  "Pinned": "Pinned",