2019-01-24 13:33:22 -07:00
|
|
|
/*
|
|
|
|
|
Copyright 2019 New Vector Ltd
|
2020-07-10 19:07:11 +01:00
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
2019-01-24 13:33:22 -07:00
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React from 'react';
|
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 { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
|
|
|
|
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';
|
2019-01-24 13:33:22 -07:00
|
|
|
|
2021-07-09 14:15:36 +02:00
|
|
|
const getDefaultDevice = (devices: Array<Partial<MediaDeviceInfo>>) => {
|
|
|
|
|
// Note we're looking for a device with deviceId 'default' but adding a device
|
|
|
|
|
// with deviceId == the empty string: this is because Chrome gives us a device
|
|
|
|
|
// with deviceId 'default', so we're looking for this, not the one we are adding.
|
|
|
|
|
if (!devices.some((i) => i.deviceId === 'default')) {
|
2021-07-09 14:19:27 +02:00
|
|
|
devices.unshift({ deviceId: '', label: _t('Default Device') });
|
2021-07-09 14:15:36 +02:00
|
|
|
return '';
|
|
|
|
|
} else {
|
|
|
|
|
return 'default';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-09 21:14:55 +01:00
|
|
|
interface IState {
|
2021-07-09 12:57:42 +02:00
|
|
|
mediaDevices: IMediaDevices;
|
2022-11-09 21:14:55 +01:00
|
|
|
[MediaDeviceKindEnum.AudioOutput]: string;
|
|
|
|
|
[MediaDeviceKindEnum.AudioInput]: string;
|
|
|
|
|
[MediaDeviceKindEnum.VideoInput]: string;
|
|
|
|
|
audioAutoGainControl: boolean;
|
|
|
|
|
audioEchoCancellation: boolean;
|
|
|
|
|
audioNoiseSuppression: boolean;
|
2021-07-09 12:57:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
|
2021-07-09 14:15:36 +02:00
|
|
|
constructor(props: {}) {
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2019-06-26 18:54:15 +02:00
|
|
|
async componentDidMount() {
|
2021-06-23 09:26:33 +02:00
|
|
|
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
2019-06-26 18:54:15 +02:00
|
|
|
if (canSeeDeviceLabels) {
|
2021-07-09 12:57:42 +02:00
|
|
|
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({
|
2021-06-23 09:26:33 +02:00
|
|
|
mediaDevices: await MediaDeviceHandler.getDevices(),
|
2021-07-09 13:45:39 +02:00
|
|
|
[MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(),
|
|
|
|
|
[MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(),
|
|
|
|
|
[MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(),
|
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) {
|
2021-07-09 12:57:42 +02:00
|
|
|
this.refreshMediaDevices(stream);
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-09 14:15:36 +02:00
|
|
|
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => {
|
|
|
|
|
MediaDeviceHandler.instance.setDevice(deviceId, kind);
|
|
|
|
|
this.setState<null>({ [kind]: deviceId });
|
2019-01-24 13:33:22 -07:00
|
|
|
};
|
|
|
|
|
|
2021-07-09 13:45:39 +02:00
|
|
|
private changeWebRtcMethod = (p2p: boolean): void => {
|
2019-01-24 20:57:40 -07:00
|
|
|
MatrixClientPeg.get().setForceTURN(!p2p);
|
2019-01-24 13:33:22 -07:00
|
|
|
};
|
|
|
|
|
|
2021-07-09 13:45:39 +02:00
|
|
|
private changeFallbackICEServerAllowed = (allow: boolean): void => {
|
2019-08-14 14:02:25 +01:00
|
|
|
MatrixClientPeg.get().setFallbackICEServerAllowed(allow);
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2021-07-09 14:15:36 +02:00
|
|
|
private renderDropdown(kind: MediaDeviceKindEnum, label: string): JSX.Element {
|
|
|
|
|
const devices = this.state.mediaDevices[kind].slice(0);
|
|
|
|
|
if (devices.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const defaultDevice = getDefaultDevice(devices);
|
|
|
|
|
return (
|
|
|
|
|
<Field
|
|
|
|
|
element="select"
|
|
|
|
|
label={label}
|
|
|
|
|
value={this.state[kind] || defaultDevice}
|
|
|
|
|
onChange={(e) => this.setDevice(e.target.value, kind)}
|
|
|
|
|
>
|
|
|
|
|
{ this.renderDeviceOptions(devices, kind) }
|
|
|
|
|
</Field>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-24 13:33:22 -07:00
|
|
|
render() {
|
|
|
|
|
let requestButton = null;
|
|
|
|
|
let speakerDropdown = null;
|
|
|
|
|
let microphoneDropdown = null;
|
|
|
|
|
let webcamDropdown = null;
|
2021-07-09 12:57:42 +02:00
|
|
|
if (!this.state.mediaDevices) {
|
2019-01-24 13:33:22 -07:00
|
|
|
requestButton = (
|
2019-02-22 10:47:18 -07:00
|
|
|
<div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
|
2021-07-19 22:43:11 +01:00
|
|
|
<p>{ _t("Missing media permissions, click the button below to request.") }</p>
|
2021-07-09 12:57:42 +02:00
|
|
|
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
|
2021-07-19 22:43:11 +01:00
|
|
|
{ _t("Request media permissions") }
|
2019-01-24 13:33:22 -07:00
|
|
|
</AccessibleButton>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (this.state.mediaDevices) {
|
2021-07-09 14:15:36 +02:00
|
|
|
speakerDropdown = (
|
|
|
|
|
this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) ||
|
|
|
|
|
<p>{ _t('No Audio Outputs detected') }</p>
|
|
|
|
|
);
|
|
|
|
|
microphoneDropdown = (
|
|
|
|
|
this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("Microphone")) ||
|
|
|
|
|
<p>{ _t('No Microphones detected') }</p>
|
|
|
|
|
);
|
|
|
|
|
webcamDropdown = (
|
|
|
|
|
this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("Camera")) ||
|
|
|
|
|
<p>{ _t('No Webcams detected') }</p>
|
|
|
|
|
);
|
2019-01-24 13:33:22 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2019-02-22 10:47:18 -07:00
|
|
|
<div className="mx_SettingsTab mx_VoiceUserSettingsTab">
|
2021-07-19 22:43:11 +01:00
|
|
|
<div className="mx_SettingsTab_heading">{ _t("Voice & Video") }</div>
|
2022-11-09 21:14:55 +01:00
|
|
|
{ requestButton }
|
2019-01-24 13:33:22 -07:00
|
|
|
<div className="mx_SettingsTab_section">
|
2022-11-09 21:14:55 +01:00
|
|
|
<span className="mx_SettingsTab_subheading">{ _t("Voice settings") }</span>
|
2021-07-09 14:15:36 +02:00
|
|
|
{ speakerDropdown }
|
|
|
|
|
{ microphoneDropdown }
|
2022-11-09 21:14:55 +01:00
|
|
|
<LabelledToggleSwitch
|
|
|
|
|
value={this.state.audioAutoGainControl}
|
|
|
|
|
onChange={async (v) => {
|
|
|
|
|
await MediaDeviceHandler.setAudioAutoGainControl(v);
|
|
|
|
|
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() });
|
|
|
|
|
}}
|
|
|
|
|
label={_t("Automatically adjust the microphone volume")}
|
|
|
|
|
data-testid='voice-auto-gain'
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mx_SettingsTab_section">
|
|
|
|
|
<span className="mx_SettingsTab_subheading">{ _t("Video settings") }</span>
|
2021-07-09 14:15:36 +02:00
|
|
|
{ webcamDropdown }
|
2019-01-24 13:33:22 -07:00
|
|
|
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
|
2022-11-09 21:14:55 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
|
|
|
|
|
<div className="mx_SettingsTab_section">
|
|
|
|
|
<span className="mx_SettingsTab_subheading">{ _t("Voice processing") }</span>
|
|
|
|
|
<div className="mx_SettingsTab_section">
|
|
|
|
|
<LabelledToggleSwitch
|
|
|
|
|
value={this.state.audioNoiseSuppression}
|
|
|
|
|
onChange={async (v) => {
|
|
|
|
|
await MediaDeviceHandler.setAudioNoiseSuppression(v);
|
|
|
|
|
this.setState({ audioNoiseSuppression: MediaDeviceHandler.getAudioNoiseSuppression() });
|
|
|
|
|
}}
|
|
|
|
|
label={_t("Noise suppression")}
|
|
|
|
|
data-testid='voice-noise-suppression'
|
|
|
|
|
/>
|
|
|
|
|
<LabelledToggleSwitch
|
|
|
|
|
value={this.state.audioEchoCancellation}
|
|
|
|
|
onChange={async (v) => {
|
|
|
|
|
await MediaDeviceHandler.setAudioEchoCancellation(v);
|
|
|
|
|
this.setState({ audioEchoCancellation: MediaDeviceHandler.getAudioEchoCancellation() });
|
|
|
|
|
}}
|
|
|
|
|
label={_t("Echo cancellation")}
|
|
|
|
|
data-testid='voice-echo-cancellation'
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mx_SettingsTab_section">
|
|
|
|
|
<span className="mx_SettingsTab_subheading">{ _t("Connection") }</span>
|
|
|
|
|
<SettingsFlag
|
|
|
|
|
name='webRtcAllowPeerToPeer'
|
|
|
|
|
level={SettingLevel.DEVICE}
|
|
|
|
|
onChange={this.changeWebRtcMethod}
|
|
|
|
|
/>
|
|
|
|
|
<SettingsFlag
|
|
|
|
|
name='fallbackICEServerAllowed'
|
|
|
|
|
level={SettingLevel.DEVICE}
|
|
|
|
|
onChange={this.changeFallbackICEServerAllowed}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2019-01-24 13:33:22 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|