@stream-io/video-react-sdk 1.2.12 → 1.2.13

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.
@@ -37,6 +37,13 @@ export type BackgroundFiltersProps = {
37
37
  * (e.g., if you choose to host it yourself).
38
38
  */
39
39
  modelFilePath?: string;
40
+ /**
41
+ * When a started filter encounters an error, this callback will be executed.
42
+ * The default behavior (not overridable) is unregistering a failed filter.
43
+ * Use this callback to display UI error message, disable the corresponsing stream,
44
+ * or to try registering the filter again.
45
+ */
46
+ onError?: (error: any) => void;
40
47
  };
41
48
  export type BackgroundFiltersAPI = {
42
49
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "1.2.12",
3
+ "version": "1.2.13",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.es.js",
@@ -29,24 +29,27 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@floating-ui/react": "^0.26.5",
32
- "@stream-io/video-client": "1.4.3",
33
- "@stream-io/video-filters-web": "0.1.2",
34
- "@stream-io/video-react-bindings": "0.4.47",
32
+ "@stream-io/video-client": "1.4.4",
33
+ "@stream-io/video-filters-web": "0.1.3",
34
+ "@stream-io/video-react-bindings": "0.4.48",
35
35
  "chart.js": "^4.4.1",
36
36
  "clsx": "^2.0.0",
37
37
  "react-chartjs-2": "^5.2.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "react": "^18 || ^19"
40
+ "react": "^18 || ^19",
41
+ "react-dom": "^18 || ^19"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@rollup/plugin-json": "^6.1.0",
44
45
  "@rollup/plugin-replace": "^5.0.5",
45
46
  "@rollup/plugin-typescript": "^11.1.6",
46
- "@stream-io/audio-filters-web": "^0.2.0",
47
+ "@stream-io/audio-filters-web": "^0.2.1",
47
48
  "@stream-io/video-styling": "^1.0.5",
48
49
  "@types/react": "^18.3.2",
50
+ "@types/react-dom": "^18.3.0",
49
51
  "react": "^18.3.1",
52
+ "react-dom": "^18.3.1",
50
53
  "rimraf": "^5.0.7",
51
54
  "rollup": "^3.29.4",
52
55
  "typescript": "^5.5.2"
@@ -7,15 +7,17 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
+ import { flushSync } from 'react-dom';
10
11
  import clsx from 'clsx';
11
12
  import { useCall } from '@stream-io/video-react-bindings';
12
- import { disposeOfMediaStream } from '@stream-io/video-client';
13
+ import { disposeOfMediaStream, getLogger } from '@stream-io/video-client';
13
14
  import {
14
15
  BackgroundBlurLevel,
15
16
  BackgroundFilter,
16
17
  createRenderer,
17
18
  isPlatformSupported,
18
19
  loadTFLite,
20
+ Renderer,
19
21
  TFLite,
20
22
  } from '@stream-io/video-filters-web';
21
23
 
@@ -62,6 +64,14 @@ export type BackgroundFiltersProps = {
62
64
  * (e.g., if you choose to host it yourself).
63
65
  */
64
66
  modelFilePath?: string;
67
+
68
+ /**
69
+ * When a started filter encounters an error, this callback will be executed.
70
+ * The default behavior (not overridable) is unregistering a failed filter.
71
+ * Use this callback to display UI error message, disable the corresponsing stream,
72
+ * or to try registering the filter again.
73
+ */
74
+ onError?: (error: any) => void;
65
75
  };
66
76
 
67
77
  export type BackgroundFiltersAPI = {
@@ -139,6 +149,7 @@ export const BackgroundFiltersProvider = (
139
149
  tfFilePath,
140
150
  modelFilePath,
141
151
  basePath,
152
+ onError,
142
153
  } = props;
143
154
 
144
155
  const [backgroundFilter, setBackgroundFilter] = useState(bgFilterFromProps);
@@ -179,6 +190,18 @@ export const BackgroundFiltersProvider = (
179
190
  .catch((err) => console.error('Failed to load TFLite', err));
180
191
  }, [basePath, isSupported, modelFilePath, tfFilePath]);
181
192
 
193
+ const handleError = useCallback(
194
+ (error: any) => {
195
+ getLogger(['filters'])(
196
+ 'warn',
197
+ 'Filter encountered an error and will be disabled',
198
+ );
199
+ disableBackgroundFilter();
200
+ onError?.(error);
201
+ },
202
+ [disableBackgroundFilter, onError],
203
+ );
204
+
182
205
  return (
183
206
  <BackgroundFiltersContext.Provider
184
207
  value={{
@@ -194,167 +217,151 @@ export const BackgroundFiltersProvider = (
194
217
  tfFilePath,
195
218
  modelFilePath,
196
219
  basePath,
220
+ onError: handleError,
197
221
  }}
198
222
  >
199
223
  {children}
200
- {tfLite && backgroundFilter && <BackgroundFilters tfLite={tfLite} />}
224
+ {tfLite && <BackgroundFilters tfLite={tfLite} />}
201
225
  </BackgroundFiltersContext.Provider>
202
226
  );
203
227
  };
204
228
 
205
229
  const BackgroundFilters = (props: { tfLite: TFLite }) => {
206
- const { tfLite } = props;
207
230
  const call = useCall();
208
- const { backgroundImage, backgroundFilter } = useBackgroundFilters();
209
- const [videoRef, setVideoRef] = useState<HTMLVideoElement | null>(null);
210
- const [bgImageRef, setBgImageRef] = useState<HTMLImageElement | null>(null);
211
- const [canvasRef, setCanvasRef] = useState<HTMLCanvasElement | null>(null);
212
- const [width, setWidth] = useState(1920);
213
- const [height, setHeight] = useState(1080);
214
-
215
- // Holds a ref to the `resolve` function of the returned Promise as part
216
- // of the `camera.registerFilter()` API. Once the filter is initialized,
217
- // it should be called with the filtered MediaStream as an argument.
218
- const signalFilterReadyRef =
219
- useRef<(value: MediaStream | PromiseLike<MediaStream>) => void>();
220
-
221
- const [mediaStream, setMediaStream] = useState<MediaStream>();
222
- const unregister = useRef<Promise<void>>();
231
+ const { children, start } = useRenderer(props.tfLite);
232
+ const { backgroundFilter, onError } = useBackgroundFilters();
233
+ const handleErrorRef = useRef<((error: any) => void) | undefined>(undefined);
234
+ handleErrorRef.current = onError;
235
+
223
236
  useEffect(() => {
224
237
  if (!call || !backgroundFilter) return;
225
- const register = (unregister.current || Promise.resolve()).then(() =>
226
- call.camera.registerFilter(async (ms) => {
227
- return new Promise<MediaStream>((resolve) => {
228
- signalFilterReadyRef.current = resolve;
229
- setMediaStream(ms);
230
- });
231
- }),
238
+ const { unregister } = call.camera.registerFilter((ms) =>
239
+ start(ms, (error) => handleErrorRef.current?.(error)),
232
240
  );
233
-
234
241
  return () => {
235
- unregister.current = register
236
- .then((unregisterFilter) => unregisterFilter())
237
- .then(() => (signalFilterReadyRef.current = undefined))
238
- .then(() => setMediaStream(undefined))
239
- .catch((err) => console.error('Failed to unregister filter', err));
242
+ unregister();
240
243
  };
241
- }, [backgroundFilter, call]);
244
+ }, [backgroundFilter, call, start]);
242
245
 
243
- const [isPlaying, setIsPlaying] = useState(false);
244
- useEffect(() => {
245
- if (!mediaStream || !videoRef) return;
246
- const handleOnPlay = () => {
247
- const [track] = mediaStream.getVideoTracks();
248
- if (!track) return;
249
- const { width: w = 0, height: h = 0 } = track.getSettings();
250
- setWidth(w);
251
- setHeight(h);
252
- setIsPlaying(true);
253
- };
254
- videoRef.addEventListener('play', handleOnPlay);
255
- videoRef.srcObject = mediaStream;
256
- videoRef.play().catch((err) => {
257
- console.error('Failed to play video', err);
258
- });
259
- return () => {
260
- videoRef.removeEventListener('play', handleOnPlay);
261
- videoRef.srcObject = null;
262
- setIsPlaying(false);
263
- };
264
- }, [mediaStream, videoRef]);
246
+ return children;
247
+ };
265
248
 
266
- useEffect(() => {
267
- const resolveFilter = signalFilterReadyRef.current;
268
- if (!canvasRef || !resolveFilter) return;
249
+ const useRenderer = (tfLite: TFLite) => {
250
+ const { backgroundFilter, backgroundBlurLevel, backgroundImage } =
251
+ useBackgroundFilters();
252
+ const videoRef = useRef<HTMLVideoElement>(null);
253
+ const canvasRef = useRef<HTMLCanvasElement>(null);
254
+ const bgImageRef = useRef<HTMLImageElement>(null);
255
+ const [videoSize, setVideoSize] = useState<{ width: number; height: number }>(
256
+ {
257
+ width: 1920,
258
+ height: 1080,
259
+ },
260
+ );
269
261
 
270
- const filter = canvasRef.captureStream();
271
- resolveFilter(filter);
272
- return () => {
273
- disposeOfMediaStream(filter);
274
- };
275
- }, [canvasRef]);
262
+ const start = useCallback(
263
+ (ms: MediaStream, onError?: (error: any) => void) => {
264
+ let outputStream: MediaStream | undefined;
265
+ let renderer: Renderer | undefined;
266
+
267
+ const output = new Promise<MediaStream>((resolve, reject) => {
268
+ if (!backgroundFilter) {
269
+ reject(new Error('No filter specified'));
270
+ return;
271
+ }
272
+
273
+ const videoEl = videoRef.current;
274
+ const canvasEl = canvasRef.current;
275
+ const bgImageEl = bgImageRef.current;
276
+
277
+ if (!videoEl || !canvasEl || (backgroundImage && !bgImageEl)) {
278
+ // You should start renderer in effect or event handlers
279
+ reject(new Error('Renderer started before elements are ready'));
280
+ return;
281
+ }
282
+
283
+ videoEl.srcObject = ms;
284
+ videoEl.play().then(
285
+ () => {
286
+ const [track] = ms.getVideoTracks();
287
+
288
+ if (!track) {
289
+ reject(new Error('No video tracks in input media stream'));
290
+ return;
291
+ }
292
+
293
+ const trackSettings = track.getSettings();
294
+ flushSync(() =>
295
+ setVideoSize({
296
+ width: trackSettings.width ?? 0,
297
+ height: trackSettings.height ?? 0,
298
+ }),
299
+ );
300
+ renderer = createRenderer(
301
+ tfLite,
302
+ videoEl,
303
+ canvasEl,
304
+ {
305
+ backgroundFilter,
306
+ backgroundBlurLevel,
307
+ backgroundImage: bgImageEl ?? undefined,
308
+ },
309
+ onError,
310
+ );
311
+ outputStream = canvasEl.captureStream();
312
+ resolve(outputStream);
313
+ },
314
+ () => {
315
+ reject(new Error('Could not play the source video stream'));
316
+ },
317
+ );
318
+ });
319
+
320
+ return {
321
+ output,
322
+ stop: () => {
323
+ renderer?.dispose();
324
+ videoRef.current && (videoRef.current.srcObject = null);
325
+ outputStream && disposeOfMediaStream(outputStream);
326
+ },
327
+ };
328
+ },
329
+ [backgroundBlurLevel, backgroundFilter, backgroundImage, tfLite],
330
+ );
276
331
 
277
- return (
278
- <div
279
- className="str-video__background-filters"
280
- style={{
281
- width: `${width}px`,
282
- height: `${height}px`,
283
- }}
284
- >
285
- {mediaStream && isPlaying && (
286
- <RenderPipeline
287
- tfLite={tfLite}
288
- videoRef={videoRef}
289
- canvasRef={canvasRef}
290
- backgroundImageRef={bgImageRef}
291
- />
292
- )}
332
+ const children = (
333
+ <div className="str-video__background-filters">
293
334
  <video
294
335
  className={clsx(
295
336
  'str-video__background-filters__video',
296
- height > width && 'str-video__background-filters__video--tall',
337
+ videoSize.height > videoSize.width &&
338
+ 'str-video__background-filters__video--tall',
297
339
  )}
298
- ref={setVideoRef}
299
- autoPlay
340
+ ref={videoRef}
300
341
  playsInline
301
- controls={false}
302
- width={width}
303
- height={height}
304
342
  muted
305
- loop
343
+ controls={false}
344
+ {...videoSize}
306
345
  />
307
346
  {backgroundImage && (
308
347
  <img
309
348
  className="str-video__background-filters__background-image"
310
- key={backgroundImage}
311
349
  alt="Background"
312
- ref={setBgImageRef}
350
+ ref={bgImageRef}
313
351
  src={backgroundImage}
314
- width={width}
315
- height={height}
316
- />
317
- )}
318
- {isPlaying && (
319
- <canvas
320
- className="str-video__background-filters__target-canvas"
321
- width={width}
322
- height={height}
323
- ref={setCanvasRef}
352
+ {...videoSize}
324
353
  />
325
354
  )}
355
+ <canvas
356
+ className="str-video__background-filters__target-canvas"
357
+ {...videoSize}
358
+ ref={canvasRef}
359
+ />
326
360
  </div>
327
361
  );
328
- };
329
362
 
330
- const RenderPipeline = (props: {
331
- tfLite: TFLite;
332
- videoRef: HTMLVideoElement | null;
333
- canvasRef: HTMLCanvasElement | null;
334
- backgroundImageRef: HTMLImageElement | null;
335
- }) => {
336
- const { tfLite, videoRef, canvasRef, backgroundImageRef } = props;
337
- const { backgroundFilter, backgroundBlurLevel } = useBackgroundFilters();
338
- useEffect(() => {
339
- if (!videoRef || !canvasRef || !backgroundFilter) return;
340
- if (backgroundFilter === 'image' && !backgroundImageRef) return;
341
-
342
- const renderer = createRenderer(tfLite, videoRef, canvasRef, {
343
- backgroundFilter,
344
- backgroundImage: backgroundImageRef ?? undefined,
345
- backgroundBlurLevel,
346
- });
347
- return () => {
348
- renderer.dispose();
349
- };
350
- }, [
351
- backgroundBlurLevel,
352
- backgroundFilter,
353
- backgroundImageRef,
354
- canvasRef,
355
- tfLite,
356
- videoRef,
357
- ]);
358
-
359
- return null;
363
+ return {
364
+ start,
365
+ children,
366
+ };
360
367
  };