diff --git a/css/main.scss b/css/main.scss
index da90c9648..3a3bf7570 100644
--- a/css/main.scss
+++ b/css/main.scss
@@ -33,7 +33,6 @@ $flagsImagePath: "../images/";
@import 'reload_overlay/reload_overlay';
@import 'mini_toolbox';
@import 'modals/desktop-picker/desktop-picker';
-@import 'modals/device-selection/device-selection';
@import 'modals/dialog';
@import 'modals/embed-meeting/embed-meeting';
@import 'modals/feedback/feedback';
diff --git a/css/modals/device-selection/_device-selection.scss b/css/modals/device-selection/_device-selection.scss
deleted file mode 100644
index a7d48c042..000000000
--- a/css/modals/device-selection/_device-selection.scss
+++ /dev/null
@@ -1,148 +0,0 @@
-.device-selection {
- .device-selectors {
- font-size: 14px;
-
- > div {
- display: block;
- margin-bottom: 4px;
- }
-
- .device-selector-icon {
- align-self: center;
- color: inherit;
- font-size: 20px;
- margin-left: 3px;
- }
-
- .device-selector-label {
- margin-bottom: 1px;
- }
-
- /* device-selector-trigger stylings attempt to mimic AtlasKit button */
- .device-selector-trigger {
- background-color: #0E1624;
- border: 1px solid #455166;
- border-radius: 5px;
- display: flex;
- height: 2.3em;
- justify-content: space-between;
- line-height: 2.3em;
- overflow: hidden;
- padding: 0 8px;
- }
- .device-selector-trigger-disabled {
- .device-selector-trigger {
- color: #a5adba;
- cursor: default;
- }
- }
-
- .device-selector-trigger-text {
- overflow: hidden;
- text-align: center;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
- }
-
- .device-selection-column {
- box-sizing: border-box;
- display: inline-block;
- vertical-align: top;
-
- &.column-selectors {
- margin-left: 15px;
- width: 45%;
- }
-
- &.column-video {
- width: 50%;
- }
- }
-
- .device-selection-video-container {
- border-radius: 3px;
- margin-bottom: 5px;
-
- .video-input-preview {
- margin-top: 2px;
- position: relative;
-
- > video {
- border-radius: 3px;
- }
-
- .video-input-preview-error {
- color: $participantNameColor;
- display: none;
- left: 0;
- position: absolute;
- right: 0;
- text-align: center;
- top: 50%;
- }
-
- &.video-preview-has-error {
- background: black;
-
- .video-input-preview-error {
- display: block;
- }
- }
-
- .video-input-preview-display {
- height: auto;
- overflow: hidden;
- width: 100%;
- }
- }
- }
-
- .audio-output-preview {
- font-size: 14px;
-
- a {
- color: #6FB1EA;
- cursor: pointer;
- text-decoration: none;
- }
-
- a:hover {
- color: #B3D4FF;
- }
- }
-
- .audio-input-preview {
- background: #1B2638;
- border-radius: 5px;
- height: 8px;
-
- .audio-input-preview-level {
- background: #75B1FF;
- border-radius: 5px;
- height: 100%;
- -webkit-transition: width .1s ease-in-out;
- -moz-transition: width .1s ease-in-out;
- -o-transition: width .1s ease-in-out;
- transition: width .1s ease-in-out;
- }
- }
-}
-
-.device-selection.video-hidden {
- display: flex;
- flex-direction: column;
- width: 100%;
-
- .column-selectors {
- width: 100%;
- margin-left: 0;
- }
-
- .column-video {
- order: 1;
- width: 100%;
- margin-top: 8px;
- }
-}
diff --git a/lang/main.json b/lang/main.json
index 7c60c96b6..f9e3e520d 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -220,7 +220,7 @@
"noPermission": "Permission not granted",
"previewUnavailable": "Preview unavailable",
"selectADevice": "Select a device",
- "testAudio": "Play a test sound"
+ "testAudio": "Test"
},
"dialIn": {
"screenTitle": "Dial-in summary"
@@ -971,6 +971,7 @@
"title": "Security Options"
},
"settings": {
+ "audio": "Audio",
"buttonLabel": "Settings",
"calendar": {
"about": "The {{appName}} calendar integration is used to securely access your calendar so it can read upcoming events.",
@@ -1012,7 +1013,8 @@
"startReactionsMuted": "Mute reaction sounds for everyone",
"startVideoMuted": "Everyone starts hidden",
"talkWhileMuted": "Talk while muted",
- "title": "Settings"
+ "title": "Settings",
+ "video": "Video"
},
"settingsView": {
"advanced": "Advanced",
@@ -1171,6 +1173,7 @@
"download": "Download our apps",
"e2ee": "End-to-End Encryption",
"embedMeeting": "Embed meeting",
+ "enableNoiseSuppression": "Enable noise suppression",
"endConference": "End meeting for all",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
diff --git a/react/features/base/ui/components/web/DialogWithTabs.tsx b/react/features/base/ui/components/web/DialogWithTabs.tsx
index c798169d8..c12a79ac1 100644
--- a/react/features/base/ui/components/web/DialogWithTabs.tsx
+++ b/react/features/base/ui/components/web/DialogWithTabs.tsx
@@ -89,6 +89,10 @@ const useStyles = makeStyles()(theme => {
}
},
+ closeButtonContainer: {
+ paddingBottom: theme.spacing(4)
+ },
+
buttonContainer: {
width: '100%',
boxSizing: 'border-box',
@@ -144,20 +148,20 @@ interface IObject {
[key: string]: string | string[] | boolean | number | number[] | {} | undefined;
}
-export interface IDialogTab {
+export interface IDialogTab
{
className?: string;
component: ComponentType;
icon: Function;
labelKey: string;
name: string;
props?: IObject;
- propsUpdateFunction?: (tabState: IObject, newProps: IObject) => IObject;
+ propsUpdateFunction?: (tabState: IObject, newProps: P) => P;
submit?: Function;
}
interface IProps extends IBaseProps {
defaultTab?: string;
- tabs: IDialogTab[];
+ tabs: IDialogTab[];
}
const DialogWithTabs = ({
@@ -294,7 +298,7 @@ const DialogWithTabs = ({
)}
{(!isMobile || selectedTab) && (
-
+
{isMobile && (
{
width: '100%',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
- padding: '8px 16px',
+ padding: '10px 16px',
paddingRight: '42px',
border: 0,
appearance: 'none',
diff --git a/react/features/device-selection/actions.web.ts b/react/features/device-selection/actions.web.ts
index e0658da67..6dd289975 100644
--- a/react/features/device-selection/actions.web.ts
+++ b/react/features/device-selection/actions.web.ts
@@ -7,31 +7,23 @@ import {
} from '../base/devices/actions';
import { getDeviceLabelById, setAudioOutputDeviceId } from '../base/devices/functions';
import { updateSettings } from '../base/settings/actions';
+import { toggleNoiseSuppression } from '../noise-suppression/actions';
+import { setScreenshareFramerate } from '../screen-share/actions';
-import { getDeviceSelectionDialogProps } from './functions';
+import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from './functions';
import logger from './logger';
/**
- * Submits the settings related to device selection.
+ * Submits the settings related to audio device selection.
*
* @param {Object} newState - The new settings.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
* welcome page or not.
* @returns {Function}
*/
-export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
+export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
- const currentState = getDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
-
- if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
- dispatch(updateSettings({
- userSelectedCameraDeviceId: newState.selectedVideoInputId,
- userSelectedCameraDeviceLabel:
- getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
- }));
-
- dispatch(setVideoInputDevice(newState.selectedVideoInputId));
- }
+ const currentState = getAudioDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
dispatch(updateSettings({
@@ -44,8 +36,8 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
}
if (newState.selectedAudioOutputId
- && newState.selectedAudioOutputId
- !== currentState.selectedAudioOutputId) {
+ && newState.selectedAudioOutputId
+ !== currentState.selectedAudioOutputId) {
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
setAudioOutputDeviceId(
@@ -62,5 +54,45 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
err);
});
}
+
+ if (newState.noiseSuppressionEnabled !== currentState.noiseSuppressionEnabled) {
+ dispatch(toggleNoiseSuppression());
+ }
+ };
+}
+
+/**
+ * Submits the settings related to device selection.
+ *
+ * @param {Object} newState - The new settings.
+ * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
+ * welcome page or not.
+ * @returns {Function}
+ */
+export function submitVideoDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
+ return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
+ const currentState = getVideoDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
+
+ if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
+ dispatch(updateSettings({
+ userSelectedCameraDeviceId: newState.selectedVideoInputId,
+ userSelectedCameraDeviceLabel:
+ getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
+ }));
+
+ dispatch(setVideoInputDevice(newState.selectedVideoInputId));
+ }
+
+ if (newState.localFlipX !== currentState.localFlipX) {
+ dispatch(updateSettings({
+ localFlipX: newState.localFlipX
+ }));
+ }
+
+ if (newState.currentFramerate !== currentState.currentFramerate) {
+ const frameRate = parseInt(newState.currentFramerate, 10);
+
+ dispatch(setScreenshareFramerate(frameRate));
+ }
};
}
diff --git a/react/features/device-selection/components/AudioDevicesSelection.web.tsx b/react/features/device-selection/components/AudioDevicesSelection.web.tsx
new file mode 100644
index 000000000..573605897
--- /dev/null
+++ b/react/features/device-selection/components/AudioDevicesSelection.web.tsx
@@ -0,0 +1,387 @@
+import { Theme } from '@mui/material';
+import { withStyles } from '@mui/styles';
+import React from 'react';
+import { WithTranslation } from 'react-i18next';
+import { connect } from 'react-redux';
+
+import { IReduxState, IStore } from '../../app/types';
+import { getAvailableDevices } from '../../base/devices/actions.web';
+import AbstractDialogTab, {
+ type IProps as AbstractDialogTabProps
+} from '../../base/dialog/components/web/AbstractDialogTab';
+import { translate } from '../../base/i18n/functions';
+import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
+import Checkbox from '../../base/ui/components/web/Checkbox';
+import logger from '../logger';
+
+import AudioInputPreview from './AudioInputPreview';
+import AudioOutputPreview from './AudioOutputPreview';
+import DeviceHidContainer from './DeviceHidContainer.web';
+import DeviceSelector from './DeviceSelector.web';
+
+/**
+ * The type of the React {@code Component} props of {@link AudioDevicesSelection}.
+ */
+interface IProps extends AbstractDialogTabProps, WithTranslation {
+
+ /**
+ * All known audio and video devices split by type. This prop comes from
+ * the app state.
+ */
+ availableDevices: {
+ audioInput?: MediaDeviceInfo[];
+ audioOutput?: MediaDeviceInfo[];
+ };
+
+ /**
+ * CSS classes object.
+ */
+ classes: any;
+
+ /**
+ * Whether or not the audio selector can be interacted with. If true,
+ * the audio input selector will be rendered as disabled. This is
+ * specifically used to prevent audio device changing in Firefox, which
+ * currently does not work due to a browser-side regression.
+ */
+ disableAudioInputChange: boolean;
+
+ /**
+ * True if device changing is configured to be disallowed. Selectors
+ * will display as disabled.
+ */
+ disableDeviceChange: boolean;
+
+ /**
+ * Redux dispatch function.
+ */
+ dispatch: IStore['dispatch'];
+
+ /**
+ * Whether or not the audio permission was granted.
+ */
+ hasAudioPermission: boolean;
+
+ /**
+ * If true, the audio meter will not display. Necessary for browsers or
+ * configurations that do not support local stats to prevent a
+ * non-responsive mic preview from displaying.
+ */
+ hideAudioInputPreview: boolean;
+
+ /**
+ * If true, the button to play a test sound on the selected speaker will not be displayed.
+ * This needs to be hidden on browsers that do not support selecting an audio output device.
+ */
+ hideAudioOutputPreview: boolean;
+
+ /**
+ * Whether or not the audio output source selector should display. If
+ * true, the audio output selector and test audio link will not be
+ * rendered.
+ */
+ hideAudioOutputSelect: boolean;
+
+ /**
+ * Whether or not the hid device container should display.
+ */
+ hideDeviceHIDContainer: boolean;
+
+ /**
+ * Whether to hide noise suppression checkbox or not.
+ */
+ hideNoiseSuppression: boolean;
+
+ /**
+ * Wether noise suppression is on or not.
+ */
+ noiseSuppressionEnabled: boolean;
+
+ /**
+ * The id of the audio input device to preview.
+ */
+ selectedAudioInputId: string;
+
+ /**
+ * The id of the audio output device to preview.
+ */
+ selectedAudioOutputId: string;
+}
+
+/**
+ * The type of the React {@code Component} state of {@link AudioDevicesSelection}.
+ */
+type State = {
+
+ /**
+ * The JitsiTrack to use for previewing audio input.
+ */
+ previewAudioTrack?: any | null;
+};
+
+const styles = (theme: Theme) => {
+ return {
+ container: {
+ display: 'flex',
+ flexDirection: 'column' as const,
+ padding: '0 2px',
+ width: '100%'
+ },
+
+ inputContainer: {
+ marginBottom: theme.spacing(3)
+ },
+
+ outputContainer: {
+ margin: `${theme.spacing(5)} 0`,
+ display: 'flex',
+ alignItems: 'flex-end'
+ },
+
+ outputButton: {
+ marginLeft: theme.spacing(3)
+ },
+
+ noiseSuppressionContainer: {
+ marginBottom: theme.spacing(5)
+ }
+ };
+};
+
+/**
+ * React {@code Component} for previewing audio and video input/output devices.
+ *
+ * @augments Component
+ */
+class AudioDevicesSelection extends AbstractDialogTab {
+
+ /**
+ * Whether current component is mounted or not.
+ *
+ * In component did mount we start a Promise to create tracks and
+ * set the tracks in the state, if we unmount the component in the meanwhile
+ * tracks will be created and will never been disposed (dispose tracks is
+ * in componentWillUnmount). When tracks are created and component is
+ * unmounted we dispose the tracks.
+ */
+ _unMounted: boolean;
+
+ /**
+ * Initializes a new DeviceSelection instance.
+ *
+ * @param {Object} props - The read-only React Component props with which
+ * the new instance is to be initialized.
+ */
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ previewAudioTrack: null
+ };
+ this._unMounted = true;
+ }
+
+ /**
+ * Generate the initial previews for audio input and video input.
+ *
+ * @inheritdoc
+ */
+ componentDidMount() {
+ this._unMounted = false;
+ Promise.all([
+ this._createAudioInputTrack(this.props.selectedAudioInputId)
+ ])
+ .catch(err => logger.warn('Failed to initialize preview tracks', err))
+ .then(() => {
+ this.props.dispatch(getAvailableDevices());
+ });
+ }
+
+ /**
+ * Checks if audio / video permissions were granted. Updates audio input and
+ * video input previews.
+ *
+ * @param {Object} prevProps - Previous props this component received.
+ * @returns {void}
+ */
+ componentDidUpdate(prevProps: IProps) {
+ if (prevProps.selectedAudioInputId
+ !== this.props.selectedAudioInputId) {
+ this._createAudioInputTrack(this.props.selectedAudioInputId);
+ }
+ }
+
+ /**
+ * Ensure preview tracks are destroyed to prevent continued use.
+ *
+ * @inheritdoc
+ */
+ componentWillUnmount() {
+ this._unMounted = true;
+ this._disposeAudioInputPreview();
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ */
+ render() {
+ const {
+ classes,
+ hasAudioPermission,
+ hideAudioInputPreview,
+ hideAudioOutputPreview,
+ hideDeviceHIDContainer,
+ hideNoiseSuppression,
+ noiseSuppressionEnabled,
+ selectedAudioOutputId,
+ t
+ } = this.props;
+ const { audioInput, audioOutput } = this._getSelectors();
+
+ return (
+
+
+ {this._renderSelector(audioInput)}
+
+ {!hideAudioInputPreview && hasAudioPermission
+ &&
}
+
+ {this._renderSelector(audioOutput)}
+ {!hideAudioOutputPreview && hasAudioPermission
+ &&
}
+
+ {!hideNoiseSuppression && (
+
+ super._onChange({
+ noiseSuppressionEnabled: !noiseSuppressionEnabled
+ }) } />
+
+ )}
+ {!hideDeviceHIDContainer
+ &&
}
+
+ );
+ }
+
+ /**
+ * Creates the JitsiTrack for the audio input preview.
+ *
+ * @param {string} deviceId - The id of audio input device to preview.
+ * @private
+ * @returns {void}
+ */
+ _createAudioInputTrack(deviceId: string) {
+ const { hideAudioInputPreview } = this.props;
+
+ if (hideAudioInputPreview) {
+ return;
+ }
+
+ return this._disposeAudioInputPreview()
+ .then(() => createLocalTrack('audio', deviceId, 5000))
+ .then(jitsiLocalTrack => {
+ if (this._unMounted) {
+ jitsiLocalTrack.dispose();
+
+ return;
+ }
+
+ this.setState({
+ previewAudioTrack: jitsiLocalTrack
+ });
+ })
+ .catch(() => {
+ this.setState({
+ previewAudioTrack: null
+ });
+ });
+ }
+
+ /**
+ * Utility function for disposing the current audio input preview.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ _disposeAudioInputPreview(): Promise {
+ return this.state.previewAudioTrack
+ ? this.state.previewAudioTrack.dispose() : Promise.resolve();
+ }
+
+ /**
+ * Creates a DeviceSelector instance based on the passed in configuration.
+ *
+ * @private
+ * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
+ * @returns {ReactElement}
+ */
+ _renderSelector(deviceSelectorProps: any) {
+ return deviceSelectorProps ? (
+
+ ) : null;
+ }
+
+ /**
+ * Returns object configurations for audio input and output.
+ *
+ * @private
+ * @returns {Object} Configurations.
+ */
+ _getSelectors() {
+ const { availableDevices, hasAudioPermission } = this.props;
+
+ const audioInput = {
+ devices: availableDevices.audioInput,
+ hasPermission: hasAudioPermission,
+ icon: 'icon-microphone',
+ isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
+ key: 'audioInput',
+ id: 'audioInput',
+ label: 'settings.selectMic',
+ onSelect: (selectedAudioInputId: string) => super._onChange({ selectedAudioInputId }),
+ selectedDeviceId: this.state.previewAudioTrack
+ ? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
+ };
+ let audioOutput;
+
+ if (!this.props.hideAudioOutputSelect) {
+ audioOutput = {
+ devices: availableDevices.audioOutput,
+ hasPermission: hasAudioPermission,
+ icon: 'icon-speaker',
+ isDisabled: this.props.disableDeviceChange,
+ key: 'audioOutput',
+ id: 'audioOutput',
+ label: 'settings.selectAudioOutput',
+ onSelect: (selectedAudioOutputId: string) => super._onChange({ selectedAudioOutputId }),
+ selectedDeviceId: this.props.selectedAudioOutputId
+ };
+ }
+
+ return { audioInput,
+ audioOutput };
+ }
+}
+
+const mapStateToProps = (state: IReduxState) => {
+ return {
+ availableDevices: state['features/base/devices'].availableDevices ?? {}
+ };
+};
+
+export default connect(mapStateToProps)(withStyles(styles)(translate(AudioDevicesSelection)));
diff --git a/react/features/device-selection/components/AudioInputPreview.js b/react/features/device-selection/components/AudioInputPreview.js
deleted file mode 100644
index 5c0df3ce2..000000000
--- a/react/features/device-selection/components/AudioInputPreview.js
+++ /dev/null
@@ -1,150 +0,0 @@
-/* @flow */
-
-import React, { Component } from 'react';
-
-import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
-
-const JitsiTrackEvents = JitsiMeetJS.events.track;
-
-/**
- * The type of the React {@code Component} props of {@link AudioInputPreview}.
- */
-type Props = {
-
- /**
- * The JitsiLocalTrack to show an audio level meter for.
- */
- track: Object
-};
-
-/**
- * The type of the React {@code Component} props of {@link AudioInputPreview}.
- */
-type State = {
-
- /**
- * The current audio input level being received, from 0 to 1.
- */
- audioLevel: number
-};
-
-/**
- * React component for displaying a audio level meter for a JitsiLocalTrack.
- */
-class AudioInputPreview extends Component {
- /**
- * Initializes a new AudioInputPreview instance.
- *
- * @param {Object} props - The read-only React Component props with which
- * the new instance is to be initialized.
- */
- constructor(props: Props) {
- super(props);
-
- this.state = {
- audioLevel: 0
- };
-
- this._updateAudioLevel = this._updateAudioLevel.bind(this);
- }
-
- /**
- * Starts listening for audio level updates after the initial render.
- *
- * @inheritdoc
- * @returns {void}
- */
- componentDidMount() {
- this._listenForAudioUpdates(this.props.track);
- }
-
- /**
- * Stops listening for audio level updates on the old track and starts
- * listening instead on the new track.
- *
- * @inheritdoc
- * @returns {void}
- */
- componentDidUpdate(prevProps: Props) {
- if (prevProps.track !== this.props.track) {
- this._listenForAudioUpdates(this.props.track);
- this._updateAudioLevel(0);
- }
- }
-
- /**
- * Unsubscribe from audio level updates.
- *
- * @inheritdoc
- * @returns {void}
- */
- componentWillUnmount() {
- this._stopListeningForAudioUpdates();
- }
-
- /**
- * Implements React's {@link Component#render()}.
- *
- * @inheritdoc
- * @returns {ReactElement}
- */
- render() {
- const audioMeterFill = {
- width: `${Math.floor(this.state.audioLevel * 100)}%`
- };
-
- return (
-
- );
- }
-
- /**
- * Starts listening for audio level updates from the library.
- *
- * @param {JitstiLocalTrack} track - The track to listen to for audio level
- * updates.
- * @private
- * @returns {void}
- */
- _listenForAudioUpdates(track) {
- this._stopListeningForAudioUpdates();
-
- track && track.on(
- JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
- this._updateAudioLevel);
- }
-
- /**
- * Stops listening to further updates from the current track.
- *
- * @private
- * @returns {void}
- */
- _stopListeningForAudioUpdates() {
- this.props.track && this.props.track.off(
- JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
- this._updateAudioLevel);
- }
-
- _updateAudioLevel: (number) => void;
-
- /**
- * Updates the internal state of the last know audio level. The level should
- * be between 0 and 1, as the level will be used as a percentage out of 1.
- *
- * @param {number} audioLevel - The new audio level for the track.
- * @private
- * @returns {void}
- */
- _updateAudioLevel(audioLevel) {
- this.setState({
- audioLevel
- });
- }
-}
-
-export default AudioInputPreview;
diff --git a/react/features/device-selection/components/AudioInputPreview.web.tsx b/react/features/device-selection/components/AudioInputPreview.web.tsx
new file mode 100644
index 000000000..ced99dedb
--- /dev/null
+++ b/react/features/device-selection/components/AudioInputPreview.web.tsx
@@ -0,0 +1,103 @@
+import React, { useEffect, useState } from 'react';
+import { makeStyles } from 'tss-react/mui';
+
+// @ts-ignore
+import JitsiMeetJS from '../../base/lib-jitsi-meet/_.web';
+
+const JitsiTrackEvents = JitsiMeetJS.events.track;
+
+/**
+ * The type of the React {@code Component} props of {@link AudioInputPreview}.
+ */
+interface IProps {
+
+ /**
+ * The JitsiLocalTrack to show an audio level meter for.
+ */
+ track: any;
+}
+
+const useStyles = makeStyles()(theme => {
+ return {
+ container: {
+ display: 'flex'
+ },
+
+ section: {
+ flex: 1,
+ height: '4px',
+ borderRadius: '1px',
+ backgroundColor: theme.palette.ui04,
+ marginRight: theme.spacing(1),
+
+ '&:last-of-type': {
+ marginRight: 0
+ }
+ },
+
+ activeSection: {
+ backgroundColor: theme.palette.success01
+ }
+ };
+});
+
+const NO_OF_PREVIEW_SECTIONS = 11;
+
+const AudioInputPreview = (props: IProps) => {
+ const [ audioLevel, setAudioLevel ] = useState(0);
+ const { classes, cx } = useStyles();
+
+ /**
+ * Starts listening for audio level updates from the library.
+ *
+ * @param {JitsiLocalTrack} track - The track to listen to for audio level
+ * updates.
+ * @private
+ * @returns {void}
+ */
+ function _listenForAudioUpdates(track: any) {
+ _stopListeningForAudioUpdates();
+
+ track?.on(
+ JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
+ setAudioLevel);
+ }
+
+ /**
+ * Stops listening to further updates from the current track.
+ *
+ * @private
+ * @returns {void}
+ */
+ function _stopListeningForAudioUpdates() {
+ props.track?.off(
+ JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
+ setAudioLevel);
+ }
+
+ useEffect(() => {
+ _listenForAudioUpdates(props.track);
+
+ return _stopListeningForAudioUpdates;
+ }, []);
+
+ useEffect(() => {
+ _listenForAudioUpdates(props.track);
+ setAudioLevel(0);
+ }, [ props.track ]);
+
+ const audioMeterFill = Math.ceil(Math.floor(audioLevel * 100) / (100 / NO_OF_PREVIEW_SECTIONS));
+
+ return (
+
+ {new Array(NO_OF_PREVIEW_SECTIONS).fill(0)
+ .map((_, idx) =>
+ (
)
+ )}
+
+ );
+};
+
+export default AudioInputPreview;
diff --git a/react/features/device-selection/components/AudioOutputPreview.js b/react/features/device-selection/components/AudioOutputPreview.web.tsx
similarity index 71%
rename from react/features/device-selection/components/AudioOutputPreview.js
rename to react/features/device-selection/components/AudioOutputPreview.web.tsx
index 73633d601..784765801 100644
--- a/react/features/device-selection/components/AudioOutputPreview.js
+++ b/react/features/device-selection/components/AudioOutputPreview.web.tsx
@@ -1,35 +1,38 @@
-/* @flow */
-
import React, { Component } from 'react';
+import { WithTranslation } from 'react-i18next';
import { translate } from '../../base/i18n/functions';
-import Audio from '../../base/media/components/Audio';
+// eslint-disable-next-line lines-around-comment
+// @ts-ignore
+import Audio from '../../base/media/components/Audio.web';
+import Button from '../../base/ui/components/web/Button';
+import { BUTTON_TYPES } from '../../base/ui/constants.any';
const TEST_SOUND_PATH = 'sounds/ring.mp3';
/**
* The type of the React {@code Component} props of {@link AudioOutputPreview}.
*/
-type Props = {
+interface IProps extends WithTranslation {
+
+ /**
+ * Button className.
+ */
+ className?: string;
/**
* The device id of the audio output device to use.
*/
- deviceId: string,
-
- /**
- * Invoked to obtain translated strings.
- */
- t: Function
-};
+ deviceId: string;
+}
/**
* React component for playing a test sound through a specified audio device.
*
* @augments Component
*/
-class AudioOutputPreview extends Component {
- _audioElement: ?Object;
+class AudioOutputPreview extends Component {
+ _audioElement: HTMLAudioElement | null;
/**
* Initializes a new AudioOutputPreview instance.
@@ -37,7 +40,7 @@ class AudioOutputPreview extends Component {
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
- constructor(props: Props) {
+ constructor(props: IProps) {
super(props);
this._audioElement = null;
@@ -66,24 +69,21 @@ class AudioOutputPreview extends Component {
*/
render() {
return (
-
+ >
);
}
- _audioElementReady: (Object) => void;
-
/**
* Sets the instance variable for the component's audio element so it can be
* accessed directly.
@@ -92,14 +92,12 @@ class AudioOutputPreview extends Component {
* @private
* @returns {void}
*/
- _audioElementReady(element: Object) {
+ _audioElementReady(element: HTMLAudioElement) {
this._audioElement = element;
this._setAudioSink();
}
- _onClick: () => void;
-
/**
* Plays a test sound.
*
@@ -107,12 +105,9 @@ class AudioOutputPreview extends Component {
* @returns {void}
*/
_onClick() {
- this._audioElement
- && this._audioElement.play();
+ this._audioElement?.play();
}
- _onKeyPress: (Object) => void;
-
/**
* KeyPress handler for accessibility.
*
@@ -120,7 +115,7 @@ class AudioOutputPreview extends Component {
*
* @returns {void}
*/
- _onKeyPress(e) {
+ _onKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClick();
@@ -135,7 +130,7 @@ class AudioOutputPreview extends Component {
*/
_setAudioSink() {
this._audioElement
- && this.props.deviceId
+ && this.props.deviceId // @ts-ignore
&& this._audioElement.setSinkId(this.props.deviceId);
}
}
diff --git a/react/features/device-selection/components/DeviceHidContainer.web.tsx b/react/features/device-selection/components/DeviceHidContainer.web.tsx
index 839cb02c0..b151ea07a 100644
--- a/react/features/device-selection/components/DeviceHidContainer.web.tsx
+++ b/react/features/device-selection/components/DeviceHidContainer.web.tsx
@@ -5,33 +5,40 @@ import { makeStyles } from 'tss-react/mui';
import Icon from '../../base/icons/components/Icon';
import { IconTrash } from '../../base/icons/svg';
+import { withPixelLineHeight } from '../../base/styles/functions.web';
import Button from '../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../base/ui/constants.any';
import { closeHidDevice, requestHidDevice } from '../../web-hid/actions';
import { getDeviceInfo, shouldRequestHIDDevice } from '../../web-hid/functions';
-const useStyles = makeStyles()(() => {
+const useStyles = makeStyles()(theme => {
return {
callControlContainer: {
- marginTop: '8px',
- marginBottom: '16px',
- fontSize: '14px',
- '> label': {
- display: 'block',
- marginBottom: '20px'
- }
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start'
},
+
+ label: {
+ ...withPixelLineHeight(theme.typography.bodyShortRegular),
+ color: theme.palette.text01,
+ marginBottom: theme.spacing(2)
+ },
+
deviceRow: {
display: 'flex',
justifyContent: 'space-between'
},
+
deleteDevice: {
cursor: 'pointer',
textAlign: 'center'
},
+
headerConnectedDevice: {
fontWeight: 600
},
+
hidContainer: {
'> span': {
marginLeft: '16px'
@@ -66,7 +73,7 @@ function DeviceHidContainer() {
className = { classes.callControlContainer }
key = 'callControl'>
@@ -77,7 +84,6 @@ function DeviceHidContainer() {
key = 'request-control-btn'
label = { t('deviceSelection.hid.pairDevice') }
onClick = { onRequestControl }
- size = 'small'
type = { BUTTON_TYPES.SECONDARY } />
)}
{!showRequestDeviceInfo && (
diff --git a/react/features/device-selection/components/DeviceSelection.js b/react/features/device-selection/components/DeviceSelection.js
deleted file mode 100644
index 0f004680a..000000000
--- a/react/features/device-selection/components/DeviceSelection.js
+++ /dev/null
@@ -1,429 +0,0 @@
-// @flow
-
-import React from 'react';
-
-import { getAvailableDevices } from '../../base/devices/actions.web';
-import AbstractDialogTab, {
- type Props as AbstractDialogTabProps
-} from '../../base/dialog/components/web/AbstractDialogTab';
-import { translate } from '../../base/i18n/functions';
-import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
-import logger from '../logger';
-
-import AudioInputPreview from './AudioInputPreview';
-import AudioOutputPreview from './AudioOutputPreview';
-import DeviceHidContainer from './DeviceHidContainer.web';
-import DeviceSelector from './DeviceSelector';
-import VideoInputPreview from './VideoInputPreview';
-
-/**
- * The type of the React {@code Component} props of {@link DeviceSelection}.
- */
-export type Props = {
- ...$Exact,
-
- /**
- * All known audio and video devices split by type. This prop comes from
- * the app state.
- */
- availableDevices: Object,
-
- /**
- * Whether or not the audio selector can be interacted with. If true,
- * the audio input selector will be rendered as disabled. This is
- * specifically used to prevent audio device changing in Firefox, which
- * currently does not work due to a browser-side regression.
- */
- disableAudioInputChange: boolean,
-
- /**
- * True if device changing is configured to be disallowed. Selectors
- * will display as disabled.
- */
- disableDeviceChange: boolean,
-
- /**
- * Whether video input dropdown should be enabled or not.
- */
- disableVideoInputSelect: boolean,
-
- /**
- * Whether or not the audio permission was granted.
- */
- hasAudioPermission: boolean,
-
- /**
- * Whether or not the audio permission was granted.
- */
- hasVideoPermission: boolean,
-
- /**
- * If true, the audio meter will not display. Necessary for browsers or
- * configurations that do not support local stats to prevent a
- * non-responsive mic preview from displaying.
- */
- hideAudioInputPreview: boolean,
-
- /**
- * If true, the button to play a test sound on the selected speaker will not be displayed.
- * This needs to be hidden on browsers that do not support selecting an audio output device.
- */
- hideAudioOutputPreview: boolean,
-
- /**
- * Whether or not the audio output source selector should display. If
- * true, the audio output selector and test audio link will not be
- * rendered.
- */
- hideAudioOutputSelect: boolean,
-
- /**
- * Whether or not the hid device container should display.
- */
- hideDeviceHIDContainer: boolean,
-
- /**
- * Whether video input preview should be displayed or not.
- * (In the case of iOS Safari).
- */
- hideVideoInputPreview: boolean,
-
- /**
- * The id of the audio input device to preview.
- */
- selectedAudioInputId: string,
-
- /**
- * The id of the audio output device to preview.
- */
- selectedAudioOutputId: string,
-
- /**
- * The id of the video input device to preview.
- */
- selectedVideoInputId: string,
-
- /**
- * Invoked to obtain translated strings.
- */
- t: Function
-};
-
-/**
- * The type of the React {@code Component} state of {@link DeviceSelection}.
- */
-type State = {
-
- /**
- * The JitsiTrack to use for previewing audio input.
- */
- previewAudioTrack: ?Object,
-
- /**
- * The JitsiTrack to use for previewing video input.
- */
- previewVideoTrack: ?Object,
-
- /**
- * The error message from trying to use a video input device.
- */
- previewVideoTrackError: ?string
-};
-
-/**
- * React {@code Component} for previewing audio and video input/output devices.
- *
- * @augments Component
- */
-class DeviceSelection extends AbstractDialogTab {
-
- /**
- * Whether current component is mounted or not.
- *
- * In component did mount we start a Promise to create tracks and
- * set the tracks in the state, if we unmount the component in the meanwhile
- * tracks will be created and will never been disposed (dispose tracks is
- * in componentWillUnmount). When tracks are created and component is
- * unmounted we dispose the tracks.
- */
- _unMounted: boolean;
-
- /**
- * Initializes a new DeviceSelection instance.
- *
- * @param {Object} props - The read-only React Component props with which
- * the new instance is to be initialized.
- */
- constructor(props: Props) {
- super(props);
-
- this.state = {
- previewAudioTrack: null,
- previewVideoTrack: null,
- previewVideoTrackError: null
- };
- this._unMounted = true;
- }
-
- /**
- * Generate the initial previews for audio input and video input.
- *
- * @inheritdoc
- */
- componentDidMount() {
- this._unMounted = false;
- Promise.all([
- this._createAudioInputTrack(this.props.selectedAudioInputId),
- this._createVideoInputTrack(this.props.selectedVideoInputId)
- ])
- .catch(err => logger.warn('Failed to initialize preview tracks', err))
- .then(() => getAvailableDevices());
- }
-
- /**
- * Checks if audio / video permissions were granted. Updates audio input and
- * video input previews.
- *
- * @param {Object} prevProps - Previous props this component received.
- * @returns {void}
- */
- componentDidUpdate(prevProps) {
- if (prevProps.selectedAudioInputId
- !== this.props.selectedAudioInputId) {
- this._createAudioInputTrack(this.props.selectedAudioInputId);
- }
-
- if (prevProps.selectedVideoInputId
- !== this.props.selectedVideoInputId) {
- this._createVideoInputTrack(this.props.selectedVideoInputId);
- }
- }
-
- /**
- * Ensure preview tracks are destroyed to prevent continued use.
- *
- * @inheritdoc
- */
- componentWillUnmount() {
- this._unMounted = true;
- this._disposeAudioInputPreview();
- this._disposeVideoInputPreview();
- }
-
- /**
- * Implements React's {@link Component#render()}.
- *
- * @inheritdoc
- */
- render() {
- const {
- hideAudioInputPreview,
- hideAudioOutputPreview,
- hideDeviceHIDContainer,
- hideVideoInputPreview,
- selectedAudioOutputId
- } = this.props;
-
- return (
-
-
- { !hideVideoInputPreview
- &&
-
-
- }
- { !hideAudioInputPreview
- &&
}
-
-
-
- { this._renderSelectors() }
-
- { !hideAudioOutputPreview
- &&
}
- { !hideDeviceHIDContainer
- &&
}
-
-
- );
- }
-
- /**
- * Creates the JitiTrack for the audio input preview.
- *
- * @param {string} deviceId - The id of audio input device to preview.
- * @private
- * @returns {void}
- */
- _createAudioInputTrack(deviceId) {
- const { hideAudioInputPreview } = this.props;
-
- if (hideAudioInputPreview) {
- return;
- }
-
- return this._disposeAudioInputPreview()
- .then(() => createLocalTrack('audio', deviceId, 5000))
- .then(jitsiLocalTrack => {
- if (this._unMounted) {
- jitsiLocalTrack.dispose();
-
- return;
- }
-
- this.setState({
- previewAudioTrack: jitsiLocalTrack
- });
- })
- .catch(() => {
- this.setState({
- previewAudioTrack: null
- });
- });
- }
-
- /**
- * Creates the JitiTrack for the video input preview.
- *
- * @param {string} deviceId - The id of video device to preview.
- * @private
- * @returns {void}
- */
- _createVideoInputTrack(deviceId) {
- const { hideVideoInputPreview } = this.props;
-
- if (hideVideoInputPreview) {
- return;
- }
-
- return this._disposeVideoInputPreview()
- .then(() => createLocalTrack('video', deviceId, 5000))
- .then(jitsiLocalTrack => {
- if (!jitsiLocalTrack) {
- return Promise.reject();
- }
-
- if (this._unMounted) {
- jitsiLocalTrack.dispose();
-
- return;
- }
-
- this.setState({
- previewVideoTrack: jitsiLocalTrack,
- previewVideoTrackError: null
- });
- })
- .catch(() => {
- this.setState({
- previewVideoTrack: null,
- previewVideoTrackError:
- this.props.t('deviceSelection.previewUnavailable')
- });
- });
- }
-
- /**
- * Utility function for disposing the current audio input preview.
- *
- * @private
- * @returns {Promise}
- */
- _disposeAudioInputPreview(): Promise<*> {
- return this.state.previewAudioTrack
- ? this.state.previewAudioTrack.dispose() : Promise.resolve();
- }
-
- /**
- * Utility function for disposing the current video input preview.
- *
- * @private
- * @returns {Promise}
- */
- _disposeVideoInputPreview(): Promise<*> {
- return this.state.previewVideoTrack
- ? this.state.previewVideoTrack.dispose() : Promise.resolve();
- }
-
- /**
- * Creates a DeviceSelector instance based on the passed in configuration.
- *
- * @private
- * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
- * @returns {ReactElement}
- */
- _renderSelector(deviceSelectorProps) {
- return (
-
-
-
-
- );
- }
-
- /**
- * Creates DeviceSelector instances for video output, audio input, and audio
- * output.
- *
- * @private
- * @returns {Array} DeviceSelector instances.
- */
- _renderSelectors() {
- const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
-
- const configurations = [
- {
- devices: availableDevices.audioInput,
- hasPermission: hasAudioPermission,
- icon: 'icon-microphone',
- isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
- key: 'audioInput',
- id: 'audioInput',
- label: 'settings.selectMic',
- onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
- selectedDeviceId: this.state.previewAudioTrack
- ? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
- },
- {
- devices: availableDevices.videoInput,
- hasPermission: hasVideoPermission,
- icon: 'icon-camera',
- isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
- key: 'videoInput',
- id: 'videoInput',
- label: 'settings.selectCamera',
- onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
- selectedDeviceId: this.state.previewVideoTrack
- ? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
- }
- ];
-
- if (!this.props.hideAudioOutputSelect) {
- configurations.push({
- devices: availableDevices.audioOutput,
- hasPermission: hasAudioPermission || hasVideoPermission,
- icon: 'icon-speaker',
- isDisabled: this.props.disableDeviceChange,
- key: 'audioOutput',
- id: 'audioOutput',
- label: 'settings.selectAudioOutput',
- onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
- selectedDeviceId: this.props.selectedAudioOutputId
- });
- }
-
- return configurations.map(config => this._renderSelector(config));
- }
-}
-
-export default translate(DeviceSelection);
diff --git a/react/features/device-selection/components/DeviceSelector.web.js b/react/features/device-selection/components/DeviceSelector.web.tsx
similarity index 71%
rename from react/features/device-selection/components/DeviceSelector.web.js
rename to react/features/device-selection/components/DeviceSelector.web.tsx
index b4e373687..b0f5fa9e4 100644
--- a/react/features/device-selection/components/DeviceSelector.web.js
+++ b/react/features/device-selection/components/DeviceSelector.web.tsx
@@ -1,58 +1,76 @@
-/* @flow */
+import { Theme } from '@mui/material';
+import { withStyles } from '@mui/styles';
import React, { Component } from 'react';
+import { WithTranslation } from 'react-i18next';
import { translate } from '../../base/i18n/functions';
+import { withPixelLineHeight } from '../../base/styles/functions.web';
import Select from '../../base/ui/components/web/Select';
/**
* The type of the React {@code Component} props of {@link DeviceSelector}.
*/
-type Props = {
+interface IProps extends WithTranslation {
+
+ /**
+ * CSS classes object.
+ */
+ classes: any;
/**
* MediaDeviceInfos used for display in the select element.
*/
- devices: Array