A reusable React hook and utility package for real-time webcam background switching and blurring using MediaPipe Selfie Segmentation. This package provides the core logic and compositing, allowing you to build your own custom UI in React, Next.js, or any modern frontend framework.
- Real-time person segmentation using MediaPipe
- Blur or replace webcam background with custom images
- No UI imposed – you control the look and feel
- Exposes hooks and methods for full flexibility
- TypeScript support
- Error handling and state management
npm install BackdropEngine
# or
yarn add BackdropEngineYou can import the hook using either a named or default import:
import React, { useMemo } from 'react';
import { useWebcamBackgroundSwitcher } from "backdrop-engine";
const backgrounds = [
{ label: "None", type: "none" },
{ label: "Blur", type: "blur" },
{ label: "Beach", type: "image", src: "/beach.jpg" },
{ label: "Mountains", type: "image", src: "/mountains.jpg" },
];
function MyCustomUI() {
// IMPORTANT: Use useMemo to prevent infinite re-renders
const options = useMemo(() => ({
backgrounds,
width: 640,
height: 480,
onError: (err) => console.error(err),
frameSkip: 2,
modelSelection: 0, // 0: fast, 1: better
mirror: true,
blurRadius: 10,
cdnUrl: undefined, // optional custom CDN
debug: false, // enable debug logging
}), []);
const {
canvasRef,
videoRef,
setBackground,
setModel,
setMirror,
setBlurRadius,
status,
error,
currentBackground,
availableBackgrounds,
modelSelection,
mirror,
blurRadius,
} = useWebcamBackgroundSwitcher(options);
return (
<div>
{/* Your custom controls */}
{availableBackgrounds.map((bg) => (
<button key={bg.option.label} onClick={() => setBackground(bg)}>
{bg.option.label}
</button>
))}
{/* Attach refs to your elements */}
<video ref={videoRef} style={{ display: "none" }} autoPlay muted playsInline />
<canvas ref={canvasRef} width={640} height={480} />
{status === "error" && <div>Error: {error?.message}</div>}
</div>
);
}For SSR safety, ensure the hook/component is only used on the client:
import dynamic from "next/dynamic";
const MyCustomUI = dynamic(() => import("./MyCustomUI"), { ssr: false });options object should be memoized with useMemo to prevent infinite re-renders. See the example above and troubleshooting section for details.
| Option | Type | Description |
|---|---|---|
| backgrounds | array | List of background options (see below) |
| width | number | Video/canvas width (default: 640) |
| height | number | Video/canvas height (default: 480) |
| onError | function | Callback for errors (webcam, image, segmentation) |
| defaultMode | string | Initial background mode (label of background, e.g. 'blur' or image label) |
| frameSkip | number | Process every Nth frame (default: 1, no skipping) |
| modelSelection | 0 | 1 | MediaPipe model: 0 = fast/landscape, 1 = better/selfie (default: 0) |
| mirror | boolean | Mirror the output horizontally (default: true) |
| blurRadius | number | Blur radius for 'blur' mode (default: 10) |
| cdnUrl | string | Custom CDN URL for MediaPipe scripts (default: official CDN) |
| debug | boolean | Enable debug logging in console (default: false) |
| Value | Type | Description |
|---|---|---|
| canvasRef | ref | Attach to your <canvas> element |
| videoRef | ref | Attach to your <video> element |
| setBackground | function | Switch background (pass a LoadedBackground object) |
| setModel | function | Switch MediaPipe model (0 or 1) |
| setMirror | function | Enable/disable mirroring |
| setBlurRadius | function | Set blur radius for 'blur' mode |
| status | string | 'loading', 'ready', 'error' |
| error | object | Error object if any |
| currentBackground | LoadedBackground | The currently active background |
| availableBackgrounds | LoadedBackground[] | List of all available backgrounds |
| modelSelection | 0 | 1 | Current MediaPipe model selection |
| mirror | boolean | Current mirror mode |
| blurRadius | number | Current blur radius |
{ label: 'Blur', type: 'blur' }
{ label: 'Beach', type: 'image', src: '/beach.jpg' }A LoadedBackground object has the following shape:
{
option: { label: string; type: 'none' | 'blur' | 'image'; src?: string };
image?: HTMLImageElement; // present if type is 'image'
isReady: boolean;
error?: Error;
}Use the option.label as a unique key for UI purposes. Pass the entire LoadedBackground object to setBackground.
- Requests webcam access and displays the video feed (hidden or shown as you wish).
- Uses MediaPipe Selfie Segmentation to separate the person from the background in real time.
- Allows you to blur the background or replace it with a custom image.
- Renders the composited output to a canvas you control.
- You build and style your own UI and controls.
- You are responsible for making your UI accessible and responsive.
- The hook provides only the core logic and refs.
- If webcam access fails, the hook sets
statusto 'error' and provides an error object. - If a background image fails to load, an error is provided.
- All errors are passed to the
onErrorcallback if provided.
- Custom Controls: Build any UI you want for background selection, loading states, etc.
- Custom Background Upload: Let users upload their own images and add to the backgrounds array.
- Video Backgrounds: Use a video element as a background (advanced, see docs).
This package supports the following background modes:
- Description: The original webcam feed is shown with no background effect.
- Use Case: For users who want to disable all effects and show the raw camera.
- Description: The background behind the person is blurred in real time, preserving privacy and focus.
- Performance: Blurring is efficient, especially when combined with frame skipping and the fast segmentation model.
- Use Case: Virtual meetings, privacy, focus enhancement.
- Description: The background is replaced with a custom image of your choice.
- Performance: Slightly more resource-intensive than blur, especially with high-resolution images, but still efficient with frame skipping and model selection.
- Use Case: Virtual backgrounds, branding, fun effects.
To maximize real-time performance and efficiency, consider implementing or enabling the following features in your UI and logic:
- Description: Process only every Nth frame for segmentation (e.g., every 2nd frame) using the
frameSkipoption. - Benefit: Reduces CPU/GPU load and segmentation calls, increasing FPS and lowering resource usage.
- How to Use: Set
frameSkipin your hook options. For example,frameSkip: 2will process every other frame.
- Description: Allow users to choose between a fast (less accurate) and a better (slower, more accurate) segmentation model. This applies to all modes (blur, image, and video).
- Benefit: Lets users balance quality and performance for their device.
- How to Use: Expose a
modelSelectionoption in your API and pass it to MediaPipe Selfie Segmentation.
- Description: Set the canvas size to match the video's actual resolution.
- Benefit: Prevents unnecessary up/downscaling, saving processing time and memory.
- How to Use: On video metadata load, set canvas width/height to match video.
- Description: Reuse a single offscreen (temporary) canvas for compositing.
- Benefit: Reduces memory and CPU churn from repeated DOM allocations.
- How to Use: Create and reuse a single offscreen canvas for all compositing steps.
Tip: Mirroring the output is a minor performance cost and can be toggled as needed.
By incorporating these features and understanding the available modes, you can ensure your implementation is efficient and suitable for a wide range of devices.
- Clone the repo
- Install dependencies:
npm install - Run the example app:
npm startornpm run storybook - Build the package:
npm run build
Pull requests and issues are welcome!
This project is dual-licensed under the Unlicense and MIT licenses.
You may use this code under the terms of either license.
By default, this package loads MediaPipe Selfie Segmentation from the official CDN:
https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation
If you want to use a different CDN or self-hosted assets (for example, if the default CDN is unavailable), you can provide a custom cdnUrl in the loader options:
import { MediaPipeLoader, DEFAULT_MEDIAPIPE_CDN } from './src/mediapipe/loader';
const loader = new MediaPipeLoader({
// cdnUrl is optional; defaults to DEFAULT_MEDIAPIPE_CDN
cdnUrl: 'https://your.custom.cdn/path/to/selfie_segmentation',
modelSelection: 0,
});If cdnUrl is not provided, the loader will use the default CDN automatically.
For robust cross-browser playback, always:
- Set
autoPlay,muted, andplaysInlineon your<video>element. - Explicitly call
.play()on the video element after settingsrcObjectin your code. This ensures the video is playing and frames are available for compositing, even if the video is hidden or autoplay is blocked by the browser.
Example:
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.play().catch(() => {});
}This is more robust than relying on autoplay alone, especially in React or SPA environments.
For optimal real-time performance, this package uses the MediaPipe Camera utility to synchronize video capture, segmentation, and compositing—just like the official MediaPipe HTML demos.
- How it works:
- The hook dynamically loads the
camera_utils.jsscript from the CDN at runtime ifwindow.Camerais not present. - Once loaded, it uses
window.Camerato drive the segmentation and compositing pipeline. - This ensures smooth, low-latency output, even on lower-end devices, and matches the performance of the HTML demo.
- The hook dynamically loads the
- Why dynamic loading?
- The npm package for
@mediapipe/camera_utilsdoes not export a usable ES module constructor forCamera. - Loading the script from CDN and using
window.Camerais the most robust and cross-platform solution for React/SPA/SSR environments.
- The npm package for
- You do not need to import Camera in your code. The hook handles everything for you.
If you want to use the Camera utility yourself:
// This is handled internally, but for reference:
if (!window.Camera) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js';
script.async = true;
document.head.appendChild(script);
}
// Then use window.Camera as a constructor- If your canvas is blank or black:
- Ensure your video element is playing (see above).
- Make sure you set
autoPlay,muted, andplaysInlineon the video element. - Always call
.play()after settingsrcObject. - The video can be hidden (
display: none), but it must be playing. - Check the browser console for warnings about video playback or permissions.
- If you see errors about
Cameranot being found, ensure you are not importing from@mediapipe/camera_utilsand let the hook dynamically load the script as described above.
If you encounter a "Maximum update depth exceeded" error, this is caused by the options object being recreated on every render. To fix this:
-
Use
useMemoto memoize the options object:import React, { useMemo } from 'react'; function MyComponent() { const options = useMemo(() => ({ backgrounds, width: 640, height: 480, debug: false, // ... other options }), []); // Empty dependency array for stable options const { canvasRef, videoRef, setBackground } = useWebcamBackgroundSwitcher(options); // ... }
-
Why this happens: React hooks compare dependencies by reference. If the options object is recreated on every render, the hook's useEffect dependencies change, causing infinite re-renders.
-
Best practice: Always memoize the options object when using this hook to prevent performance issues and infinite loops.
You can let users upload their own images and use them as backgrounds. Simply create a Blob URL from the uploaded file and pass it as the src in your background option.
// HTML:
// <input type="file" id="bgUpload" />
document.getElementById('bgUpload').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
// Add to your backgrounds array
backgrounds.push({ label: file.name, type: 'image', src: url });
// Update your UI and re-render as needed
}
});import React, { useRef } from 'react';
import { useWebcamBackgroundSwitcher } from 'backdrop-engine';
function MyComponent() {
const fileInputRef = useRef();
const [backgrounds, setBackgrounds] = React.useState([
{ label: 'Blur', type: 'blur' },
// ...other backgrounds
]);
const { setBackground, ...rest } = useWebcamBackgroundSwitcher({ backgrounds });
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
const bg = { label: file.name, type: 'image', src: url };
setBackgrounds((prev) => [...prev, bg]);
setBackground(bg);
}
};
return (
<div>
<input type="file" ref={fileInputRef} onChange={handleFileChange} />
{/* ...rest of your UI */}
</div>
);
}Note: The package will automatically load the image from the Blob URL and use it as a background.
BackdropEngine exports all its types for TypeScript users. You can import them directly from the package:
import type {
UseWebcamBackgroundSwitcherOptions,
BackgroundOption,
LoadedBackground
} from 'backdrop-engine';This allows you to type your options, backgrounds, and more for full type safety.
If you have questions, suggestions, or encounter any issues, please open an issue or start a discussion on the repository. Contributions and feedback are always welcome!
Thank you for using BackdropEngine.