Files
ThreadNet-Web/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

233 lines
10 KiB
TypeScript
Raw Normal View History

/*
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
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ReactNode } from "react";
2023-05-29 10:19:37 +12:00
import { logger } from "matrix-js-sdk/src/logger";
import { FALLBACK_ICE_SERVER } from "matrix-js-sdk/src/webrtc/call";
2021-10-22 17:23:32 -05:00
2021-06-29 13:11:58 +01:00
import { _t } from "../../../../../languageHandler";
2021-07-09 13:08:39 +02:00
import MediaDeviceHandler, { 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";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsSubsection from "../../shared/SettingsSubsection";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
2022-11-09 21:14:55 +01:00
interface IState {
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();
}
};
2021-07-09 12:57:42 +02:00
export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
public static contextType = MatrixClientContext;
2024-08-01 17:14:28 +01:00
public declare context: React.ContextType<typeof MatrixClientContext>;
2024-08-01 13:01:05 +01:00
public constructor(props: {}, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context);
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
};
}
public async componentDidMount(): Promise<void> {
2021-06-23 09:26:33 +02:00
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
2023-05-29 10:19:37 +12:00
await this.refreshMediaDevices();
}
}
2021-07-09 13:45:39 +02:00
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
this.setState({
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),
});
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());
}
};
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);
}
};
2023-05-29 10:19:37 +12:00
private setDevice = async (deviceId: string, kind: MediaDeviceKindEnum): Promise<void> => {
// set state immediately so UI is responsive
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) });
}
};
2021-07-09 13:45:39 +02:00
private changeWebRtcMethod = (p2p: boolean): void => {
this.context.setForceTURN(!p2p);
};
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) => {
return (
<option key={`${category}-${d.deviceId}`} value={d.deviceId}>
{d.label}
</option>
);
2019-06-17 19:36:52 +02:00
});
}
private renderDropdown(kind: MediaDeviceKindEnum, label: string): ReactNode {
const devices = this.state.mediaDevices?.[kind].slice(0);
if (!devices?.length) return null;
2021-07-09 14:15:36 +02: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>
);
}
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) {
requestButton = (
<div>
<p>{_t("settings|voip|missing_permissions_prompt")}</p>
2021-07-09 12:57:42 +02:00
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{_t("settings|voip|request_permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = this.renderDropdown(
MediaDeviceKindEnum.AudioOutput,
_t("settings|voip|audio_output"),
) || <p>{_t("settings|voip|audio_output_empty")}</p>;
microphoneDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("common|microphone")) || (
<p>{_t("settings|voip|audio_input_empty")}</p>
2021-07-09 14:15:36 +02:00
);
webcamDropdown = this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("common|camera")) || (
<p>{_t("settings|voip|video_input_empty")}</p>
2021-07-09 14:15:36 +02:00
);
}
return (
<SettingsTab>
<SettingsSection>
{requestButton}
<SettingsSubsection heading={_t("settings|voip|voice_section")} stretchContent>
{speakerDropdown}
{microphoneDropdown}
<LabelledToggleSwitch
value={this.state.audioAutoGainControl}
onChange={async (v): Promise<void> => {
await MediaDeviceHandler.setAudioAutoGainControl(v);
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() });
}}
label={_t("settings|voip|voice_agc")}
data-testid="voice-auto-gain"
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("settings|voip|video_section")} stretchContent>
{webcamDropdown}
<SettingsFlag name="VideoView.flipVideoHorizontally" level={SettingLevel.ACCOUNT} />
</SettingsSubsection>
</SettingsSection>
2022-11-09 21:14:55 +01:00
<SettingsSection heading={_t("common|advanced")}>
<SettingsSubsection heading={_t("settings|voip|voice_processing")}>
2022-11-09 21:14:55 +01:00
<LabelledToggleSwitch
value={this.state.audioNoiseSuppression}
onChange={async (v): Promise<void> => {
2022-11-09 21:14:55 +01:00
await MediaDeviceHandler.setAudioNoiseSuppression(v);
this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() });
}}
label={_t("settings|voip|noise_suppression")}
2022-11-09 21:14:55 +01:00
data-testid="voice-noise-suppression"
/>
<LabelledToggleSwitch
value={this.state.audioEchoCancellation}
onChange={async (v): Promise<void> => {
2022-11-09 21:14:55 +01:00
await MediaDeviceHandler.setAudioEchoCancellation(v);
this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() });
}}
label={_t("settings|voip|echo_cancellation")}
2022-11-09 21:14:55 +01:00
data-testid="voice-echo-cancellation"
/>
</SettingsSubsection>
<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"
label={_t("settings|voip|enable_fallback_ice_server", {
server: new URL(FALLBACK_ICE_SERVER).pathname,
})}
2022-11-09 21:14:55 +01:00
level={SettingLevel.DEVICE}
hideIfCannotSet
2022-11-09 21:14:55 +01:00
/>
</SettingsSubsection>
</SettingsSection>
</SettingsTab>
);
}
}