Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 55 additions & 1 deletion docs/USER_SETTINGS_CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,44 @@

## Overview

This document describes the implementation of Capabilities for User Settings as part of issue #495. This implementation adds a comprehensive service layer, validation, testing infrastructure, and enhanced capabilities to the User Settings system, following the architectural pattern established by the notification system refactoring.
This document describes the implementation of Capabilities for User Settings as part of issue #495 and Virtual Background support as part of the Backup System enhancement. This implementation adds a comprehensive service layer, validation, testing infrastructure, and enhanced capabilities to the User Settings system, following the architectural pattern established by the notification system refactoring.

## Virtual Background Feature (v3)

### Overview
The virtual background feature allows users to replace their actual background during video calls with various effects:
- **Blur**: Applies a blur effect to the background
- **Image**: Uses a custom image as the background
- **Color**: Uses a solid color as the background
- **None**: Disables virtual background (default)

### Implementation Details

#### Settings Schema (v3)
Added the following fields to `AppSettings`:
- `virtualBackgroundEnabled`: boolean - Master toggle for virtual background
- `virtualBackgroundType`: enum ('none' | 'blur' | 'image' | 'color') - Type of background effect
- `virtualBackgroundImage`: string (max 500 chars) - URL for custom background image
- `virtualBackgroundBlur`: number (0-100) - Blur intensity
- `virtualBackgroundColor`: string (max 7 chars) - Hex color for solid color backgrounds

#### UI Components
- Added Virtual Background section to settings page (`src/pages/settings/index.tsx`)
- Integrated with video conference component (`src/components/collaboration/VideoConference.tsx`)
- Created custom hook for virtual background management (`src/hooks/useVirtualBackground.ts`)

#### Utility Functions
Created `src/utils/virtualBackgroundUtils.ts` with:
- `applyVirtualBackground()`: Applies virtual background effects to video streams
- `settingsToVirtualBackgroundConfig()`: Converts settings to config object
- `isValidImageUrl()`: Validates image URLs
- `isValidHexColor()`: Validates hex color codes
- `isValidBlurIntensity()`: Validates blur intensity values

#### Migration
- Schema version updated from v2 to v3
- Automatic migration from v2 to v3 adds default virtual background settings
- Preserves existing user settings during migration

## Problems Addressed

Expand Down Expand Up @@ -63,6 +100,7 @@ Business logic layer that handles:
- **Reset to Defaults**: `resetToDefaults()` - Reverts to default settings
- **Capabilities**: `getCapabilities()`, `canEditSetting()` - Permission system
- **Migration**: `migrateSettings()` - Schema version migration
- **Virtual Background**: Support for video conference virtual backgrounds (NEW in v3)

Example usage:

Expand Down Expand Up @@ -184,10 +222,14 @@ Service layer has comprehensive unit tests covering:
- Reset to defaults
- Capabilities system
- Migration logic
- Virtual background settings validation (NEW)
- Virtual background migration from v2 to v3 (NEW)

Run unit tests:
```bash
pnpm test src/lib/settings/__tests__/service.test.ts
pnpm test src/utils/__tests__/virtualBackgroundUtils.test.ts
pnpm test src/hooks/__tests__/useVirtualBackground.test.ts
```

### Integration Tests
Expand All @@ -201,6 +243,8 @@ Integration tests verify:
- Capabilities system integration
- Migration integration
- LocalStorage integration
- Virtual background settings export/import (NEW)
- Virtual background settings reset to defaults (NEW)

Run integration tests:
```bash
Expand Down Expand Up @@ -256,6 +300,16 @@ Schema version management:
- User data preservation during migration
- Future-proof schema evolution

### 6. Virtual Background (NEW in v3)

Virtual background support for video conferences:
- Four background types: none, blur, image, color
- Custom image background support with URL validation
- Configurable blur intensity (0-100)
- Solid color background with hex color picker
- Integrated with video conferencing component
- Full backup/restore support via export/import

## Benefits

### For Developers
Expand Down
19 changes: 14 additions & 5 deletions src/components/collaboration/VideoConference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import { Mic, Video, Monitor, Phone, VideoOff, MicOff } from 'lucide-react';
import { io, type Socket } from 'socket.io-client';
import type { CollaborationUser } from '../../hooks/useCollaboration';
import { useVirtualBackground } from '../../hooks/useVirtualBackground';

interface VideoConferenceProps {
roomId: string;
Expand Down Expand Up @@ -43,6 +44,8 @@ export function VideoConference({ roomId, user, websocketUrl }: VideoConferenceP
const [sharingScreen, setSharingScreen] = useState(false);
const [status, setStatus] = useState('Idle');

const virtualBackground = useVirtualBackground();

const signalingUrl =
websocketUrl || process.env.NEXT_PUBLIC_WEBSOCKET_URL || 'http://localhost:3001';

Expand Down Expand Up @@ -94,8 +97,9 @@ export function VideoConference({ roomId, user, websocketUrl }: VideoConferenceP
return () => {
socket.disconnect();
socketRef.current = null;
virtualBackground.stopProcessing();
};
}, [roomId, signalingUrl, user.id, user.name]);
}, [roomId, signalingUrl, user.id, user.name, virtualBackground]);

useEffect(() => {
if (localVideoRef.current) {
Expand Down Expand Up @@ -148,14 +152,18 @@ export function VideoConference({ roomId, user, websocketUrl }: VideoConferenceP
const startLocalStream = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
setLocalStream(stream);
stream.getAudioTracks().forEach((track) => {

// Apply virtual background if enabled
const processedStream = await virtualBackground.applyToStream(stream);

setLocalStream(processedStream);
processedStream.getAudioTracks().forEach((track) => {
track.enabled = microphoneEnabled;
});
stream.getVideoTracks().forEach((track) => {
processedStream.getVideoTracks().forEach((track) => {
track.enabled = cameraEnabled;
});
return stream;
return processedStream;
} catch (error) {
setStatus('Unable to access camera or microphone');
console.error(error);
Expand Down Expand Up @@ -247,6 +255,7 @@ export function VideoConference({ roomId, user, websocketUrl }: VideoConferenceP
};

const endCall = () => {
virtualBackground.stopProcessing();
pcRef.current?.close();
pcRef.current = null;
localStream?.getTracks().forEach((track) => track.stop());
Expand Down
131 changes: 131 additions & 0 deletions src/hooks/__tests__/useVirtualBackground.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* useVirtualBackground Hook - Unit Tests
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useVirtualBackground } from '../useVirtualBackground';
import { useSettingsStore } from '@/lib/settings/store';

// Mock the settings store
vi.mock('@/lib/settings/store');

describe('useVirtualBackground Hook', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns correct configuration when virtual background is disabled', () => {
vi.mocked(useSettingsStore).mockReturnValue({
settings: {
virtualBackgroundEnabled: false,
virtualBackgroundType: 'none',
virtualBackgroundImage: '',
virtualBackgroundBlur: 10,
virtualBackgroundColor: '#000000',
version: 3,
theme: 'system',
language: 'en',
notificationsEnabled: true,
emailNotifications: true,
prefetchingEnabled: true,
reducedMotion: false,
electronicSignatureEnabled: false,
signatureName: '',
requireSignatureOnCertificates: false,
},
} as any);

const { result } = renderHook(() => useVirtualBackground());

expect(result.current.isEnabled).toBe(false);
expect(result.current.config.enabled).toBe(false);
expect(result.current.config.type).toBe('none');
});

it('returns correct configuration when virtual background is enabled with image', () => {
vi.mocked(useSettingsStore).mockReturnValue({
settings: {
virtualBackgroundEnabled: true,
virtualBackgroundType: 'image',
virtualBackgroundImage: 'https://example.com/bg.jpg',
virtualBackgroundBlur: 10,
virtualBackgroundColor: '#000000',
version: 3,
theme: 'system',
language: 'en',
notificationsEnabled: true,
emailNotifications: true,
prefetchingEnabled: true,
reducedMotion: false,
electronicSignatureEnabled: false,
signatureName: '',
requireSignatureOnCertificates: false,
},
} as any);

const { result } = renderHook(() => useVirtualBackground());

expect(result.current.isEnabled).toBe(true);
expect(result.current.config.enabled).toBe(true);
expect(result.current.config.type).toBe('image');
expect(result.current.config.imageUrl).toBe('https://example.com/bg.jpg');
});

it('returns correct configuration when virtual background is enabled with blur', () => {
vi.mocked(useSettingsStore).mockReturnValue({
settings: {
virtualBackgroundEnabled: true,
virtualBackgroundType: 'blur',
virtualBackgroundImage: '',
virtualBackgroundBlur: 25,
virtualBackgroundColor: '#000000',
version: 3,
theme: 'system',
language: 'en',
notificationsEnabled: true,
emailNotifications: true,
prefetchingEnabled: true,
reducedMotion: false,
electronicSignatureEnabled: false,
signatureName: '',
requireSignatureOnCertificates: false,
},
} as any);

const { result } = renderHook(() => useVirtualBackground());

expect(result.current.isEnabled).toBe(true);
expect(result.current.config.enabled).toBe(true);
expect(result.current.config.type).toBe('blur');
expect(result.current.config.blurIntensity).toBe(25);
});

it('returns correct configuration when virtual background is enabled with color', () => {
vi.mocked(useSettingsStore).mockReturnValue({
settings: {
virtualBackgroundEnabled: true,
virtualBackgroundType: 'color',
virtualBackgroundImage: '',
virtualBackgroundBlur: 10,
virtualBackgroundColor: '#FF5733',
version: 3,
theme: 'system',
language: 'en',
notificationsEnabled: true,
emailNotifications: true,
prefetchingEnabled: true,
reducedMotion: false,
electronicSignatureEnabled: false,
signatureName: '',
requireSignatureOnCertificates: false,
},
} as any);

const { result } = renderHook(() => useVirtualBackground());

expect(result.current.isEnabled).toBe(true);
expect(result.current.config.enabled).toBe(true);
expect(result.current.config.type).toBe('color');
expect(result.current.config.backgroundColor).toBe('#FF5733');
});
});
87 changes: 87 additions & 0 deletions src/hooks/useVirtualBackground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* useVirtualBackground Hook
* Manages virtual background functionality for video streams
*/

import { useState, useCallback, useRef } from 'react';
import { useSettingsStore } from '@/lib/settings/store';
import {
applyVirtualBackground,
settingsToVirtualBackgroundConfig,
type VirtualBackgroundConfig,
} from '@/utils/virtualBackgroundUtils';

export function useVirtualBackground() {
const settings = useSettingsStore((s) => s.settings);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const originalStreamRef = useRef<MediaStream | null>(null);
const processedStreamRef = useRef<MediaStream | null>(null);

/**
* Apply virtual background to a media stream
*/
const applyToStream = useCallback(
async (stream: MediaStream): Promise<MediaStream> => {
setIsProcessing(true);
setError(null);

try {
// Store original stream for cleanup
if (!originalStreamRef.current) {
originalStreamRef.current = stream;
}

const config = settingsToVirtualBackgroundConfig(settings);

if (!config.enabled || config.type === 'none') {
return stream;
}

const processedStream = await applyVirtualBackground(stream, config);
processedStreamRef.current = processedStream;

return processedStream;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to apply virtual background';
setError(errorMessage);
console.error('Virtual background error:', err);
return stream;
} finally {
setIsProcessing(false);
}
},
[settings],
);

/**
* Stop processing and return to original stream
*/
const stopProcessing = useCallback(() => {
if (processedStreamRef.current) {
processedStreamRef.current.getTracks().forEach((track) => track.stop());
processedStreamRef.current = null;
}
setIsProcessing(false);
setError(null);
}, []);

/**
* Check if virtual background is currently enabled
*/
const isEnabled = settings.virtualBackgroundEnabled && settings.virtualBackgroundType !== 'none';

/**
* Get current virtual background configuration
*/
const config = settingsToVirtualBackgroundConfig(settings);

return {
applyToStream,
stopProcessing,
isProcessing,
error,
isEnabled,
config,
};
}
Loading