Mute state improvements including muting if skipLobby=true in SPA (Based on #2834) (#2846)

This commit is contained in:
Timo
2024-11-28 18:05:12 +01:00
committed by GitHub
parent f7c7f41afd
commit 28da8c48da
7 changed files with 203 additions and 25 deletions

View File

@@ -131,48 +131,46 @@ export const GroupCallView: FC<Props> = ({
const latestDevices = useRef<MediaDevices>();
latestDevices.current = deviceContext;
// TODO: why do we use a ref here instead of using muteStates directly?
const latestMuteStates = useRef<MuteStates>();
latestMuteStates.current = muteStates;
useEffect(() => {
const defaultDeviceSetup = async (
requestedDeviceData: JoinCallData,
): Promise<void> => {
const defaultDeviceSetup = async ({
audioInput,
videoInput,
}: JoinCallData): Promise<void> => {
// XXX: I think this is broken currently - LiveKit *won't* request
// permissions and give you device names unless you specify a kind, but
// here we want all kinds of devices. This needs a fix in livekit-client
// for the following name-matching logic to do anything useful.
const devices = await Room.getLocalDevices(undefined, true);
const { audioInput, videoInput } = requestedDeviceData;
if (audioInput === null) {
latestMuteStates.current!.audio.setEnabled?.(false);
} else {
if (audioInput) {
const deviceId = findDeviceByName(audioInput, "audioinput", devices);
if (!deviceId) {
logger.warn("Unknown audio input: " + audioInput);
// override the default mute state
latestMuteStates.current!.audio.setEnabled?.(false);
} else {
logger.debug(
`Found audio input ID ${deviceId} for name ${audioInput}`,
);
latestDevices.current!.audioInput.select(deviceId);
latestMuteStates.current!.audio.setEnabled?.(true);
}
}
if (videoInput === null) {
latestMuteStates.current!.video.setEnabled?.(false);
} else {
if (videoInput) {
const deviceId = findDeviceByName(videoInput, "videoinput", devices);
if (!deviceId) {
logger.warn("Unknown video input: " + videoInput);
// override the default mute state
latestMuteStates.current!.video.setEnabled?.(false);
} else {
logger.debug(
`Found video input ID ${deviceId} for name ${videoInput}`,
);
latestDevices.current!.videoInput.select(deviceId);
latestMuteStates.current!.video.setEnabled?.(true);
}
}
};
@@ -199,7 +197,6 @@ export const GroupCallView: FC<Props> = ({
} else {
// No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => {
await defaultDeviceSetup({ audioInput: null, videoInput: null });
await enterRTCSession(rtcSession, perParticipantE2EE);
})().catch((e) => {
logger.error("Error joining RTC session", e);

View File

@@ -0,0 +1,172 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import React, { ReactNode } from "react";
import { beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { useMuteStates } from "./MuteStates";
import {
MediaDevice,
MediaDevices,
MediaDevicesContext,
} from "../livekit/MediaDevicesContext";
import { mockConfig } from "../utils/test";
function TestComponent(): ReactNode {
const muteStates = useMuteStates();
return (
<div>
<div data-testid="audio-enabled">
{muteStates.audio.enabled.toString()}
</div>
<div data-testid="video-enabled">
{muteStates.video.enabled.toString()}
</div>
</div>
);
}
const mockMicrophone: MediaDeviceInfo = {
deviceId: "",
kind: "audioinput",
label: "",
groupId: "",
toJSON() {
return {};
},
};
const mockSpeaker: MediaDeviceInfo = {
deviceId: "",
kind: "audiooutput",
label: "",
groupId: "",
toJSON() {
return {};
},
};
const mockCamera: MediaDeviceInfo = {
deviceId: "",
kind: "videoinput",
label: "",
groupId: "",
toJSON() {
return {};
},
};
function mockDevices(available: MediaDeviceInfo[]): MediaDevice {
return {
available,
selectedId: "",
select: (): void => {},
};
}
function mockMediaDevices(
{
microphone,
speaker,
camera,
}: {
microphone?: boolean;
speaker?: boolean;
camera?: boolean;
} = { microphone: true, speaker: true, camera: true },
): MediaDevices {
return {
audioInput: mockDevices(microphone ? [mockMicrophone] : []),
audioOutput: mockDevices(speaker ? [mockSpeaker] : []),
videoInput: mockDevices(camera ? [mockCamera] : []),
startUsingDeviceNames: (): void => {},
stopUsingDeviceNames: (): void => {},
};
}
describe("useMuteStates", () => {
beforeEach(() => {
vi.spyOn(React, "useContext").mockReturnValue({});
});
afterEach(() => {
vi.restoreAllMocks();
});
afterAll(() => {
vi.clearAllMocks();
});
it("disabled when no input devices", () => {
mockConfig();
render(
<MemoryRouter>
<MediaDevicesContext.Provider
value={mockMediaDevices({
microphone: false,
camera: false,
})}
>
<TestComponent />
</MediaDevicesContext.Provider>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
});
it("should be enabled by default", () => {
mockConfig();
render(
<MemoryRouter>
<MediaDevicesContext.Provider value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext.Provider>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
});
it("uses defaults from config", () => {
mockConfig({
media_devices: {
enable_audio: false,
enable_video: false,
},
});
render(
<MemoryRouter>
<MediaDevicesContext.Provider value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext.Provider>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
});
it("skipLobby mutes inputs", () => {
mockConfig();
render(
<MemoryRouter initialEntries={["/room/?skipLobby=true"]}>
<MediaDevicesContext.Provider value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext.Provider>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
});
});

View File

@@ -19,6 +19,7 @@ import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext";
import { useReactiveState } from "../useReactiveState";
import { ElementWidgetActions, widget } from "../widget";
import { Config } from "../config/Config";
import { useUrlParams } from "../UrlParams";
/**
* If there already are this many participants in the call, we automatically mute
@@ -72,13 +73,14 @@ function useMuteState(
export function useMuteStates(): MuteStates {
const devices = useMediaDevices();
const audio = useMuteState(
devices.audioInput,
() => Config.get().media_devices.enable_audio,
);
const { skipLobby } = useUrlParams();
const audio = useMuteState(devices.audioInput, () => {
return Config.get().media_devices.enable_audio && !skipLobby;
});
const video = useMuteState(
devices.videoInput,
() => Config.get().media_devices.enable_video,
() => Config.get().media_devices.enable_video && !skipLobby,
);
useEffect(() => {