Skip to content

Commit

Permalink
Mute state improvements including muting if skipLobby=true in SPA (B…
Browse files Browse the repository at this point in the history
…ased on #2834)  (#2846)
  • Loading branch information
toger5 authored Nov 28, 2024
1 parent f7c7f41 commit 28da8c4
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 25 deletions.
4 changes: 2 additions & 2 deletions docs/url-params.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ There are two formats for Element Call urls.
| `deviceId` | Matrix device ID | Yes | Not applicable | The Matrix device ID for the widget host. |
| `displayName` | | No | No | Display name used for auto-registration. |
| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. |
| `fontScale` | A decimal number such as `0.9` | No | No | Factor by which to scale the interface's font size. |
| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. |
| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. |
| `hideHeader` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the room header when in a call. |
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
Expand All @@ -55,7 +55,7 @@ There are two formats for Element Call urls.
| `returnToLobby` | `true` or `false` | No, defaults to `false` | Not applicable | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. |
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
| `skipLobby` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. |
| `skipLobby` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | Not applicable | The Matrix user ID. |
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
Expand Down
2 changes: 1 addition & 1 deletion src/livekit/MediaDevicesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const devicesStub: MediaDevices = {
stopUsingDeviceNames: () => {},
};

const MediaDevicesContext = createContext<MediaDevices>(devicesStub);
export const MediaDevicesContext = createContext<MediaDevices>(devicesStub);

interface Props {
children: JSX.Element;
Expand Down
23 changes: 10 additions & 13 deletions src/room/GroupCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
};
Expand All @@ -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);
Expand Down
172 changes: 172 additions & 0 deletions src/room/MuteStates.test.tsx
Original file line number Diff line number Diff line change
@@ -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");
});
});
12 changes: 7 additions & 5 deletions src/room/MuteStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(() => {
Expand Down
6 changes: 2 additions & 4 deletions src/rtcSessionHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { expect, test, vi } from "vitest";

import { enterRTCSession } from "../src/rtcSessionHelpers";
import { Config } from "../src/config/Config";
import { DEFAULT_CONFIG } from "./config/ConfigOptions";
import { mockConfig } from "./utils/test";

test("It joins the correct Session", async () => {
const focusFromOlderMembership = {
Expand All @@ -34,8 +33,7 @@ test("It joins the correct Session", async () => {
],
};

vi.spyOn(Config, "get").mockReturnValue({
...DEFAULT_CONFIG,
mockConfig({
livekit: { livekit_service_url: "http://my-default-service-url.com" },
});
const mockedSession = vi.mocked({
Expand Down
9 changes: 9 additions & 0 deletions src/utils/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
RemoteUserMediaViewModel,
} from "../state/MediaViewModel";
import { E2eeType } from "../e2ee/e2eeType";
import { DEFAULT_CONFIG, ResolvedConfigOptions } from "../config/ConfigOptions";
import { Config } from "../config/Config";

export function withFakeTimers(continuation: () => void): void {
vi.useFakeTimers();
Expand Down Expand Up @@ -197,3 +199,10 @@ export async function withRemoteMedia(
vm.destroy();
}
}

export function mockConfig(config: Partial<ResolvedConfigOptions> = {}): void {
vi.spyOn(Config, "get").mockReturnValue({
...DEFAULT_CONFIG,
...config,
});
}

0 comments on commit 28da8c4

Please sign in to comment.