Refactor media devices to live outside React as Observables

This moves the media devices state out of React to further our transition to a MVVM architecture in which we can more easily model and store complex application state. I have created an AppViewModel to act as the overarching state holder for any future non-React state we end up creating, and the MediaDevices reside within this. We should move more application logic (including the CallViewModel itself) there in the future.
This commit is contained in:
Robin
2025-05-16 06:41:46 -04:00
parent 32b6250cc3
commit 032e1e438c
24 changed files with 754 additions and 681 deletions

View File

@@ -10,10 +10,12 @@ import { type FC } from "react";
import { render } from "@testing-library/react";
import userEvent, { type UserEvent } from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import { of } from "rxjs";
import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext";
import { MediaDevicesContext } from "./MediaDevicesContext";
import { useAudioContext } from "./useAudioContext";
import { soundEffectVolume as soundEffectVolumeSetting } from "./settings/settings";
import { mockMediaDevices } from "./utils/test";
const staticSounds = Promise.resolve({
aSound: new ArrayBuffer(0),
@@ -102,13 +104,21 @@ afterEach(() => {
});
test("can play a single sound", async () => {
const { findByText } = render(<TestComponentWrapper />);
const { findByText } = render(
<MediaDevicesContext.Provider value={mockMediaDevices({})}>
<TestComponentWrapper />
</MediaDevicesContext.Provider>,
);
await user.click(await findByText("Valid sound"));
expect(testAudioContext.createBufferSource).toHaveBeenCalledOnce();
});
test("will ignore sounds that are not registered", async () => {
const { findByText } = render(<TestComponentWrapper />);
const { findByText } = render(
<MediaDevicesContext.Provider value={mockMediaDevices({})}>
<TestComponentWrapper />
</MediaDevicesContext.Provider>,
);
await user.click(await findByText("Invalid sound"));
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
});
@@ -116,19 +126,13 @@ test("will ignore sounds that are not registered", async () => {
test("will use the correct device", () => {
render(
<MediaDevicesContext.Provider
value={{
audioInput: deviceStub,
value={mockMediaDevices({
audioOutput: {
selectedId: "chosen-device",
selectedGroupId: "",
available: new Map(),
available$: of(new Map<never, never>()),
selected$: of({ id: "chosen-device", virtualEarpiece: false }),
select: () => {},
useAsEarpiece: false,
},
videoInput: deviceStub,
startUsingDeviceNames: () => {},
stopUsingDeviceNames: () => {},
}}
})}
>
<TestComponentWrapper />
</MediaDevicesContext.Provider>,
@@ -139,7 +143,11 @@ test("will use the correct device", () => {
test("will use the correct volume level", async () => {
soundEffectVolumeSetting.setValue(0.33);
const { findByText } = render(<TestComponentWrapper />);
const { findByText } = render(
<MediaDevicesContext.Provider value={mockMediaDevices({})}>
<TestComponentWrapper />
</MediaDevicesContext.Provider>,
);
await user.click(await findByText("Valid sound"));
expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith(
0.33,
@@ -151,19 +159,13 @@ test("will use the correct volume level", async () => {
test("will use the pan if earpiece is selected", async () => {
const { findByText } = render(
<MediaDevicesContext.Provider
value={{
audioInput: deviceStub,
value={mockMediaDevices({
audioOutput: {
selectedId: "chosen-device",
selectedGroupId: "",
available: new Map(),
available$: of(new Map<never, never>()),
selected$: of({ id: "chosen-device", virtualEarpiece: true }),
select: () => {},
useAsEarpiece: true,
},
videoInput: deviceStub,
startUsingDeviceNames: () => {},
stopUsingDeviceNames: () => {},
}}
})}
>
<TestComponentWrapper />
</MediaDevicesContext.Provider>,