Prevent new devices from automatically starting unmuted in call (#2959)
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user