Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add background blur feature for supported devices #2812

Draft
wants to merge 37 commits into
base: livekit
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c798c79
Add background Blur
toger5 Nov 20, 2024
140fe48
make compatible with unsopported browsers
toger5 Nov 20, 2024
70ff435
add types
toger5 Nov 21, 2024
d23beef
remove unused dependencies.
toger5 Nov 27, 2024
95bc7db
update blur checkbox (disabled state)
toger5 Nov 27, 2024
0be312a
i18n
toger5 Nov 27, 2024
aade8f9
eslint + prettier
toger5 Nov 27, 2024
ae71c6e
add @types/dom-mediacapture-transform for the remaining type errors
toger5 Nov 27, 2024
a6745ef
properly add @livekit/track-processors
toger5 Nov 27, 2024
c2b6518
dont update if processor already set
toger5 Nov 27, 2024
7d8e9a7
fixes and gpu based blurring
toger5 Nov 27, 2024
9eae919
make default false
toger5 Nov 29, 2024
d179db1
more detailed error message
toger5 Dec 2, 2024
95c3ca8
review
toger5 Dec 2, 2024
c6d4844
review
toger5 Dec 2, 2024
574c895
disable blur in local track again
toger5 Dec 2, 2024
b77c4af
refactor
toger5 Dec 6, 2024
59bc73c
Use local assets for blurring
hughns Dec 9, 2024
1860646
Lint
hughns Dec 9, 2024
aa5cf2f
Wording change
hughns Dec 9, 2024
b6cc9c5
Lint
hughns Dec 9, 2024
5ead61d
Merge branch 'livekit' into toger5/track-processor-blur
hughns Dec 11, 2024
c860be7
.
hughns Dec 11, 2024
f18ce5a
Format
hughns Dec 11, 2024
843f081
Merge branch 'livekit' into toger5/track-processor-blur
toger5 Dec 13, 2024
3f48810
bump components-js (not yet working)
toger5 Dec 13, 2024
6accf08
Merge branch 'livekit' into toger5/track-processor-blur
hughns Dec 18, 2024
7983adb
Lint
hughns Dec 18, 2024
7c985b0
Revert "Bump matrix-js-sdk for bundle improvements (#2911)"
toger5 Dec 19, 2024
e0f4bf8
working version
toger5 Dec 19, 2024
77ff358
Merge branch 'livekit' into toger5/track-processor-blur
hughns Jan 6, 2025
bb0e1f3
Exclude tasks-vision from knip
hughns Jan 6, 2025
96ea920
Try reenabling local implementation of BlurBackgroundTransformer
hughns Jan 6, 2025
3811074
Merge branch 'livekit' into toger5/track-processor-blur
hughns Jan 6, 2025
ba2e7a7
Merge branch 'livekit' into toger5/track-processor-blur
hughns Jan 8, 2025
4c4d012
Fix @matrix-org/matrix-sdk-crypto-wasm version
hughns Jan 8, 2025
00d724a
Bump @livekit/track-processors to ignore empty frames
hughns Jan 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export default {
// then Knip will flag it as a false positive
// https://github.com/webpro-nl/knip/issues/766
"@vector-im/compound-web",
// We need this so the eslint is happy with @livekit/track-processors.
// This might be a bug in the livekit repo but for now we fix it on the
// element call side.
"@types/dom-mediacapture-transform",
"matrix-widget-api",
],
ignoreExportsUsedInFile: true,
Expand Down
3 changes: 3 additions & 0 deletions locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
"effect_volume_description": "Volume for sound effects such as: joining or leaving a call, and reactions.",
"effect_volume_label": "Sound effect volume"
},
"background_blur_header": "Background",
"background_blur_label": "Blur the background of the video",
"blur_not_supported_by_browser": "(Background blur is not supported by this device)",
"developer_settings_label": "Developer Settings",
"developer_settings_label_description": "Expose developer settings in the settings window.",
"developer_tab_title": "Developer",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@formatjs/intl-segmenter": "^11.7.3",
"@livekit/components-core": "^0.11.0",
"@livekit/components-react": "^2.0.0",
"@livekit/track-processors": "^0.3.2",
"@mediapipe/tasks-vision": "^0.10.18",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/core": "^1.25.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0",
Expand All @@ -50,6 +52,7 @@
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1",
"@types/content-type": "^1.1.5",
"@types/dom-mediacapture-transform": "^0.1.10",
"@types/grecaptcha": "^3.0.9",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12",
Expand Down
54 changes: 33 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Initializer } from "./initializer";
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
import { widget } from "./widget";
import { useTheme } from "./useTheme";
import { ProcessorProvider } from "./livekit/TrackProcessorContext";

const SentryRoute = Sentry.withSentryRouting(Route);

Expand Down Expand Up @@ -82,27 +83,25 @@ export const App: FC<AppProps> = ({ history }) => {
<TooltipProvider>
{loaded ? (
<Suspense fallback={null}>
<ClientProvider>
<MediaDevicesProvider>
<Sentry.ErrorBoundary fallback={errorPage}>
<DisconnectedBanner />
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="*">
<RoomPage />
</SentryRoute>
</Switch>
</Sentry.ErrorBoundary>
</MediaDevicesProvider>
</ClientProvider>
<Providers>
<Sentry.ErrorBoundary fallback={errorPage}>
<DisconnectedBanner />
<Switch>
<SentryRoute exact path="/">
<HomePage />
</SentryRoute>
<SentryRoute exact path="/login">
<LoginPage />
</SentryRoute>
<SentryRoute exact path="/register">
<RegisterPage />
</SentryRoute>
<SentryRoute path="*">
<RoomPage />
</SentryRoute>
</Switch>
</Sentry.ErrorBoundary>
</Providers>
</Suspense>
) : (
<LoadingView />
Expand All @@ -113,3 +112,16 @@ export const App: FC<AppProps> = ({ history }) => {
</Router>
);
};

const Providers: FC<{
children: JSX.Element;
}> = ({ children }) => {
// We use this to stack all used providers to not make the App component to verbose
return (
<ClientProvider>
<MediaDevicesProvider>
<ProcessorProvider>{children}</ProcessorProvider>
</MediaDevicesProvider>
</ClientProvider>
);
};
62 changes: 62 additions & 0 deletions src/livekit/BlurBackgroundTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
Copyright 2024 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import {
BackgroundTransformer,
VideoTransformer,
VideoTransformerInitOptions,
} from "@livekit/track-processors";
import { ImageSegmenter } from "@mediapipe/tasks-vision";

interface WasmFileset {
/** The path to the Wasm loader script. */
wasmLoaderPath: string;
/** The path to the Wasm binary. */
wasmBinaryPath: string;
}

// n.b. this only includes the SIMD versions of the WASM files which have good support:
// https://caniuse.com/?search=simd
const wasmFileset: WasmFileset = {
wasmLoaderPath: new URL(
"../../node_modules/@mediapipe/tasks-vision/wasm/vision_wasm_internal.js",
hughns marked this conversation as resolved.
Show resolved Hide resolved
import.meta.url,
).href,
wasmBinaryPath: new URL(
"../../node_modules/@mediapipe/tasks-vision/wasm/vision_wasm_internal.wasm",
import.meta.url,
).href,
};

const modelAssetPath = new URL(
"../mediapipe/imageSegmenter/selfie_segmenter.tflite",
import.meta.url,
).href;

export class BlurBackgroundTransformer extends BackgroundTransformer {
public async init({
outputCanvas,
inputElement: inputVideo,
}: VideoTransformerInitOptions): Promise<void> {
// call super.super.init()
await VideoTransformer.prototype.init.call(this, {
outputCanvas,
inputElement: inputVideo,
});

this.imageSegmenter = await ImageSegmenter.createFromOptions(wasmFileset, {
baseOptions: {
modelAssetPath,
delegate: "GPU",
...this.options.segmenterOptions,
},
runningMode: "VIDEO",
outputCategoryMask: true,
outputConfidenceMasks: false,
});
}
}
113 changes: 113 additions & 0 deletions src/livekit/TrackProcessorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
Copyright 2024 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { BackgroundOptions, ProcessorWrapper } from "@livekit/track-processors";
import {
createContext,
FC,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { LocalVideoTrack } from "livekit-client";

import {
backgroundBlur as backgroundBlurSettings,
useSetting,
} from "../settings/settings";
import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer";

type ProcessorState = {
supported: boolean | undefined;
processor: undefined | ProcessorWrapper<BackgroundOptions>;
/**
* Call this method to try to initialize a processor.
* This only needs to happen if supported is undefined.
* If the backgroundBlur setting is set to true this does not need to be called
* and the processorState.supported will update automatically to the correct value.
*/
checkSupported: () => void;
};
const ProcessorContext = createContext<ProcessorState | undefined>(undefined);

export const useTrackProcessor = (): ProcessorState | undefined =>
useContext(ProcessorContext);

export const useTrackProcessorSync = (
videoTrack: LocalVideoTrack | null,
): void => {
const { processor } = useTrackProcessor() || {};
useEffect(() => {
if (processor && !videoTrack?.getProcessor()) {
void videoTrack?.setProcessor(processor);
}
if (!processor && videoTrack?.getProcessor()) {
void videoTrack?.stopProcessor();
}
}, [processor, videoTrack]);
};

interface Props {
children: JSX.Element;
}
export const ProcessorProvider: FC<Props> = ({ children }) => {
// The setting the user wants to have
const [blurActivated] = useSetting(backgroundBlurSettings);

// If `ProcessorState.supported` is undefined the user can activate that we want
// to have it at least checked (this is useful to show the settings menu properly)
// We dont want to try initializing the blur if the user is not even looking at the setting
const [shouldCheckSupport, setShouldCheckSupport] = useState(blurActivated);

// Cache the processor so we only need to initialize it once.
const blur = useRef<ProcessorWrapper<BackgroundOptions> | undefined>(
undefined,
);

const checkSupported = useCallback(() => {
setShouldCheckSupport(true);
}, []);
// This is the actual state exposed through the context
const [processorState, setProcessorState] = useState<ProcessorState>(() => ({
supported: false,
processor: undefined,
checkSupported,
}));

useEffect(() => {
if (!shouldCheckSupport) return;
try {
if (!blur.current) {
blur.current = new ProcessorWrapper(
new BlurBackgroundTransformer({ blurRadius: 15 }),
"background-blur",
);
}
setProcessorState({
checkSupported,
supported: true,
processor: blurActivated ? blur.current : undefined,
});
} catch (e) {
setProcessorState({
checkSupported,
supported: false,
processor: undefined,
});
logger.error("disable background blur", e);
}
}, [blurActivated, checkSupported, shouldCheckSupport]);

return (
<ProcessorContext.Provider value={processorState}>
{children}
</ProcessorContext.Provider>
);
};
22 changes: 21 additions & 1 deletion src/livekit/useLiveKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ConnectionState,
E2EEOptions,
ExternalE2EEKeyProvider,
LocalVideoTrack,
Room,
RoomOptions,
Track,
Expand All @@ -33,6 +34,11 @@ import {
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
import { E2eeType } from "../e2ee/e2eeType";
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
import {
useTrackProcessor,
useTrackProcessorSync,
} from "./TrackProcessorContext";
import { useInitial } from "../useInitial";

interface UseLivekitResult {
livekitRoom?: Room;
Expand Down Expand Up @@ -79,12 +85,15 @@ export function useLiveKit(
const devices = useMediaDevices();
const initialDevices = useRef<MediaDevices>(devices);

const { processor } = useTrackProcessor() || {};
const initialProcessor = useInitial(() => processor);
const roomOptions = useMemo(
(): RoomOptions => ({
...defaultLiveKitOptions,
videoCaptureDefaults: {
...defaultLiveKitOptions.videoCaptureDefaults,
deviceId: initialDevices.current.videoInput.selectedId,
processor: initialProcessor,
},
audioCaptureDefaults: {
...defaultLiveKitOptions.audioCaptureDefaults,
Expand All @@ -95,7 +104,7 @@ export function useLiveKit(
},
e2ee: e2eeOptions,
}),
[e2eeOptions],
[e2eeOptions, initialProcessor],
);

// Store if audio/video are currently updating. If to prohibit unnecessary calls
Expand All @@ -120,6 +129,15 @@ export function useLiveKit(
return r;
}, [roomOptions, e2eeSystem]);

const videoTrack = useMemo(
() =>
Array.from(room.localParticipant.videoTrackPublications.values()).find(
(v) => v.source === Track.Source.Camera,
)?.track as LocalVideoTrack | null,
[room.localParticipant.videoTrackPublications],
);
useTrackProcessorSync(videoTrack);

const connectionState = useECConnectionState(
{
deviceId: initialDevices.current.audioInput.selectedId,
Expand Down Expand Up @@ -195,13 +213,15 @@ export function useLiveKit(
audioMuteUpdating.current = true;
trackPublication = await participant.setMicrophoneEnabled(
buttonEnabled.current.audio,
room.options.audioCaptureDefaults,
);
audioMuteUpdating.current = false;
break;
case MuteDevice.Camera:
videoMuteUpdating.current = true;
trackPublication = await participant.setCameraEnabled(
buttonEnabled.current.video,
room.options.videoCaptureDefaults,
);
videoMuteUpdating.current = false;
break;
Expand Down
5 changes: 5 additions & 0 deletions src/mediapipe/imageSegmenter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Google AI Edge MediaPipe Selfie Segmentation

- See: https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter
- Latest: https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite
- License: Apache 2.0 as per https://storage.googleapis.com/mediapipe-assets/Model%20Card%20MediaPipe%20Selfie%20Segmentation.pdf
Binary file not shown.
Loading
Loading