Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
013ea86
first things first
cpettet Aug 18, 2025
44c7ab0
working for some stuff
cpettet Aug 18, 2025
4260439
some renaming
cpettet Aug 20, 2025
711f4b6
enable/disable camera
cpettet Aug 20, 2025
2d027d9
video resolution
cpettet Aug 20, 2025
85fcbdc
less redundant
cpettet Aug 20, 2025
69680ca
..
cpettet Sep 3, 2025
e695b8d
test fixes
cpettet Sep 3, 2025
26ebf42
should work on gha
cpettet Sep 3, 2025
dc29859
that type
cpettet Sep 3, 2025
335a47f
revert and redo
cpettet Sep 3, 2025
46f745f
lint issues
cpettet Sep 3, 2025
4e30edd
should be good?
cpettet Sep 3, 2025
a245d74
should work on gha and local
cpettet Sep 3, 2025
de1d224
ans configurable
cpettet Sep 4, 2025
dda6c9e
test
cpettet Sep 4, 2025
cf5f406
audio/video control button tests
cpettet Sep 4, 2025
9b90d9d
fix lint
cpettet Sep 4, 2025
020364f
fix tests
cpettet Sep 4, 2025
7f4d686
tests for config
cpettet Sep 10, 2025
9bef624
enable-disable waiting room
cpettet Sep 10, 2025
6e69991
cleaner implementation
cpettet Sep 10, 2025
3142edc
audio and video on join
cpettet Sep 12, 2025
3269dca
background publisher taken care of
cpettet Sep 12, 2025
0889143
configured control panel
cpettet Sep 12, 2025
f4c15ab
fix that test
cpettet Sep 12, 2025
587471c
too excited
cpettet Sep 12, 2025
6170788
participant-list
cpettet Sep 16, 2025
3e10a68
lint
cpettet Sep 16, 2025
01713d7
config file
cpettet Sep 16, 2025
84e7ec3
resolve another bug
cpettet Sep 16, 2025
f5fa301
fixed tests
cpettet Sep 17, 2025
4d478d5
chat button
cpettet Sep 17, 2025
f37c7a8
screen-sharing
cpettet Sep 17, 2025
8380877
archiving button. layoutMode --> defaultLayoutMode
cpettet Sep 17, 2025
1c2660d
captions button
cpettet Sep 17, 2025
7f6e5c0
showemoji
cpettet Sep 17, 2025
70f8369
rename some flags
cpettet Sep 17, 2025
2801140
renaming
cpettet Sep 17, 2025
f4f5999
fixing tests
cpettet Sep 17, 2025
a423cea
merge changes
cpettet Sep 18, 2025
ec9c5f0
fix tests. use en.json
cpettet Sep 18, 2025
2c0dd1d
renaming
cpettet Sep 18, 2025
d5e4db6
fix
cpettet Sep 18, 2025
c428a25
test fix
cpettet Sep 18, 2025
add0885
show participant list
cpettet Sep 18, 2025
5859fa7
new test file. renaming some tests for consistency
cpettet Sep 18, 2025
3bddf3c
a review from yours truly
cpettet Sep 18, 2025
ebed32a
my manual testing
cpettet Sep 18, 2025
e725537
not enough ../
cpettet Sep 18, 2025
39722fa
ghc suggests
cpettet Sep 19, 2025
96d8dbf
for the meeting room
cpettet Sep 22, 2025
40c6585
some nits
cpettet Sep 22, 2025
170018e
single not double
cpettet Sep 22, 2025
20d56e0
helper instead
cpettet Sep 22, 2025
e71ad04
the great renaming
cpettet Sep 22, 2025
0a4bb74
better config fetch handling
cpettet Sep 22, 2025
5ab05b2
missing jsdoc
cpettet Sep 23, 2025
c0d5791
simpler solution to solve the issue
cpettet Sep 24, 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
26 changes: 26 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"videoSettings": {
"allowBackgroundEffects": true,
Copy link

@czoli1976 czoli1976 Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest renaming it to "allowVideoEffects" because, while currently these effects apply only to the background, the same panel could potentially be used in the future for other effects (e.g., lighting correction, eye correction).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this as a team during design review and considered Visual Effects instead of Background Effects before deciding on this naming scheme. All of our components reference BackgroundEffects and everything is currently a background effect. Let's keep as-is for now, seems like premature optimization to rename this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok @cpettet !

"allowCameraControl": true,
"allowVideoOnJoin": true,
"defaultResolution": "1280x720"
},
"audioSettings": {
"allowAdvancedNoiseSuppression": true,
"allowAudioOnJoin": true,
"allowMicrophoneControl": true
},
"waitingRoomSettings": {
"allowDeviceSelection": true
},
"meetingRoomSettings": {
"allowArchiving": true,
"allowCaptions": true,
"allowChat": true,
"allowDeviceSelection": true,
"allowEmojis": true,
"allowScreenShare": true,
"defaultLayoutMode": "active-speaker",
"showParticipantList": true
}
}
21 changes: 13 additions & 8 deletions frontend/src/App.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, afterEach } from 'vitest';
import React from 'react';
import { PropsWithChildren } from 'react';
import App from './App';

// Mock the page components to make text assertions easy
Expand All @@ -13,22 +13,27 @@ vi.mock('./pages/UnsupportedBrowserPage', () => ({
// Mock context providers and wrappers
vi.mock('./Context/PreviewPublisherProvider', () => ({
__esModule: true,
PreviewPublisherProvider: ({ children }: React.PropsWithChildren) => children,
default: ({ children }: React.PropsWithChildren) => children,
PreviewPublisherProvider: ({ children }: PropsWithChildren) => children,
default: ({ children }: PropsWithChildren) => children,
}));
vi.mock('./Context/PublisherProvider', () => ({
__esModule: true,
PublisherProvider: ({ children }: React.PropsWithChildren) => children,
default: ({ children }: React.PropsWithChildren) => children,
PublisherProvider: ({ children }: PropsWithChildren) => children,
default: ({ children }: PropsWithChildren) => children,
}));
vi.mock('./Context/SessionProvider/session', () => ({
default: ({ children }: React.PropsWithChildren) => children,
default: ({ children }: PropsWithChildren) => children,
}));
vi.mock('./components/RedirectToWaitingRoom', () => ({
default: ({ children }: React.PropsWithChildren) => children,
default: ({ children }: PropsWithChildren) => children,
}));
vi.mock('./Context/RoomContext', () => ({
default: ({ children }: React.PropsWithChildren) => children,
default: ({ children }: PropsWithChildren) => children,
}));
vi.mock('./Context/ConfigProvider', () => ({
__esModule: true,
ConfigContextProvider: ({ children }: PropsWithChildren) => children,
default: ({ children }: PropsWithChildren) => children,
}));

afterEach(() => {
Expand Down
13 changes: 4 additions & 9 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { PublisherProvider } from './Context/PublisherProvider';
import RedirectToWaitingRoom from './components/RedirectToWaitingRoom';
import UnsupportedBrowserPage from './pages/UnsupportedBrowserPage';
import RoomContext from './Context/RoomContext';
import { BackgroundPublisherProvider } from './Context/BackgroundPublisherProvider';

const App = () => {
return (
Expand All @@ -21,11 +20,9 @@ const App = () => {
<Route
path="/waiting-room/:roomName"
element={
<BackgroundPublisherProvider>
<PreviewPublisherProvider>
<WaitingRoom />
</PreviewPublisherProvider>
</BackgroundPublisherProvider>
<PreviewPublisherProvider>
<WaitingRoom />
</PreviewPublisherProvider>
}
/>
<Route
Expand All @@ -34,9 +31,7 @@ const App = () => {
<SessionProvider>
<RedirectToWaitingRoom>
<PublisherProvider>
<BackgroundPublisherProvider>
<Room />
</BackgroundPublisherProvider>
<Room />
</PublisherProvider>
</RedirectToWaitingRoom>
</SessionProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { act, cleanup, renderHook } from '@testing-library/react';
import { afterAll, afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { hasMediaProcessorSupport, initPublisher, Publisher } from '@vonage/client-sdk-video';
import {
hasMediaProcessorSupport,
initPublisher,
Publisher,
PublisherProperties,
} from '@vonage/client-sdk-video';
import EventEmitter from 'events';
import useBackgroundPublisher from './useBackgroundPublisher';
import { UserContextType } from '../../user';
Expand All @@ -14,17 +19,20 @@ import {
defaultVideoDevice,
} from '../../../utils/mockData/device';
import { DEVICE_ACCESS_STATUS } from '../../../utils/constants';
import usePublisherOptions from '../../PublisherProvider/usePublisherOptions';

vi.mock('@vonage/client-sdk-video');
vi.mock('../../../hooks/useUserContext.tsx');
vi.mock('../../../hooks/usePermissions.tsx');
vi.mock('../../../hooks/useDevices.tsx');
vi.mock('../../PublisherProvider/usePublisherOptions');
const mockUseUserContext = useUserContext as Mock<[], UserContextType>;
const mockUsePermissions = usePermissions as Mock<[], PermissionsHookType>;
const mockUseDevices = useDevices as Mock<
[],
{ allMediaDevices: AllMediaDevices; getAllMediaDevices: () => void }
>;
const mockUsePublisherOptions = usePublisherOptions as Mock<[], PublisherProperties>;

const defaultSettings = {
publishAudio: false,
Expand Down Expand Up @@ -66,6 +74,9 @@ describe('useBackgroundPublisher', () => {
accessStatus: DEVICE_ACCESS_STATUS.PENDING,
setAccessStatus: mockSetAccessStatus,
});
mockUsePublisherOptions.mockReturnValue({
publishVideo: true,
});
});

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => {
>();
const { setAccessStatus, accessStatus } = usePermissions();
const backgroundPublisherRef = useRef<Publisher | null>(null);
const [isPublishing, setIsPublishing] = useState(false);
const [isPublishing, setIsPublishing] = useState<boolean>(false);
const initialBackgroundRef = useRef<VideoFilter | undefined>(
user.defaultSettings.backgroundFilter
);
const [backgroundFilter, setBackgroundFilter] = useState<VideoFilter | undefined>(
user.defaultSettings.backgroundFilter
);
const [isVideoEnabled, setIsVideoEnabled] = useState(true);
const [isVideoEnabled, setIsVideoEnabled] = useState<boolean>(true);
const [localVideoSource, setLocalVideoSource] = useState<string | undefined>(undefined);
const deviceStoreRef = useRef<DeviceStore>(new DeviceStore());

Expand Down Expand Up @@ -180,6 +180,8 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => {
videoFilter,
resolution: '1280x720',
videoSource,
publishAudio: false,
publishVideo: isVideoEnabled,
};

backgroundPublisherRef.current = initPublisher(undefined, publisherOptions, (err: unknown) => {
Expand All @@ -191,7 +193,7 @@ const useBackgroundPublisher = (): BackgroundPublisherContextType => {
}
});
addPublisherListeners(backgroundPublisherRef.current);
}, [addPublisherListeners]);
}, [addPublisherListeners, isVideoEnabled]);

/**
* Destroys the background publisher
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/Context/ConfigProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createContext, ReactNode, useMemo } from 'react';
import useConfig, { defaultConfig } from './useConfig';

export type ConfigProviderProps = {
children: ReactNode;
};

export type ConfigContextType = ReturnType<typeof useConfig>;

export const ConfigContext = createContext<ConfigContextType>({
audioSettings: defaultConfig.audioSettings,
meetingRoomSettings: defaultConfig.meetingRoomSettings,
waitingRoomSettings: defaultConfig.waitingRoomSettings,
videoSettings: defaultConfig.videoSettings,
});

/**
* ConfigProvider - React Context Provider for ConfigContext
* ConfigContext contains all application configuration including video settings, audio settings,
* waiting room settings, and meeting room settings loaded from config.json.
* We use Context to make the configuration available in many components across the app without
* prop drilling: https://react.dev/learn/passing-data-deeply-with-context#use-cases-for-context
* See useConfig.tsx for configuration structure and loading logic
* @param {ConfigProviderProps} props - The provider properties
* @property {ReactNode} children - The content to be rendered
* @returns {ConfigContext} a context provider for application configuration
*/
export const ConfigProvider = ({ children }: ConfigProviderProps) => {
const config = useConfig();
const value = useMemo(() => config, [config]);

return <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>;
};
4 changes: 4 additions & 0 deletions frontend/src/Context/ConfigProvider/useConfig/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import useConfig, { defaultConfig } from './useConfig';

export { defaultConfig };
export default useConfig;
131 changes: 131 additions & 0 deletions frontend/src/Context/ConfigProvider/useConfig/useConfig.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import useConfig, { AppConfig } from './useConfig';

describe('useConfig', () => {
let nativeFetch: typeof global.fetch;
const defaultConfig: AppConfig = {
videoSettings: {
allowCameraControl: true,
defaultResolution: '1280x720',
allowVideoOnJoin: true,
allowBackgroundEffects: true,
},
audioSettings: {
allowAdvancedNoiseSuppression: true,
allowAudioOnJoin: true,
allowMicrophoneControl: true,
},
waitingRoomSettings: {
allowDeviceSelection: true,
},
meetingRoomSettings: {
allowArchiving: true,
allowCaptions: true,
allowChat: true,
allowDeviceSelection: true,
allowEmojis: true,
allowScreenShare: true,
defaultLayoutMode: 'active-speaker',
showParticipantList: true,
},
};
const consoleErrorSpy = vi.spyOn(console, 'error');
const consoleInfoSpy = vi.spyOn(console, 'info');

beforeAll(() => {
nativeFetch = global.fetch;
});

beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue({
json: async () => ({}),
});
});

afterEach(() => {
vi.resetAllMocks();
});

afterAll(() => {
global.fetch = nativeFetch;
});

it('returns the default config when no config.json is loaded', async () => {
const { result } = renderHook(() => useConfig());

await waitFor(() => {
expect(result.current).toEqual(defaultConfig);
});
});

it('merges config.json values if loaded (mocked fetch)', async () => {
// All values in this config should override the defaultConfig
const mockConfig: AppConfig = {
videoSettings: {
allowCameraControl: false,
defaultResolution: '640x480',
allowVideoOnJoin: false,
allowBackgroundEffects: false,
},
audioSettings: {
allowAdvancedNoiseSuppression: false,
allowAudioOnJoin: false,
allowMicrophoneControl: false,
},
waitingRoomSettings: {
allowDeviceSelection: false,
},
meetingRoomSettings: {
allowArchiving: false,
allowCaptions: false,
allowChat: false,
allowDeviceSelection: false,
allowEmojis: false,
allowScreenShare: false,
defaultLayoutMode: 'grid',
showParticipantList: false,
},
};
global.fetch = vi.fn().mockResolvedValue({
json: async () => mockConfig,
headers: {
get: () => 'application/json',
},
});
const { result } = renderHook(() => useConfig());

await waitFor(() => {
expect(result.current).toMatchObject(mockConfig);
});
});

it('falls back to defaultConfig if fetch fails', async () => {
const mockFetchError = new Error('mocking a failure to fetch');
global.fetch = vi.fn().mockRejectedValue(mockFetchError);
const { result } = renderHook(() => useConfig());

await waitFor(() => {
expect(result.current).toEqual(defaultConfig);
});
expect(consoleErrorSpy).toHaveBeenCalledWith('Error loading config:', expect.any(Error));
});

it('falls back to defaultConfig if no config.json is found', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
headers: {
get: () => 'text/html',
},
});
const { result } = renderHook(() => useConfig());

await waitFor(() => {
expect(result.current).toEqual(defaultConfig);
});
expect(consoleInfoSpy).toHaveBeenCalledWith('No valid JSON found, using default config');
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
});
Loading
Loading