Prevent new devices from automatically starting unmuted in call (#2959)

This commit is contained in:
Robin
2025-01-17 10:30:28 -05:00
committed by GitHub
parent cda802a2e9
commit e636542b1e
6 changed files with 59 additions and 35 deletions

View File

@@ -118,6 +118,7 @@ function createGroupCallView(widget: WidgetHelpers | null): {
skipLobby={false} skipLobby={false}
hideHeader={true} hideHeader={true}
rtcSession={rtcSession as unknown as MatrixRTCSession} rtcSession={rtcSession as unknown as MatrixRTCSession}
isJoined
muteStates={muteState} muteStates={muteState}
widget={widget} widget={widget}
/> />

View File

@@ -46,7 +46,6 @@ import {
} from "../livekit/MediaDevicesContext"; } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers"; import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
import { useRoomAvatar } from "./useRoomAvatar"; import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName"; import { useRoomName } from "./useRoomName";
@@ -74,6 +73,7 @@ interface Props {
skipLobby: boolean; skipLobby: boolean;
hideHeader: boolean; hideHeader: boolean;
rtcSession: MatrixRTCSession; rtcSession: MatrixRTCSession;
isJoined: boolean;
muteStates: MuteStates; muteStates: MuteStates;
widget: WidgetHelpers | null; widget: WidgetHelpers | null;
} }
@@ -86,11 +86,11 @@ export const GroupCallView: FC<Props> = ({
skipLobby, skipLobby,
hideHeader, hideHeader,
rtcSession, rtcSession,
isJoined,
muteStates, muteStates,
widget, widget,
}) => { }) => {
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
const leaveSoundContext = useLatest( const leaveSoundContext = useLatest(
useAudioContext({ useAudioContext({
sounds: callEventAudioSounds, sounds: callEventAudioSounds,

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { type FC, useCallback, useState, type ReactNode } from "react"; import { type FC, useCallback, useState } from "react";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@@ -20,8 +20,12 @@ import {
} from "../livekit/MediaDevicesContext"; } from "../livekit/MediaDevicesContext";
import { mockConfig } from "../utils/test"; import { mockConfig } from "../utils/test";
function TestComponent(): ReactNode { interface TestComponentProps {
const muteStates = useMuteStates(); isJoined?: boolean;
}
const TestComponent: FC<TestComponentProps> = ({ isJoined = false }) => {
const muteStates = useMuteStates(isJoined);
const onToggleAudio = useCallback( const onToggleAudio = useCallback(
() => muteStates.audio.setEnabled?.(!muteStates.audio.enabled), () => muteStates.audio.setEnabled?.(!muteStates.audio.enabled),
[muteStates], [muteStates],
@@ -37,7 +41,7 @@ function TestComponent(): ReactNode {
</div> </div>
</div> </div>
); );
} };
const mockMicrophone: MediaDeviceInfo = { const mockMicrophone: MediaDeviceInfo = {
deviceId: "", deviceId: "",
@@ -134,7 +138,7 @@ describe("useMuteStates", () => {
expect(screen.getByTestId("video-enabled").textContent).toBe("false"); expect(screen.getByTestId("video-enabled").textContent).toBe("false");
}); });
it("should be enabled by default", () => { it("enables devices by default in the lobby", () => {
mockConfig(); mockConfig();
render( render(
@@ -148,6 +152,22 @@ describe("useMuteStates", () => {
expect(screen.getByTestId("video-enabled").textContent).toBe("true"); expect(screen.getByTestId("video-enabled").textContent).toBe("true");
}); });
it("disables devices by default in the call", () => {
// Disabling new devices in the call ensures that connecting a webcam
// mid-call won't cause it to suddenly be enabled without user input
mockConfig();
render(
<MemoryRouter>
<MediaDevicesContext.Provider value={mockMediaDevices()}>
<TestComponent isJoined />
</MediaDevicesContext.Provider>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
});
it("uses defaults from config", () => { it("uses defaults from config", () => {
mockConfig({ mockConfig({
media_devices: { media_devices: {

View File

@@ -74,17 +74,17 @@ function useMuteState(
); );
} }
export function useMuteStates(): MuteStates { export function useMuteStates(isJoined: boolean): MuteStates {
const devices = useMediaDevices(); const devices = useMediaDevices();
const { skipLobby } = useUrlParams(); const { skipLobby } = useUrlParams();
const audio = useMuteState(devices.audioInput, () => { const audio = useMuteState(devices.audioInput, () => {
return Config.get().media_devices.enable_audio && !skipLobby; return Config.get().media_devices.enable_audio && !skipLobby && !isJoined;
}); });
const video = useMuteState( const video = useMuteState(
devices.videoInput, devices.videoInput,
() => Config.get().media_devices.enable_video && !skipLobby, () => Config.get().media_devices.enable_video && !skipLobby && !isJoined,
); );
useEffect(() => { useEffect(() => {

View File

@@ -40,6 +40,7 @@ import { useOptInAnalytics } from "../settings/settings";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { Link } from "../button/Link"; import { Link } from "../button/Link";
import { ErrorView } from "../ErrorView"; import { ErrorView } from "../ErrorView";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
export const RoomPage: FC = () => { export const RoomPage: FC = () => {
const { const {
@@ -66,7 +67,10 @@ export const RoomPage: FC = () => {
const { avatarUrl, displayName: userDisplayName } = useProfile(client); const { avatarUrl, displayName: userDisplayName } = useProfile(client);
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers); const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
const muteStates = useMuteStates(); const isJoined = useMatrixRTCSessionJoinState(
groupCallState.kind === "loaded" ? groupCallState.rtcSession : undefined,
);
const muteStates = useMuteStates(isJoined);
useEffect(() => { useEffect(() => {
// If we've finished loading, are not already authed and we've been given a display name as // If we've finished loading, are not already authed and we've been given a display name as
@@ -111,6 +115,7 @@ export const RoomPage: FC = () => {
widget={widget} widget={widget}
client={client!} client={client!}
rtcSession={groupCallState.rtcSession} rtcSession={groupCallState.rtcSession}
isJoined={isJoined}
isPasswordlessUser={passwordlessUser} isPasswordlessUser={passwordlessUser}
confineToRoom={confineToRoom} confineToRoom={confineToRoom}
preload={preload} preload={preload}

View File

@@ -10,35 +10,33 @@ import {
type MatrixRTCSession, type MatrixRTCSession,
MatrixRTCSessionEvent, MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useMatrixRTCSessionJoinState( export function useMatrixRTCSessionJoinState(
rtcSession: MatrixRTCSession, rtcSession: MatrixRTCSession | undefined,
): boolean { ): boolean {
const [isJoined, setJoined] = useState(rtcSession.isJoined()); const [, setNumUpdates] = useState(0);
const onJoinStateChanged = useCallback(
(isJoined: boolean) => {
logger.info(
`Session in room ${rtcSession.room.roomId} changed to ${
isJoined ? "joined" : "left"
}`,
);
setJoined(isJoined);
},
[rtcSession],
);
useEffect(() => { useEffect(() => {
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged); if (rtcSession !== undefined) {
const onJoinStateChanged = (isJoined: boolean): void => {
logger.info(
`Session in room ${rtcSession.room.roomId} changed to ${
isJoined ? "joined" : "left"
}`,
);
setNumUpdates((n) => n + 1); // Force an update
};
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
return (): void => { return (): void => {
rtcSession.off( rtcSession.off(
MatrixRTCSessionEvent.JoinStateChanged, MatrixRTCSessionEvent.JoinStateChanged,
onJoinStateChanged, onJoinStateChanged,
); );
}; };
}, [rtcSession, onJoinStateChanged]); }
}, [rtcSession]);
return isJoined; return rtcSession?.isJoined() ?? false;
} }