import {
BackgroundFiltersProvider,
Call,
StreamCall,
StreamVideo,
StreamVideoClient,
} from "@stream-io/video-react-sdk";
export default function App() {
let client: StreamVideoClient; /* = ... */
let call: Call; /* = ... */
return (
<StreamVideo client={client}>
<StreamCall call={call}>
<BackgroundFiltersProvider
backgroundFilter="blur" // initial filter
backgroundImages={[
"https://my-domain.com/bg/random-bg-1.jpg",
"https://my-domain.com/bg/random-bg-2.jpg",
]}
>
<MyUILayout />
<MyBackgroundFilterSettings /> {/* your settings UI */}
</BackgroundFiltersProvider>
</StreamCall>
</StreamVideo>
);
}Video & Audio filters
Apply background blur and replacement filters using BackgroundFiltersProvider and useBackgroundFilters().
Best Practices
- Check
isSupportedbefore showing filter controls. - Handle
onErrorcallback - filters auto-disable on failure. - Monitor
performance.degradedto warn users about CPU issues. - Keep audio in parent window when using Document PiP to avoid autoplay restrictions.
- Safari support is experimental - enable with
forceSafariSupport={true}.
Supported on Chrome 124+, Firefox 124+, Edge 124+. Mobile support coming soon.
Video Filters
Step 1 - Enable and initialize the filters
Background filters are provided through the following APIs and components:
<BackgroundFiltersProvider />- a React context provider that will allow you to use the filters API in your applicationuseBackgroundFilters()- a React hook that will allow you to access the filters API in your application
A basic integration looks like this:
Step 2 - Use the API to control the filters
Once you have the BackgroundFiltersProvider rendered in your application,
you can use the useBackgroundFilters() hook to access the filters API and control the behavior of the filters.
import { useBackgroundFilters } from "@stream-io/video-react-sdk";
export const MyBackgroundFilterSettings = () => {
const {
isSupported, // checks if these filters can run on this device
isReady, // checks if the filters are ready to be enabled
isLoading, // indicates that the filters are being loaded
disableBackgroundFilter, // disables the filter
applyBackgroundBlurFilter, // applies the blur filter
applyBackgroundImageFilter, // applies the image filter
backgroundImages, // list of available images
} = useBackgroundFilters();
if (!isSupported) {
return <div>Background filters are not supported on this device</div>;
}
if (!isReady || isLoading) {
return <div className="my-loading-indicator" />;
}
return (
<div className="my-video-filters">
<button onClick={disableBackgroundFilter}>Disable</button>
<button onClick={() => applyBackgroundBlurFilter("high")}>Blur</button>
<button onClick={() => applyBackgroundBlurFilter("medium")}>Blur</button>
<button onClick={() => applyBackgroundBlurFilter("low")}>Blur</button>
<ul>
{backgroundImages.map((image) => (
<li key={image}>
<img src={image} alt="background" />
<button onClick={() => applyBackgroundImageFilter(image)}>
Apply background
</button>
</li>
))}
</ul>
</div>
);
};applyBackgroundBlurFilter also takes a more fine-grained numeric argument, that specifies the strength of the applied Gaussian blur filter: applyBackgroundBlurFilter(5.1). Note that higher values make the filter more resource intensive.
Step 3 - Handle errors
The performance of the video filter depends on the capabilities of the user device. Even on supported devices, the filter may not work properly, for example, if the device is resource-constrained.
By default, the malfunctioning filter is disabled. You can add the onError callback to handle such situations. Use it to display an error notification, to disable the camera completely, etc.
import { BackgroundFiltersProvider } from "@stream-io/video-react-sdk";
const call = useCall();
<BackgroundFiltersProvider
backgroundFilter="blur"
onError={(error) => {
console.error(
"Blur filter encountered an error, camera will be disabled",
error,
);
call?.camera.disable();
}}
>
{/* ... */}
</BackgroundFiltersProvider>;Performance metrics
Background filters run real-time segmentation and may be demanding on some devices. To help you surface performance issues to your users, the SDK exposes a built-in performance monitor.
const { performance } = useBackgroundFilters();
if (performance.degraded) {
console.log("Background filters degraded:", performance?.reason);
}
return (
<>
{performance.degraded && (
<div className="warning-banner">
Consider disabling background filters for optimal performance.
</div>
)}
{/* ... */}
</>
);Degradation reasons include:
frame-drop- background filters are causing drop in frame ratecpu-throttling- drop in performance due to CPU throttling
The provider uses the camera's source frames as the baseline and calculates a smoothed processed to source frames ratio using an Exponential Moving Average (EMA). This avoids temporary spikes and keeps degradation detection stable and non-flickery.
Self-hosting MediaPipe assets (CSP compatibility)
By default, the background blur and replacement filters load their ML model and WebAssembly files at runtime from the unpkg.com CDN. The default basePath resolves to:
https://unpkg.com/@stream-io/video-filters-web@<version>/mediapipeThis works out of the box, but it can be a problem if your application enforces a strict Content Security Policy (CSP). You would have to allowlist unpkg.com in your connect-src (and related) directives, or - preferably - avoid the third-party dependency altogether by hosting the assets on your own domain and pointing the SDK to them.
Step 1 - Copy the assets into your app
The assets ship inside the @stream-io/video-filters-web package, in its mediapipe/ folder:
mediapipe/
├── models/ # .tflite segmentation models
└── wasm/ # MediaPipe vision WebAssembly + JS glue codeCopy that folder into your app's public/static assets directory, preserving the structure:
rm -rf public/mediapipe
cp -R node_modules/@stream-io/video-filters-web/mediapipe public/mediapipeThe rm -rf keeps the command idempotent: without it, re-running cp -R nests the source under the existing folder (public/mediapipe/mediapipe/...) and leaves stale files behind.
Step 2 - Override basePath
Set the basePath prop on <BackgroundFiltersProvider> to the location that serves your copied mediapipe folder (the directory that contains models/ and wasm/). Note that the public/ directory is served from the web root, so files in public/mediapipe are exposed at /mediapipe (no /public prefix):
<BackgroundFiltersProvider
basePath="https://my.domain.com/mediapipe"
// ...existing config
>
{/* ... */}
</BackgroundFiltersProvider>A same-origin path works too (e.g. basePath="/mediapipe"), which keeps everything within your own 'self' CSP directive.
Keep the assets in sync with the SDK
The hosted assets must match the installed SDK version - mismatched models, WASM, and SDK code can be incompatible. To prevent version drift, copy the folder automatically on install instead of committing a one-off copy. Add a postinstall script (or a step in your build) to your package.json.
{
"scripts": {
"postinstall": "node -e \"require('fs').cpSync('node_modules/@stream-io/video-filters-web/mediapipe', 'public/mediapipe', { recursive: true })\""
}
}Every install then refreshes public/mediapipe from whatever SDK version is installed.
If you opt into the legacy filter (useLegacyFilter), it loads a TensorFlow Lite model and WASM from a separate tf/ folder in the same package (default https://unpkg.com/@stream-io/video-filters-web@<version>/tf). The same basePath prop is reused, so to self-host the legacy filter, copy node_modules/@stream-io/video-filters-web/tf and point basePath at it instead - or use the tfFilePath and modelFilePath props for fine-grained control.
Once the assets are self-hosted and basePath is set, the app no longer makes requests to unpkg.com, so you can remove it from your CSP allowlist.
Legacy API
For compatibility reasons, we still provide the original background filter implementation. This uses our previous segmentation model and pipeline instead of the new MediaPipe-based implementation.
You can opt-in by enabling the useLegacyFilter flag:
import { BackgroundFiltersProvider } from "@stream-io/video-react-sdk";
const call = useCall();
<BackgroundFiltersProvider useLegacyFilter>
{/* ... */}
</BackgroundFiltersProvider>;Caveats
Safari
By default, Safari is excluded from the supported browsers for video filters. The reason is the fact that Safari applies aggressive timer throttling whenever the tab goes to the background. This causes the video frame rate to drastically go down or appear completely frozen.
However, we expose a way to still enable the filters on Safari 17.4+, knowing that they may not work as expected in every case:
import { BackgroundFiltersProvider } from "@stream-io/video-react-sdk";
<BackgroundFiltersProvider
forceSafariSupport={true} // enable Safari support
{...otherProps}
>
{/* ... */}
</BackgroundFiltersProvider>;Audio Filters
Check the microphone.registerFilter() API to register custom audio filters.