@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.
- package/CHANGELOG.md +13 -0
- package/dist/index.cjs.js +79 -89
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +80 -90
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/BackgroundFilters/BackgroundFilters.d.ts +7 -0
- package/package.json +9 -6
- package/src/components/BackgroundFilters/BackgroundFilters.tsx +136 -129
|
@@ -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.
|
|
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.
|
|
33
|
-
"@stream-io/video-filters-web": "0.1.
|
|
34
|
-
"@stream-io/video-react-bindings": "0.4.
|
|
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.
|
|
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 &&
|
|
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 {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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 &&
|
|
337
|
+
videoSize.height > videoSize.width &&
|
|
338
|
+
'str-video__background-filters__video--tall',
|
|
297
339
|
)}
|
|
298
|
-
ref={
|
|
299
|
-
autoPlay
|
|
340
|
+
ref={videoRef}
|
|
300
341
|
playsInline
|
|
301
|
-
controls={false}
|
|
302
|
-
width={width}
|
|
303
|
-
height={height}
|
|
304
342
|
muted
|
|
305
|
-
|
|
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={
|
|
350
|
+
ref={bgImageRef}
|
|
313
351
|
src={backgroundImage}
|
|
314
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
};
|