diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx
index 719315e89..99f7eaf84 100644
--- a/src/room/MuteStates.test.tsx
+++ b/src/room/MuteStates.test.tsx
@@ -6,9 +6,10 @@ Please see LICENSE in the repository root for full details.
*/
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
-import { type ReactNode } from "react";
+import { type FC, useCallback, useState, type ReactNode } from "react";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
+import userEvent from "@testing-library/user-event";
import { useMuteStates } from "./MuteStates";
import {
@@ -21,11 +22,16 @@ import { mockConfig } from "../utils/test";
function TestComponent(): ReactNode {
const muteStates = useMuteStates();
+ const onToggleAudio = useCallback(
+ () => muteStates.audio.setEnabled?.(!muteStates.audio.enabled),
+ [muteStates],
+ );
return (
{muteStates.audio.enabled.toString()}
+
{muteStates.video.enabled.toString()}
@@ -174,4 +180,50 @@ describe("useMuteStates", () => {
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
});
+
+ it("remembers previous state when devices disappear and reappear", async () => {
+ const user = userEvent.setup();
+ mockConfig();
+ const noDevices = mockMediaDevices({ microphone: false, camera: false });
+ const someDevices = mockMediaDevices();
+ const ReappearanceTest: FC = () => {
+ const [devices, setDevices] = useState(someDevices);
+ const onConnectDevicesClick = useCallback(
+ () => setDevices(someDevices),
+ [],
+ );
+ const onDisconnectDevicesClick = useCallback(
+ () => setDevices(noDevices),
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+
+ );
+ };
+
+ render(
);
+ expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
+ expect(screen.getByTestId("video-enabled").textContent).toBe("true");
+ await user.click(screen.getByRole("button", { name: "Toggle audio" }));
+ expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
+ expect(screen.getByTestId("video-enabled").textContent).toBe("true");
+ await user.click(
+ screen.getByRole("button", { name: "Disconnect devices" }),
+ );
+ expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
+ expect(screen.getByTestId("video-enabled").textContent).toBe("false");
+ await user.click(screen.getByRole("button", { name: "Connect devices" }));
+ // Audio should remember that it was muted, while video should re-enable
+ expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
+ expect(screen.getByTestId("video-enabled").textContent).toBe("true");
+ });
});
diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts
index 4a8aa9ddc..132273787 100644
--- a/src/room/MuteStates.ts
+++ b/src/room/MuteStates.ts
@@ -57,8 +57,9 @@ function useMuteState(
enabledByDefault: () => boolean,
): MuteState {
const [enabled, setEnabled] = useReactiveState
(
+ // Determine the default value once devices are actually connected
(prev) =>
- device.available.size > 0 ? (prev ?? enabledByDefault()) : undefined,
+ prev ?? (device.available.size > 0 ? enabledByDefault() : undefined),
[device],
);
return useMemo(