2019-01-24 13:33:22 -07:00
|
|
|
/*
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2020-07-10 19:07:11 +01:00
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
2024-09-09 14:57:16 +01:00
|
|
|
Copyright 2019 New Vector Ltd
|
2019-01-24 13:33:22 -07:00
|
|
|
|
2025-01-06 11:18:54 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-09-09 14:57:16 +01:00
|
|
|
Please see LICENSE files in the repository root for full details.
|
2019-01-24 13:33:22 -07:00
|
|
|
*/
|
|
|
|
|
|
2025-03-27 10:43:58 +00:00
|
|
|
import React, { type JSX, type ReactNode } from "react";
|
2023-05-29 10:19:37 +12:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2023-06-05 14:53:11 +01:00
|
|
|
import { FALLBACK_ICE_SERVER } from "matrix-js-sdk/src/webrtc/call";
|
2025-02-05 13:25:06 +00:00
|
|
|
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
2021-10-22 17:23:32 -05:00
|
|
|
|
2021-06-29 13:11:58 +01:00
|
|
|
import { _t } from "../../../../../languageHandler";
|
2025-02-05 13:25:06 +00:00
|
|
|
import MediaDeviceHandler, { type IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
|
2019-02-22 10:47:18 -07:00
|
|
|
import Field from "../../../elements/Field";
|
|
|
|
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
2021-06-29 13:11:58 +01:00
|
|
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
2021-07-09 13:45:39 +02:00
|
|
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
2022-11-09 21:14:55 +01:00
|
|
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
2022-11-11 10:38:51 +01:00
|
|
|
import { requestMediaPermissions } from "../../../../../utils/media/requestMediaPermissions";
|
2023-05-19 12:03:39 +12:00
|
|
|
import SettingsTab from "../SettingsTab";
|
|
|
|
|
import { SettingsSection } from "../../shared/SettingsSection";
|
2024-11-05 18:36:26 +00:00
|
|
|
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
2023-06-14 13:42:07 +01:00
|
|
|
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
2019-01-24 13:33:22 -07:00
|
|
|
|
2022-11-09 21:14:55 +01:00
|
|
|
interface IState {
|
2023-02-16 09:38:44 +00:00
|
|
|
mediaDevices: IMediaDevices | null;
|
|
|
|
|
[MediaDeviceKindEnum.AudioOutput]: string | null;
|
|
|
|
|
[MediaDeviceKindEnum.AudioInput]: string | null;
|
|
|
|
|
[MediaDeviceKindEnum.VideoInput]: string | null;
|
2022-11-09 21:14:55 +01:00
|
|
|
audioAutoGainControl: boolean;
|
|
|
|
|
audioEchoCancellation: boolean;
|
|
|
|
|
audioNoiseSuppression: boolean;
|
2021-07-09 12:57:42 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-29 10:19:37 +12:00
|
|
|
/**
|
|
|
|
|
* Maps deviceKind to the right get method on MediaDeviceHandler
|
|
|
|
|
* Helpful for setting state
|
|
|
|
|
*/
|
|
|
|
|
const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | null => {
|
|
|
|
|
switch (deviceKind) {
|
|
|
|
|
case MediaDeviceKindEnum.AudioOutput:
|
|
|
|
|
return MediaDeviceHandler.getAudioOutput();
|
|
|
|
|
case MediaDeviceKindEnum.AudioInput:
|
|
|
|
|
return MediaDeviceHandler.getAudioInput();
|
|
|
|
|
case MediaDeviceKindEnum.VideoInput:
|
|
|
|
|
return MediaDeviceHandler.getVideoInput();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-04 13:41:34 +00:00
|
|
|
export default class VoiceUserSettingsTab extends React.Component<EmptyObject, IState> {
|
2023-06-14 13:42:07 +01:00
|
|
|
public static contextType = MatrixClientContext;
|
2024-12-02 09:39:36 +00:00
|
|
|
declare public context: React.ContextType<typeof MatrixClientContext>;
|
2023-06-14 13:42:07 +01:00
|
|
|
|
2025-03-19 10:40:06 +00:00
|
|
|
public constructor(props: EmptyObject) {
|
|
|
|
|
super(props);
|
2019-01-24 13:33:22 -07:00
|
|
|
|
|
|
|
|
this.state = {
|
2021-07-09 12:57:42 +02:00
|
|
|
mediaDevices: null,
|
2021-07-09 13:45:39 +02:00
|
|
|
[MediaDeviceKindEnum.AudioOutput]: null,
|
|
|
|
|
[MediaDeviceKindEnum.AudioInput]: null,
|
|
|
|
|
[MediaDeviceKindEnum.VideoInput]: null,
|
2022-11-09 21:14:55 +01:00
|
|
|
audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl(),
|
|
|
|
|
audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation(),
|
|
|
|
|
audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(),
|
2019-01-24 13:43:16 -07:00
|
|
|
};
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public async componentDidMount(): Promise<void> {
|
2021-06-23 09:26:33 +02:00
|
|
|
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
2019-06-26 18:54:15 +02:00
|
|
|
if (canSeeDeviceLabels) {
|
2023-05-29 10:19:37 +12:00
|
|
|
await this.refreshMediaDevices();
|
2019-06-26 18:54:15 +02:00
|
|
|
}
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
|
2021-07-09 13:45:39 +02:00
|
|
|
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
|
2019-01-24 13:33:22 -07:00
|
|
|
this.setState({
|
2023-03-16 11:07:29 +00:00
|
|
|
mediaDevices: (await MediaDeviceHandler.getDevices()) ?? null,
|
2023-05-29 10:19:37 +12:00
|
|
|
[MediaDeviceKindEnum.AudioOutput]: mapDeviceKindToHandlerValue(MediaDeviceKindEnum.AudioOutput),
|
|
|
|
|
[MediaDeviceKindEnum.AudioInput]: mapDeviceKindToHandlerValue(MediaDeviceKindEnum.AudioInput),
|
|
|
|
|
[MediaDeviceKindEnum.VideoInput]: mapDeviceKindToHandlerValue(MediaDeviceKindEnum.VideoInput),
|
2019-01-24 13:33:22 -07:00
|
|
|
});
|
2019-06-26 18:54:15 +02:00
|
|
|
if (stream) {
|
|
|
|
|
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
|
|
|
|
// so that we don't leave it lingering around with webcam enabled etc
|
|
|
|
|
// as here we called gUM to ask user for permission to their device names only
|
|
|
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
|
|
|
}
|
2019-01-24 13:33:22 -07:00
|
|
|
};
|
|
|
|
|
|
2021-07-09 13:45:39 +02:00
|
|
|
private requestMediaPermissions = async (): Promise<void> => {
|
2022-11-11 10:38:51 +01:00
|
|
|
const stream = await requestMediaPermissions();
|
|
|
|
|
if (stream) {
|
2023-05-29 10:19:37 +12:00
|
|
|
await this.refreshMediaDevices(stream);
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-05-29 10:19:37 +12:00
|
|
|
private setDevice = async (deviceId: string, kind: MediaDeviceKindEnum): Promise<void> => {
|
|
|
|
|
// set state immediately so UI is responsive
|
2023-05-05 20:13:50 +12:00
|
|
|
this.setState<any>({ [kind]: deviceId });
|
2023-05-29 10:19:37 +12:00
|
|
|
try {
|
|
|
|
|
await MediaDeviceHandler.instance.setDevice(deviceId, kind);
|
2024-10-16 16:43:07 +01:00
|
|
|
} catch {
|
2023-05-29 10:19:37 +12:00
|
|
|
logger.error(`Failed to set device ${kind}: ${deviceId}`);
|
|
|
|
|
// reset state to current value
|
|
|
|
|
this.setState<any>({ [kind]: mapDeviceKindToHandlerValue(kind) });
|
|
|
|
|
}
|
2019-01-24 13:33:22 -07:00
|
|
|
};
|
|
|
|
|
|
2021-07-09 13:45:39 +02:00
|
|
|
private changeWebRtcMethod = (p2p: boolean): void => {
|
2023-06-14 13:42:07 +01:00
|
|
|
this.context.setForceTURN(!p2p);
|
2019-01-24 13:33:22 -07:00
|
|
|
};
|
|
|
|
|
|
2021-07-09 13:45:39 +02:00
|
|
|
private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
|
2019-06-17 19:36:52 +02:00
|
|
|
return devices.map((d) => {
|
2021-07-19 22:43:11 +01:00
|
|
|
return (
|
|
|
|
|
<option key={`${category}-${d.deviceId}`} value={d.deviceId}>
|
|
|
|
|
{d.label}
|
|
|
|
|
</option>
|
|
|
|
|
);
|
2019-06-17 19:36:52 +02:00
|
|
|
});
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
|
2023-02-16 09:38:44 +00:00
|
|
|
private renderDropdown(kind: MediaDeviceKindEnum, label: string): ReactNode {
|
2023-03-16 11:07:29 +00:00
|
|
|
const devices = this.state.mediaDevices?.[kind].slice(0);
|
|
|
|
|
if (!devices?.length) return null;
|
2021-07-09 14:15:36 +02:00
|
|
|
|
2022-11-15 10:02:40 +01:00
|
|
|
const defaultDevice = MediaDeviceHandler.getDefaultDevice(devices);
|
2021-07-09 14:15:36 +02:00
|
|
|
return (
|
|
|
|
|
<Field
|
|
|
|
|
element="select"
|
|
|
|
|
label={label}
|
|
|
|
|
value={this.state[kind] || defaultDevice}
|
|
|
|
|
onChange={(e) => this.setDevice(e.target.value, kind)}
|
|
|
|
|
>
|
|
|
|
|
{this.renderDeviceOptions(devices, kind)}
|
|
|
|
|
</Field>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 09:38:44 +00:00
|
|
|
public render(): ReactNode {
|
|
|
|
|
let requestButton: ReactNode | undefined;
|
|
|
|
|
let speakerDropdown: ReactNode | undefined;
|
|
|
|
|
let microphoneDropdown: ReactNode | undefined;
|
|
|
|
|
let webcamDropdown: ReactNode | undefined;
|
2021-07-09 12:57:42 +02:00
|
|
|
if (!this.state.mediaDevices) {
|
2019-01-24 13:33:22 -07:00
|
|
|
requestButton = (
|
2023-05-19 12:03:39 +12:00
|
|
|
<div>
|
2023-09-26 18:35:55 +01:00
|
|
|
<p>{_t("settings|voip|missing_permissions_prompt")}</p>
|
2021-07-09 12:57:42 +02:00
|
|
|
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
|
2023-09-26 18:35:55 +01:00
|
|
|
{_t("settings|voip|request_permissions")}
|
2019-01-24 13:33:22 -07:00
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (this.state.mediaDevices) {
|
2023-09-26 18:35:55 +01:00
|
|
|
speakerDropdown = this.renderDropdown(
|
|
|
|
|
MediaDeviceKindEnum.AudioOutput,
|
|
|
|
|
_t("settings|voip|audio_output"),
|
|
|
|
|
) || <p>{_t("settings|voip|audio_output_empty")}</p>;
|
2023-08-31 11:22:10 +01:00
|
|
|
microphoneDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("common|microphone")) || (
|
2023-09-26 18:35:55 +01:00
|
|
|
<p>{_t("settings|voip|audio_input_empty")}</p>
|
2021-07-09 14:15:36 +02:00
|
|
|
);
|
2023-08-31 11:22:10 +01:00
|
|
|
webcamDropdown = this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("common|camera")) || (
|
2023-09-26 18:35:55 +01:00
|
|
|
<p>{_t("settings|voip|video_input_empty")}</p>
|
2021-07-09 14:15:36 +02:00
|
|
|
);
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2023-05-19 12:03:39 +12:00
|
|
|
<SettingsTab>
|
2024-05-07 10:32:24 +01:00
|
|
|
<SettingsSection>
|
2023-05-19 12:03:39 +12:00
|
|
|
{requestButton}
|
2023-09-26 18:35:55 +01:00
|
|
|
<SettingsSubsection heading={_t("settings|voip|voice_section")} stretchContent>
|
2023-05-19 12:03:39 +12:00
|
|
|
{speakerDropdown}
|
|
|
|
|
{microphoneDropdown}
|
|
|
|
|
<LabelledToggleSwitch
|
|
|
|
|
value={this.state.audioAutoGainControl}
|
|
|
|
|
onChange={async (v): Promise<void> => {
|
|
|
|
|
await MediaDeviceHandler.setAudioAutoGainControl(v);
|
|
|
|
|
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() });
|
|
|
|
|
}}
|
2023-09-26 18:35:55 +01:00
|
|
|
label={_t("settings|voip|voice_agc")}
|
2023-05-19 12:03:39 +12:00
|
|
|
data-testid="voice-auto-gain"
|
|
|
|
|
/>
|
|
|
|
|
</SettingsSubsection>
|
2023-09-26 18:35:55 +01:00
|
|
|
<SettingsSubsection heading={_t("settings|voip|video_section")} stretchContent>
|
2023-05-19 12:03:39 +12:00
|
|
|
{webcamDropdown}
|
|
|
|
|
<SettingsFlag name="VideoView.flipVideoHorizontally" level={SettingLevel.ACCOUNT} />
|
|
|
|
|
</SettingsSubsection>
|
|
|
|
|
</SettingsSection>
|
2022-11-09 21:14:55 +01:00
|
|
|
|
2023-09-26 13:04:17 +01:00
|
|
|
<SettingsSection heading={_t("common|advanced")}>
|
2023-09-26 18:35:55 +01:00
|
|
|
<SettingsSubsection heading={_t("settings|voip|voice_processing")}>
|
2022-11-09 21:14:55 +01:00
|
|
|
<LabelledToggleSwitch
|
|
|
|
|
value={this.state.audioNoiseSuppression}
|
2023-01-12 13:25:14 +00:00
|
|
|
onChange={async (v): Promise<void> => {
|
2022-11-09 21:14:55 +01:00
|
|
|
await MediaDeviceHandler.setAudioNoiseSuppression(v);
|
|
|
|
|
this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() });
|
|
|
|
|
}}
|
2023-09-19 17:16:38 +01:00
|
|
|
label={_t("settings|voip|noise_suppression")}
|
2022-11-09 21:14:55 +01:00
|
|
|
data-testid="voice-noise-suppression"
|
|
|
|
|
/>
|
|
|
|
|
<LabelledToggleSwitch
|
|
|
|
|
value={this.state.audioEchoCancellation}
|
2023-01-12 13:25:14 +00:00
|
|
|
onChange={async (v): Promise<void> => {
|
2022-11-09 21:14:55 +01:00
|
|
|
await MediaDeviceHandler.setAudioEchoCancellation(v);
|
|
|
|
|
this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() });
|
|
|
|
|
}}
|
2023-09-19 17:16:38 +01:00
|
|
|
label={_t("settings|voip|echo_cancellation")}
|
2022-11-09 21:14:55 +01:00
|
|
|
data-testid="voice-echo-cancellation"
|
|
|
|
|
/>
|
2023-05-19 12:03:39 +12:00
|
|
|
</SettingsSubsection>
|
2023-09-26 18:35:55 +01:00
|
|
|
<SettingsSubsection heading={_t("settings|voip|connection_section")}>
|
2022-11-09 21:14:55 +01:00
|
|
|
<SettingsFlag
|
|
|
|
|
name="webRtcAllowPeerToPeer"
|
|
|
|
|
level={SettingLevel.DEVICE}
|
|
|
|
|
onChange={this.changeWebRtcMethod}
|
|
|
|
|
/>
|
|
|
|
|
<SettingsFlag
|
|
|
|
|
name="fallbackICEServerAllowed"
|
2023-09-19 17:16:38 +01:00
|
|
|
label={_t("settings|voip|enable_fallback_ice_server", {
|
2023-06-05 14:53:11 +01:00
|
|
|
server: new URL(FALLBACK_ICE_SERVER).pathname,
|
|
|
|
|
})}
|
2022-11-09 21:14:55 +01:00
|
|
|
level={SettingLevel.DEVICE}
|
2024-10-02 15:08:15 +01:00
|
|
|
hideIfCannotSet
|
2022-11-09 21:14:55 +01:00
|
|
|
/>
|
2023-05-19 12:03:39 +12:00
|
|
|
</SettingsSubsection>
|
|
|
|
|
</SettingsSection>
|
|
|
|
|
</SettingsTab>
|
2019-01-24 13:33:22 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|