Skip to content

Commit 059a159

Browse files
authored
VIDSOL-105: Background Replacement Support for Web VERA - Custom Images (#201)
1 parent 0404b36 commit 059a159

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1776
-640
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@ This application provides features for common conferencing use cases, such as:
6060
<summary>Input and output device selectors.</summary>
6161
<img src="docs/assets/DeviceSelector.png" alt="Screenshot of audio devices selector">
6262
</details>
63-
- Background blur and noise suppression toggles.
63+
- <details>
64+
<summary>Noise suppression toggles in meeting room</summary>
65+
<img src="docs/assets/NoiseSupression.png" alt="Screenshot of noise supression toggle">
66+
</details>
67+
- <details>
68+
<summary>Background effects in meeting and waiting room. You can set predefined images, custom image or slight/strong background blur. Images can be uploaded from local device or URL in these formats: JPG, PNG, GIF or BMP.</summary>
69+
<img src="docs/assets/BGEffects.png" alt="Screenshot of background effects">
70+
</details>
6471
- <details>
6572
<summary>Composed archiving capabilities to record your meetings.</summary>
6673
<img src="docs/assets/Archiving.png" alt="Screenshot of archiving dialog box">

docs/assets/BGEffects.png

4.63 MB
Loading

docs/assets/NoiseSupression.png

280 KB
Loading

frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffect.spec.tsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

frontend/src/components/BackgroundEffects/AddBackgroundEffect/AddBackgroundEffect.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import { vi, describe, it, expect, beforeAll } from 'vitest';
3+
import AddBackgroundEffectLayout from './AddBackgroundEffectLayout';
4+
5+
vi.mock('../../../../utils/useImageStorage/useImageStorage', () => ({
6+
__esModule: true,
7+
default: () => ({
8+
storageError: '',
9+
handleImageFromFile: vi.fn(async () => ({
10+
dataUrl: 'data:image/png;base64,MOCKED',
11+
})),
12+
handleImageFromLink: vi.fn(async () => ({
13+
dataUrl: 'data:image/png;base64,MOCKED_LINK',
14+
})),
15+
}),
16+
}));
17+
18+
describe('AddBackgroundEffectLayout', () => {
19+
const cb = vi.fn();
20+
21+
beforeAll(() => {
22+
vi.clearAllMocks();
23+
});
24+
25+
it('should render', () => {
26+
render(<AddBackgroundEffectLayout customBackgroundImageChange={vi.fn()} />);
27+
expect(screen.getByText(/Drag and drop, or click here to upload image/i)).toBeInTheDocument();
28+
expect(screen.getByPlaceholderText(/Link from the web/i)).toBeInTheDocument();
29+
expect(screen.getByTestId('background-effect-link-submit-button')).toBeInTheDocument();
30+
});
31+
32+
it('shows error for invalid file type', async () => {
33+
render(<AddBackgroundEffectLayout customBackgroundImageChange={vi.fn()} />);
34+
const input = screen.getByLabelText(/upload/i);
35+
const file = new File(['dummy'], 'test.txt', { type: 'text/plain' });
36+
fireEvent.change(input, { target: { files: [file] } });
37+
expect(
38+
await screen.findByText(/Only JPG, PNG, GIF, or BMP images are allowed/i)
39+
).toBeInTheDocument();
40+
});
41+
42+
it('shows error for file size too large', async () => {
43+
render(<AddBackgroundEffectLayout customBackgroundImageChange={vi.fn()} />);
44+
const input = screen.getByLabelText(/upload/i);
45+
const file = new File(['x'.repeat(3 * 1024 * 1024)], 'big.png', { type: 'image/png' });
46+
Object.defineProperty(file, 'size', { value: 3 * 1024 * 1024 });
47+
fireEvent.change(input, { target: { files: [file] } });
48+
expect(await screen.findByText(/Image must be less than 2MB/i)).toBeInTheDocument();
49+
});
50+
51+
it('handles valid image file upload', async () => {
52+
render(<AddBackgroundEffectLayout customBackgroundImageChange={cb} />);
53+
const input = screen.getByLabelText(/upload/i);
54+
const file = new File(['dummy'], 'test.png', { type: 'image/png' });
55+
fireEvent.change(input, { target: { files: [file] } });
56+
await waitFor(() => expect(cb).toHaveBeenCalledWith('data:image/png;base64,MOCKED'));
57+
});
58+
59+
it('handles valid link submit', async () => {
60+
render(<AddBackgroundEffectLayout customBackgroundImageChange={cb} />);
61+
const input = screen.getByPlaceholderText(/Link from the web/i);
62+
fireEvent.change(input, { target: { value: 'https://example.com/image.png' } });
63+
const button = screen.getByTestId('background-effect-link-submit-button');
64+
fireEvent.click(button);
65+
await waitFor(() => expect(cb).toHaveBeenCalledWith('data:image/png;base64,MOCKED_LINK'));
66+
});
67+
});
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
Box,
3+
Button,
4+
CircularProgress,
5+
InputAdornment,
6+
TextField,
7+
Typography,
8+
} from '@mui/material';
9+
import { ChangeEvent, ReactElement, useState } from 'react';
10+
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
11+
import LinkIcon from '@mui/icons-material/Link';
12+
import FileUploader from '../../FileUploader/FileUploader';
13+
import { ALLOWED_TYPES, MAX_SIZE_MB } from '../../../../utils/constants';
14+
import useImageStorage from '../../../../utils/useImageStorage/useImageStorage';
15+
16+
export type AddBackgroundEffectLayoutProps = {
17+
customBackgroundImageChange: (dataUrl: string) => void;
18+
};
19+
20+
/**
21+
* AddBackgroundEffectLayout Component
22+
*
23+
* This component manages the UI for adding background effects.
24+
* @param {AddBackgroundEffectLayoutProps} props - The props for the component.
25+
* @property {Function} customBackgroundImageChange - Callback function to handle background image change.
26+
* @returns {ReactElement} The add background effect layout component.
27+
*/
28+
const AddBackgroundEffectLayout = ({
29+
customBackgroundImageChange,
30+
}: AddBackgroundEffectLayoutProps): ReactElement => {
31+
const [fileError, setFileError] = useState<string>('');
32+
const [imageLink, setImageLink] = useState<string>('');
33+
const [linkLoading, setLinkLoading] = useState<boolean>(false);
34+
const { storageError, handleImageFromFile, handleImageFromLink } = useImageStorage();
35+
36+
type HandleFileChangeType = ChangeEvent<HTMLInputElement> | { target: { files: FileList } };
37+
38+
const handleFileChange = async (e: HandleFileChangeType) => {
39+
const { files } = e.target;
40+
if (!files || files.length === 0) {
41+
return;
42+
}
43+
44+
const file = files[0];
45+
if (!file) {
46+
return;
47+
}
48+
49+
if (!ALLOWED_TYPES.includes(file.type)) {
50+
setFileError('Only JPG, PNG, GIF, or BMP images are allowed.');
51+
return;
52+
}
53+
54+
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
55+
setFileError(`Image must be less than ${MAX_SIZE_MB}MB.`);
56+
return;
57+
}
58+
59+
try {
60+
const newImage = await handleImageFromFile(file);
61+
if (newImage) {
62+
setFileError('');
63+
customBackgroundImageChange(newImage.dataUrl);
64+
}
65+
} catch {
66+
setFileError('Failed to process uploaded image.');
67+
}
68+
};
69+
70+
const handleLinkSubmit = async () => {
71+
setFileError('');
72+
setLinkLoading(true);
73+
try {
74+
const newImage = await handleImageFromLink(imageLink);
75+
if (newImage) {
76+
setFileError('');
77+
customBackgroundImageChange(newImage.dataUrl);
78+
} else {
79+
setFileError('Failed to store image.');
80+
}
81+
} catch {
82+
// error handled in hook
83+
} finally {
84+
setLinkLoading(false);
85+
}
86+
};
87+
88+
return (
89+
<Box
90+
sx={{
91+
overflow: 'auto',
92+
}}
93+
>
94+
<FileUploader handleFileChange={handleFileChange} />
95+
96+
{(fileError || storageError) && (
97+
<Typography color="error" mt={1} fontSize={14}>
98+
{fileError || storageError}
99+
</Typography>
100+
)}
101+
102+
<Box mt={2} display="flex" alignItems="center" gap={1}>
103+
<TextField
104+
fullWidth
105+
size="small"
106+
placeholder="Link from the web"
107+
className="add-background-effect-input"
108+
value={imageLink}
109+
onChange={(e) => setImageLink(e.target.value)}
110+
InputProps={{
111+
startAdornment: (
112+
<InputAdornment position="start">
113+
{linkLoading ? <CircularProgress size={24} /> : <LinkIcon sx={{ fontSize: 24 }} />}
114+
</InputAdornment>
115+
),
116+
}}
117+
/>
118+
119+
<Button
120+
data-testid="background-effect-link-submit-button"
121+
variant="contained"
122+
color="primary"
123+
onClick={handleLinkSubmit}
124+
disabled={linkLoading}
125+
style={{ minWidth: 0, padding: '8px 12px' }}
126+
>
127+
{linkLoading ? (
128+
<CircularProgress size={24} color="inherit" />
129+
) : (
130+
<ArrowForwardIcon sx={{ fontSize: 24 }} />
131+
)}
132+
</Button>
133+
</Box>
134+
</Box>
135+
);
136+
};
137+
138+
export default AddBackgroundEffectLayout;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import AddBackgroundEffectLayout from './AddBackgroundEffectLayout';
2+
3+
export default AddBackgroundEffectLayout;

frontend/src/components/BackgroundEffects/AddBackgroundEffect/Index.tsx

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { vi, describe, it, expect } from 'vitest';
4+
import BackgroundEffectTabs from './BackgroundEffectTabs';
5+
6+
describe('BackgroundEffectTabs', () => {
7+
const setTabSelected = vi.fn();
8+
const setBackgroundSelected = vi.fn();
9+
const clearBgWhenSelectedDeleted = vi.fn();
10+
const customBackgroundImageChange = vi.fn();
11+
12+
it('renders tabs and defaults to Backgrounds tab', () => {
13+
render(
14+
<BackgroundEffectTabs
15+
mode="meeting"
16+
tabSelected={0}
17+
setTabSelected={setTabSelected}
18+
backgroundSelected=""
19+
setBackgroundSelected={setBackgroundSelected}
20+
cleanupSelectedBackgroundReplacement={clearBgWhenSelectedDeleted}
21+
customBackgroundImageChange={customBackgroundImageChange}
22+
/>
23+
);
24+
expect(screen.getByRole('tab', { name: /Backgrounds/i })).toBeInTheDocument();
25+
expect(screen.getByRole('tab', { name: /Add Background/i })).toBeInTheDocument();
26+
expect(screen.getByRole('tab', { name: /Backgrounds/i })).toHaveAttribute(
27+
'aria-selected',
28+
'true'
29+
);
30+
});
31+
32+
it('switches to Add Background tab when clicked', async () => {
33+
render(
34+
<BackgroundEffectTabs
35+
mode="meeting"
36+
tabSelected={0}
37+
setTabSelected={setTabSelected}
38+
backgroundSelected=""
39+
setBackgroundSelected={setBackgroundSelected}
40+
cleanupSelectedBackgroundReplacement={clearBgWhenSelectedDeleted}
41+
customBackgroundImageChange={customBackgroundImageChange}
42+
/>
43+
);
44+
const addTab = screen.getByRole('tab', { name: /Add Background/i });
45+
await userEvent.click(addTab);
46+
expect(setTabSelected).toHaveBeenCalledWith(1);
47+
});
48+
49+
it('renders AddBackgroundEffectLayout when Add Background tab is selected', () => {
50+
render(
51+
<BackgroundEffectTabs
52+
mode="waiting"
53+
tabSelected={1}
54+
setTabSelected={setTabSelected}
55+
backgroundSelected=""
56+
setBackgroundSelected={setBackgroundSelected}
57+
cleanupSelectedBackgroundReplacement={clearBgWhenSelectedDeleted}
58+
customBackgroundImageChange={customBackgroundImageChange}
59+
/>
60+
);
61+
expect(screen.getByText(/upload/i)).toBeInTheDocument();
62+
});
63+
});

0 commit comments

Comments
 (0)