/* Copyright 2024 New Vector Ltd. Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2019 New Vector Ltd 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"; import { logger } from "matrix-js-sdk/src/logger"; import { FALLBACK_ICE_SERVER } from "matrix-js-sdk/src/webrtc/call"; import { _t } from "../../../../../languageHandler"; import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler"; import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import SettingsFlag from "../../../elements/SettingsFlag"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; 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"; interface IState { mediaDevices: IMediaDevices | null; [MediaDeviceKindEnum.AudioOutput]: string | null; [MediaDeviceKindEnum.AudioInput]: string | null; [MediaDeviceKindEnum.VideoInput]: string | null; audioAutoGainControl: boolean; audioEchoCancellation: boolean; audioNoiseSuppression: boolean; } /** * 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(); } }; export default class VoiceUserSettingsTab extends React.Component<{}, IState> { public static contextType = MatrixClientContext; public declare context: React.ContextType; public constructor(props: {}, context: React.ContextType) { super(props, context); this.state = { mediaDevices: null, [MediaDeviceKindEnum.AudioOutput]: null, [MediaDeviceKindEnum.AudioInput]: null, [MediaDeviceKindEnum.VideoInput]: null, audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl(), audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation(), audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression(), }; } public async componentDidMount(): Promise { const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices(); if (canSeeDeviceLabels) { await this.refreshMediaDevices(); } } private refreshMediaDevices = async (stream?: MediaStream): Promise => { this.setState({ mediaDevices: (await MediaDeviceHandler.getDevices()) ?? null, [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()); } }; private requestMediaPermissions = async (): Promise => { const stream = await requestMediaPermissions(); if (stream) { await this.refreshMediaDevices(stream); } }; private setDevice = async (deviceId: string, kind: MediaDeviceKindEnum): Promise => { // set state immediately so UI is responsive this.setState({ [kind]: deviceId }); try { await MediaDeviceHandler.instance.setDevice(deviceId, kind); } catch { logger.error(`Failed to set device ${kind}: ${deviceId}`); // reset state to current value this.setState({ [kind]: mapDeviceKindToHandlerValue(kind) }); } }; private changeWebRtcMethod = (p2p: boolean): void => { this.context.setForceTURN(!p2p); }; private renderDeviceOptions(devices: Array, category: MediaDeviceKindEnum): Array { return devices.map((d) => { return ( ); }); } private renderDropdown(kind: MediaDeviceKindEnum, label: string): ReactNode { const devices = this.state.mediaDevices?.[kind].slice(0); if (!devices?.length) return null; const defaultDevice = MediaDeviceHandler.getDefaultDevice(devices); return ( this.setDevice(e.target.value, kind)} > {this.renderDeviceOptions(devices, kind)} ); } public render(): ReactNode { let requestButton: ReactNode | undefined; let speakerDropdown: ReactNode | undefined; let microphoneDropdown: ReactNode | undefined; let webcamDropdown: ReactNode | undefined; if (!this.state.mediaDevices) { requestButton = (

{_t("settings|voip|missing_permissions_prompt")}

{_t("settings|voip|request_permissions")}
); } else if (this.state.mediaDevices) { speakerDropdown = this.renderDropdown( MediaDeviceKindEnum.AudioOutput, _t("settings|voip|audio_output"), ) ||

{_t("settings|voip|audio_output_empty")}

; microphoneDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("common|microphone")) || (

{_t("settings|voip|audio_input_empty")}

); webcamDropdown = this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("common|camera")) || (

{_t("settings|voip|video_input_empty")}

); } return ( {requestButton} {speakerDropdown} {microphoneDropdown} => { await MediaDeviceHandler.setAudioAutoGainControl(v); this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() }); }} label={_t("settings|voip|voice_agc")} data-testid="voice-auto-gain" /> {webcamDropdown} => { await MediaDeviceHandler.setAudioNoiseSuppression(v); this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() }); }} label={_t("settings|voip|noise_suppression")} data-testid="voice-noise-suppression" /> => { await MediaDeviceHandler.setAudioEchoCancellation(v); this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() }); }} label={_t("settings|voip|echo_cancellation")} data-testid="voice-echo-cancellation" /> ); } }