feat(webrtc): add background blur and virtual background options

This commit is contained in:
2026-07-06 13:08:59 +00:00
parent ed408c4044
commit 239640975f
4 changed files with 108 additions and 0 deletions
@@ -102,6 +102,29 @@ export function DeviceSettingsModal({ onClose }: Props) {
))}
</select>
</div>
<div>
<label className="block text-xxs text-gb-fg-s mb-1">Background Effect</label>
<select
value={useVoiceStore((s) => s.bgMode)}
onChange={(e) => {
const mode = e.target.value as 'disabled' | 'background-blur' | 'virtual-background';
if (mode === 'virtual-background') {
const url = prompt('Enter image URL for virtual background:', 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809');
if (url !== null) {
useVoiceStore.getState().setBgMode(mode, url);
}
} else {
useVoiceStore.getState().setBgMode(mode);
}
}}
className="w-full bg-gb-bg-s text-gb-fg border border-gb-bg-t rounded-sm px-2 py-1 text-sm outline-none focus:border-gb-orange"
>
<option value="disabled">None</option>
<option value="background-blur">Blur Background</option>
<option value="virtual-background">Virtual Background (Image URL)</option>
</select>
</div>
</div>
<div className="mt-6 flex justify-end">
+47
View File
@@ -11,6 +11,7 @@ import {
} from 'livekit-client';
import { api } from '../lib/api.ts';
import { useWebSocketStore } from './ws.ts';
import { BackgroundProcessor } from '@livekit/track-processors';
export interface VoiceParticipant {
@@ -42,6 +43,8 @@ interface VoiceState {
isVideoOn: boolean;
isScreenSharing: boolean;
noiseSuppression: boolean;
bgMode: 'disabled' | 'background-blur' | 'virtual-background';
_bgProcessor: any;
participants: VoiceParticipant[];
error: string | null;
_room: Room | null;
@@ -54,6 +57,7 @@ interface VoiceState {
toggleVideo: () => Promise<void>;
toggleScreenShare: () => Promise<void>;
toggleNoiseSuppression: () => void;
setBgMode: (mode: 'disabled' | 'background-blur' | 'virtual-background', imagePath?: string) => Promise<void>;
_addWhisper: (w: WhisperNotification) => void;
whisperTo: (targetUserId: string, targetUsername: string, message?: string) => void;
dismissWhisper: (timestamp: number) => void;
@@ -80,6 +84,8 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
isVideoOn: false,
isScreenSharing: false,
noiseSuppression: true,
bgMode: 'disabled',
_bgProcessor: null,
participants: [],
error: null,
_room: null,
@@ -306,6 +312,47 @@ export const useVoiceStore = create<VoiceState>((set, get) => ({
const newState = !get().isVideoOn;
await room.localParticipant.setCameraEnabled(newState);
set({ isVideoOn: newState });
if (newState && get().bgMode !== 'disabled') {
const mode = get().bgMode;
// re-trigger setBgMode to re-attach processor to the new track
await get().setBgMode(mode);
}
},
setBgMode: async (mode: 'disabled' | 'background-blur' | 'virtual-background', imagePath?: string) => {
const room = get()._room;
if (!room) return;
let processor = get()._bgProcessor;
if (!processor && mode !== 'disabled') {
try {
processor = BackgroundProcessor({ mode: 'background-blur', blurRadius: 10 });
set({ _bgProcessor: processor });
} catch (err) {
console.warn('Failed to initialize BackgroundProcessor:', err);
return;
}
}
if (processor) {
if (mode === 'disabled') {
await processor.switchTo({ mode: 'disabled' });
} else if (mode === 'background-blur') {
await processor.switchTo({ mode: 'background-blur', blurRadius: 10 });
} else if (mode === 'virtual-background') {
await processor.switchTo({ mode: 'virtual-background', imagePath: imagePath || 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809' });
}
// attach to camera track if it's running
const pub = room.localParticipant.getTrackPublication(Track.Source.Camera);
const track = pub?.track as any;
if (track && track.setProcessor) {
await track.setProcessor(processor);
}
}
set({ bgMode: mode });
},
toggleScreenShare: async () => {