@stream-io/video-react-sdk 0.5.8 → 0.5.10

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,86 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { BackgroundBlurLevel, BackgroundFilter } from '@stream-io/video-filters-web';
3
+ export type BackgroundFiltersProps = {
4
+ /**
5
+ * Enables or disables the background-blurring feature.
6
+ * @default true.
7
+ */
8
+ isBlurringEnabled?: boolean;
9
+ /**
10
+ * A list of URLs to use as background images.
11
+ */
12
+ backgroundImages?: string[];
13
+ /**
14
+ * The background filter to apply to the video (by default).
15
+ * @default 'none'.
16
+ */
17
+ backgroundFilter?: BackgroundFilter;
18
+ /**
19
+ * The URL of the image to use as the background (by default).
20
+ */
21
+ backgroundImage?: string;
22
+ /**
23
+ * The level of blur to apply to the background (by default).
24
+ * @default 'high'.
25
+ */
26
+ backgroundBlurLevel?: BackgroundBlurLevel;
27
+ /**
28
+ * The base path for the TensorFlow Lite files.
29
+ * @default 'https://unpkg.com/@stream-io/video-filters-web/tf'.
30
+ */
31
+ basePath?: string;
32
+ /**
33
+ * The path to the TensorFlow Lite WebAssembly file.
34
+ *
35
+ * Override this prop to use a custom path to the TensorFlow Lite WebAssembly file
36
+ * (e.g., if you choose to host it yourself).
37
+ */
38
+ tfFilePath?: string;
39
+ /**
40
+ * The path to the TensorFlow Lite model file.
41
+ * Override this prop to use a custom path to the TensorFlow Lite model file
42
+ * (e.g., if you choose to host it yourself).
43
+ */
44
+ modelFilePath?: string;
45
+ };
46
+ export type BackgroundFiltersAPI = {
47
+ /**
48
+ * Whether the current platform supports the background filters.
49
+ */
50
+ isSupported: boolean;
51
+ /**
52
+ * Indicates whether the background filters engine is loaded and ready.
53
+ */
54
+ isReady: boolean;
55
+ /**
56
+ * Disables all background filters applied to the video.
57
+ */
58
+ disableBackgroundFilter: () => void;
59
+ /**
60
+ * Applies a background blur filter to the video.
61
+ *
62
+ * @param blurLevel the level of blur to apply to the background.
63
+ */
64
+ applyBackgroundBlurFilter: (blurLevel: BackgroundBlurLevel) => void;
65
+ /**
66
+ * Applies a background image filter to the video.
67
+ *
68
+ * @param imageUrl the URL of the image to use as the background.
69
+ */
70
+ applyBackgroundImageFilter: (imageUrl: string) => void;
71
+ };
72
+ /**
73
+ * The context value for the background filters context.
74
+ */
75
+ export type BackgroundFiltersContextValue = BackgroundFiltersProps & BackgroundFiltersAPI;
76
+ /**
77
+ * A hook to access the background filters context API.
78
+ */
79
+ export declare const useBackgroundFilters: () => BackgroundFiltersContextValue;
80
+ /**
81
+ * A provider component that enables the use of background filters in your app.
82
+ *
83
+ * Please make sure you have the `@stream-io/video-filters-web` package installed
84
+ * in your project before using this component.
85
+ */
86
+ export declare const BackgroundFiltersProvider: (props: PropsWithChildren<BackgroundFiltersProps>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export * from './BackgroundFilters';
@@ -1,5 +1,5 @@
1
1
  import { MouseEventHandler } from 'react';
2
- export declare const useEnterLeaveHandlers: <T extends HTMLElement>({ onMouseEnter, onMouseLeave, }?: Partial<Record<"onMouseEnter" | "onMouseLeave", MouseEventHandler<T>>>) => {
2
+ export declare const useEnterLeaveHandlers: <T extends HTMLElement>({ onMouseEnter, onMouseLeave, }?: Partial<Record<'onMouseEnter' | 'onMouseLeave', MouseEventHandler<T>>>) => {
3
3
  handleMouseEnter: MouseEventHandler<T>;
4
4
  handleMouseLeave: MouseEventHandler<T>;
5
5
  tooltipVisible: boolean;
@@ -1,4 +1,5 @@
1
1
  export * from './Avatar';
2
+ export * from './BackgroundFilters';
2
3
  export * from './Button';
3
4
  export * from './CallControls';
4
5
  export * from './CallParticipantsList';
@@ -1,2 +1,2 @@
1
1
  import { ComponentType, ReactElement } from 'react';
2
- export declare const isComponentType: <T extends {}>(elementOrComponent?: ReactElement<any, string | import("react").JSXElementConstructor<any>> | ComponentType<T> | null | undefined) => elementOrComponent is ComponentType<T>;
2
+ export declare const isComponentType: <T extends {}>(elementOrComponent?: ComponentType<T> | ReactElement | null) => elementOrComponent is ComponentType<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-sdk",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.es.js",
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "clean": "rimraf dist",
11
11
  "start": "rollup -c -w",
12
- "build": "NODE_ENV=production rollup -c && yarn copy-css",
12
+ "build": "rimraf dist && NODE_ENV=production rollup -c && yarn copy-css",
13
13
  "copy-css": "cp -R ../../node_modules/@stream-io/video-styling/dist/* dist/",
14
14
  "start:docs": "npx stream-chat-docusaurus -s"
15
15
  },
@@ -29,8 +29,9 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@floating-ui/react": "^0.26.5",
32
- "@stream-io/video-client": "^0.6.6",
33
- "@stream-io/video-react-bindings": "^0.4.6",
32
+ "@stream-io/video-client": "0.6.8",
33
+ "@stream-io/video-filters-web": "0.0.1",
34
+ "@stream-io/video-react-bindings": "0.4.8",
34
35
  "chart.js": "^4.4.1",
35
36
  "clsx": "^2.0.0",
36
37
  "react-chartjs-2": "^5.2.0"
@@ -40,14 +41,14 @@
40
41
  "react-dom": "^18.0.0"
41
42
  },
42
43
  "devDependencies": {
43
- "@rollup/plugin-json": "^6.0.1",
44
- "@rollup/plugin-replace": "^5.0.4",
45
- "@rollup/plugin-typescript": "^11.1.5",
46
- "@stream-io/video-styling": "^1.0.1",
44
+ "@rollup/plugin-json": "^6.1.0",
45
+ "@rollup/plugin-replace": "^5.0.5",
46
+ "@rollup/plugin-typescript": "^11.1.6",
47
+ "@stream-io/video-styling": "^1.0.2",
47
48
  "react": "^18.2.0",
48
49
  "react-dom": "^18.2.0",
49
50
  "rimraf": "^5.0.5",
50
51
  "rollup": "^3.29.4",
51
- "typescript": "^5.2.2"
52
+ "typescript": "^5.4.3"
52
53
  }
53
54
  }
@@ -0,0 +1,354 @@
1
+ import {
2
+ createContext,
3
+ PropsWithChildren,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import clsx from 'clsx';
11
+ import { useCall } from '@stream-io/video-react-bindings';
12
+ import {
13
+ BackgroundBlurLevel,
14
+ BackgroundFilter,
15
+ createRenderer,
16
+ isPlatformSupported,
17
+ loadTFLite,
18
+ TFLite,
19
+ } from '@stream-io/video-filters-web';
20
+
21
+ export type BackgroundFiltersProps = {
22
+ /**
23
+ * Enables or disables the background-blurring feature.
24
+ * @default true.
25
+ */
26
+ isBlurringEnabled?: boolean;
27
+
28
+ /**
29
+ * A list of URLs to use as background images.
30
+ */
31
+ backgroundImages?: string[];
32
+
33
+ /**
34
+ * The background filter to apply to the video (by default).
35
+ * @default 'none'.
36
+ */
37
+ backgroundFilter?: BackgroundFilter;
38
+
39
+ /**
40
+ * The URL of the image to use as the background (by default).
41
+ */
42
+ backgroundImage?: string;
43
+
44
+ /**
45
+ * The level of blur to apply to the background (by default).
46
+ * @default 'high'.
47
+ */
48
+ backgroundBlurLevel?: BackgroundBlurLevel;
49
+
50
+ /**
51
+ * The base path for the TensorFlow Lite files.
52
+ * @default 'https://unpkg.com/@stream-io/video-filters-web/tf'.
53
+ */
54
+ basePath?: string;
55
+
56
+ /**
57
+ * The path to the TensorFlow Lite WebAssembly file.
58
+ *
59
+ * Override this prop to use a custom path to the TensorFlow Lite WebAssembly file
60
+ * (e.g., if you choose to host it yourself).
61
+ */
62
+ tfFilePath?: string;
63
+
64
+ /**
65
+ * The path to the TensorFlow Lite model file.
66
+ * Override this prop to use a custom path to the TensorFlow Lite model file
67
+ * (e.g., if you choose to host it yourself).
68
+ */
69
+ modelFilePath?: string;
70
+ };
71
+
72
+ export type BackgroundFiltersAPI = {
73
+ /**
74
+ * Whether the current platform supports the background filters.
75
+ */
76
+ isSupported: boolean;
77
+
78
+ /**
79
+ * Indicates whether the background filters engine is loaded and ready.
80
+ */
81
+ isReady: boolean;
82
+
83
+ /**
84
+ * Disables all background filters applied to the video.
85
+ */
86
+ disableBackgroundFilter: () => void;
87
+
88
+ /**
89
+ * Applies a background blur filter to the video.
90
+ *
91
+ * @param blurLevel the level of blur to apply to the background.
92
+ */
93
+ applyBackgroundBlurFilter: (blurLevel: BackgroundBlurLevel) => void;
94
+
95
+ /**
96
+ * Applies a background image filter to the video.
97
+ *
98
+ * @param imageUrl the URL of the image to use as the background.
99
+ */
100
+ applyBackgroundImageFilter: (imageUrl: string) => void;
101
+ };
102
+
103
+ /**
104
+ * The context value for the background filters context.
105
+ */
106
+ export type BackgroundFiltersContextValue = BackgroundFiltersProps &
107
+ BackgroundFiltersAPI;
108
+
109
+ /**
110
+ * The context for the background filters.
111
+ */
112
+ const BackgroundFiltersContext = createContext<
113
+ BackgroundFiltersContextValue | undefined
114
+ >(undefined);
115
+
116
+ /**
117
+ * A hook to access the background filters context API.
118
+ */
119
+ export const useBackgroundFilters = () => {
120
+ const context = useContext(BackgroundFiltersContext);
121
+ if (!context) {
122
+ throw new Error(
123
+ 'useBackgroundFilters must be used within a BackgroundFiltersProvider',
124
+ );
125
+ }
126
+ return context;
127
+ };
128
+
129
+ /**
130
+ * A provider component that enables the use of background filters in your app.
131
+ *
132
+ * Please make sure you have the `@stream-io/video-filters-web` package installed
133
+ * in your project before using this component.
134
+ */
135
+ export const BackgroundFiltersProvider = (
136
+ props: PropsWithChildren<BackgroundFiltersProps>,
137
+ ) => {
138
+ const {
139
+ children,
140
+ isBlurringEnabled = true,
141
+ backgroundImages = [],
142
+ backgroundFilter: bgFilterFromProps = undefined,
143
+ backgroundImage: bgImageFromProps = undefined,
144
+ backgroundBlurLevel: bgBlurLevelFromProps = 'high',
145
+ tfFilePath,
146
+ modelFilePath,
147
+ basePath,
148
+ } = props;
149
+
150
+ const [backgroundFilter, setBackgroundFilter] = useState(bgFilterFromProps);
151
+ const [backgroundImage, setBackgroundImage] = useState(bgImageFromProps);
152
+ const [backgroundBlurLevel, setBackgroundBlurLevel] =
153
+ useState(bgBlurLevelFromProps);
154
+
155
+ const applyBackgroundImageFilter = useCallback((imageUrl: string) => {
156
+ setBackgroundFilter('image');
157
+ setBackgroundImage(imageUrl);
158
+ }, []);
159
+
160
+ const applyBackgroundBlurFilter = useCallback(
161
+ (blurLevel: BackgroundBlurLevel = 'high') => {
162
+ setBackgroundFilter('blur');
163
+ setBackgroundBlurLevel(blurLevel);
164
+ },
165
+ [],
166
+ );
167
+
168
+ const disableBackgroundFilter = useCallback(() => {
169
+ setBackgroundFilter(undefined);
170
+ setBackgroundImage(undefined);
171
+ setBackgroundBlurLevel('high');
172
+ }, []);
173
+
174
+ const [isSupported, setIsSupported] = useState(false);
175
+ useEffect(() => {
176
+ isPlatformSupported().then(setIsSupported);
177
+ }, []);
178
+
179
+ const [tfLite, setTfLite] = useState<TFLite>();
180
+ useEffect(() => {
181
+ // don't try to load TFLite if the platform is not supported
182
+ if (!isSupported) return;
183
+ loadTFLite({ basePath, modelFilePath, tfFilePath })
184
+ .then(setTfLite)
185
+ .catch((err) => console.error('Failed to load TFLite', err));
186
+ }, [basePath, isSupported, modelFilePath, tfFilePath]);
187
+
188
+ return (
189
+ <BackgroundFiltersContext.Provider
190
+ value={{
191
+ isSupported,
192
+ isReady: !!tfLite,
193
+ backgroundImage,
194
+ backgroundBlurLevel,
195
+ backgroundFilter,
196
+ disableBackgroundFilter,
197
+ applyBackgroundBlurFilter,
198
+ applyBackgroundImageFilter,
199
+ backgroundImages,
200
+ isBlurringEnabled,
201
+ tfFilePath,
202
+ modelFilePath,
203
+ basePath,
204
+ }}
205
+ >
206
+ {children}
207
+ {tfLite && backgroundFilter && <BackgroundFilters tfLite={tfLite} />}
208
+ </BackgroundFiltersContext.Provider>
209
+ );
210
+ };
211
+
212
+ const BackgroundFilters = (props: { tfLite: TFLite }) => {
213
+ const { tfLite } = props;
214
+ const call = useCall();
215
+ const { backgroundImage, backgroundFilter } = useBackgroundFilters();
216
+ const [videoRef, setVideoRef] = useState<HTMLVideoElement | null>(null);
217
+ const [bgImageRef, setBgImageRef] = useState<HTMLImageElement | null>(null);
218
+ const [canvasRef, setCanvasRef] = useState<HTMLCanvasElement | null>(null);
219
+ const [width, setWidth] = useState(1920);
220
+ const [height, setHeight] = useState(1080);
221
+
222
+ const resolveFilterRef =
223
+ useRef<(value: MediaStream | PromiseLike<MediaStream>) => void>();
224
+
225
+ const [mediaStream, setMediaStream] = useState<MediaStream>();
226
+ const registerFilterRef = useRef(Promise.resolve(async () => {}));
227
+ useEffect(() => {
228
+ if (!call || !backgroundFilter) return;
229
+ registerFilterRef.current = registerFilterRef.current.then(() =>
230
+ call.camera.registerFilter(async (ms) => {
231
+ return new Promise<MediaStream>((resolve) => {
232
+ setMediaStream(ms);
233
+ resolveFilterRef.current = resolve;
234
+ });
235
+ }),
236
+ );
237
+
238
+ return () => {
239
+ registerFilterRef.current
240
+ .then((unregister) => unregister())
241
+ .then(() => setMediaStream(undefined))
242
+ .catch((err) => console.error('Failed to unregister filter', err));
243
+ };
244
+ }, [backgroundFilter, call]);
245
+
246
+ useEffect(() => {
247
+ if (!mediaStream || !videoRef || !canvasRef) return;
248
+
249
+ const handleOnPlay = () => {
250
+ const [track] = mediaStream.getVideoTracks();
251
+ if (track) {
252
+ const { width: w = 0, height: h = 0 } = track.getSettings();
253
+ setWidth(w);
254
+ setHeight(h);
255
+ }
256
+
257
+ const resolveFilter = resolveFilterRef.current;
258
+ if (!resolveFilter) return;
259
+ const filter = canvasRef.captureStream();
260
+ resolveFilter(filter);
261
+ };
262
+ videoRef.addEventListener('play', handleOnPlay);
263
+
264
+ videoRef.srcObject = mediaStream;
265
+ videoRef.play().catch((err) => console.error('Failed to play video', err));
266
+ return () => {
267
+ videoRef.removeEventListener('play', handleOnPlay);
268
+ videoRef.srcObject = null;
269
+ };
270
+ }, [canvasRef, mediaStream, videoRef]);
271
+
272
+ return (
273
+ <div
274
+ className="str-video__background-filters"
275
+ style={{
276
+ width: `${width}px`,
277
+ height: `${height}px`,
278
+ }}
279
+ >
280
+ {mediaStream && (
281
+ <RenderPipeline
282
+ tfLite={tfLite}
283
+ videoRef={videoRef}
284
+ canvasRef={canvasRef}
285
+ backgroundImageRef={bgImageRef}
286
+ />
287
+ )}
288
+ <video
289
+ className={clsx(
290
+ 'str-video__background-filters__video',
291
+ height > width && 'str-video__background-filters__video--tall',
292
+ )}
293
+ ref={setVideoRef}
294
+ autoPlay
295
+ playsInline
296
+ controls={false}
297
+ width={width}
298
+ height={height}
299
+ muted
300
+ loop
301
+ />
302
+ {backgroundImage && (
303
+ <img
304
+ className="str-video__background-filters__background-image"
305
+ key={backgroundImage}
306
+ alt="Background"
307
+ ref={setBgImageRef}
308
+ src={backgroundImage}
309
+ width={width}
310
+ height={height}
311
+ />
312
+ )}
313
+ <canvas
314
+ className="str-video__background-filters__target-canvas"
315
+ key={`key-${width}${height}`}
316
+ width={width}
317
+ height={height}
318
+ ref={setCanvasRef}
319
+ />
320
+ </div>
321
+ );
322
+ };
323
+
324
+ const RenderPipeline = (props: {
325
+ tfLite: TFLite;
326
+ videoRef: HTMLVideoElement | null;
327
+ canvasRef: HTMLCanvasElement | null;
328
+ backgroundImageRef: HTMLImageElement | null;
329
+ }) => {
330
+ const { tfLite, videoRef, canvasRef, backgroundImageRef } = props;
331
+ const { backgroundFilter, backgroundBlurLevel } = useBackgroundFilters();
332
+ useEffect(() => {
333
+ if (!videoRef || !canvasRef || !backgroundFilter) return;
334
+ if (backgroundFilter === 'image' && !backgroundImageRef) return;
335
+
336
+ const renderer = createRenderer(tfLite, videoRef, canvasRef, {
337
+ backgroundFilter,
338
+ backgroundImage: backgroundImageRef ?? undefined,
339
+ backgroundBlurLevel,
340
+ });
341
+ return () => {
342
+ renderer.dispose();
343
+ };
344
+ }, [
345
+ backgroundBlurLevel,
346
+ backgroundFilter,
347
+ backgroundImageRef,
348
+ canvasRef,
349
+ tfLite,
350
+ videoRef,
351
+ ]);
352
+
353
+ return null;
354
+ };
@@ -0,0 +1 @@
1
+ export * from './BackgroundFilters';
@@ -1,4 +1,5 @@
1
1
  export * from './Avatar';
2
+ export * from './BackgroundFilters';
2
3
  export * from './Button';
3
4
  export * from './CallControls';
4
5
  export * from './CallParticipantsList';