ソースを参照

feat(device-selection) Separate Devices into Audio and Video in Settings (#12987)

Create separate tabs for Audio and Video in the Settings Dialog
Move some settings from the More tab to Audio/ Video tab
Implement redesign
Convert some files to TS
Move some styles from SCSS to JSS
Enable device selection on welcome page
factor2
Robert Pintilii 2年前
コミット
0d0bec3aad
コミッターのメールアドレスに関連付けられたアカウントが存在しません
25個のファイルの変更1195行の追加1025行の削除
  1. 0
    1
      css/main.scss
  2. 0
    148
      css/modals/device-selection/_device-selection.scss
  3. 5
    2
      lang/main.json
  4. 8
    4
      react/features/base/ui/components/web/DialogWithTabs.tsx
  5. 1
    1
      react/features/base/ui/components/web/Select.tsx
  6. 48
    16
      react/features/device-selection/actions.web.ts
  7. 387
    0
      react/features/device-selection/components/AudioDevicesSelection.web.tsx
  8. 0
    150
      react/features/device-selection/components/AudioInputPreview.js
  9. 103
    0
      react/features/device-selection/components/AudioInputPreview.web.tsx
  10. 26
    31
      react/features/device-selection/components/AudioOutputPreview.web.tsx
  11. 16
    10
      react/features/device-selection/components/DeviceHidContainer.web.tsx
  12. 0
    429
      react/features/device-selection/components/DeviceSelection.js
  13. 52
    54
      react/features/device-selection/components/DeviceSelector.web.tsx
  14. 368
    0
      react/features/device-selection/components/VideoDeviceSelection.web.tsx
  15. 0
    58
      react/features/device-selection/components/VideoInputPreview.js
  16. 73
    0
      react/features/device-selection/components/VideoInputPreview.web.tsx
  17. 0
    4
      react/features/device-selection/components/index.js
  18. 60
    13
      react/features/device-selection/functions.web.ts
  19. 0
    1
      react/features/screen-share/actions.native.ts
  20. 0
    7
      react/features/settings/actions.ts
  21. 0
    58
      react/features/settings/components/web/MoreTab.tsx
  22. 1
    1
      react/features/settings/components/web/SettingsButton.js
  23. 44
    30
      react/features/settings/components/web/SettingsDialog.tsx
  24. 3
    2
      react/features/settings/constants.ts
  25. 0
    5
      react/features/settings/functions.any.ts

+ 0
- 1
css/main.scss ファイルの表示

@@ -33,7 +33,6 @@ $flagsImagePath: "../images/";
33 33
 @import 'reload_overlay/reload_overlay';
34 34
 @import 'mini_toolbox';
35 35
 @import 'modals/desktop-picker/desktop-picker';
36
-@import 'modals/device-selection/device-selection';
37 36
 @import 'modals/dialog';
38 37
 @import 'modals/embed-meeting/embed-meeting';
39 38
 @import 'modals/feedback/feedback';

+ 0
- 148
css/modals/device-selection/_device-selection.scss ファイルの表示

@@ -1,148 +0,0 @@
1
-.device-selection {
2
-    .device-selectors {
3
-        font-size: 14px;
4
-
5
-        > div {
6
-            display: block;
7
-            margin-bottom: 4px;
8
-        }
9
-
10
-        .device-selector-icon {
11
-            align-self: center;
12
-            color: inherit;
13
-            font-size: 20px;
14
-            margin-left: 3px;
15
-        }
16
-
17
-        .device-selector-label {
18
-            margin-bottom: 1px;
19
-        }
20
-
21
-        /* device-selector-trigger stylings attempt to mimic AtlasKit button */
22
-        .device-selector-trigger {
23
-            background-color: #0E1624;
24
-            border: 1px solid #455166;
25
-            border-radius: 5px;
26
-            display: flex;
27
-            height: 2.3em;
28
-            justify-content: space-between;
29
-            line-height: 2.3em;
30
-            overflow: hidden;
31
-            padding: 0 8px;
32
-        }
33
-        .device-selector-trigger-disabled {
34
-            .device-selector-trigger {
35
-                color: #a5adba;
36
-                cursor: default;
37
-            }
38
-        }
39
-
40
-        .device-selector-trigger-text {
41
-            overflow: hidden;
42
-            text-align: center;
43
-            text-overflow: ellipsis;
44
-            white-space: nowrap;
45
-            width: 100%;
46
-        }
47
-    }
48
-
49
-    .device-selection-column {
50
-        box-sizing: border-box;
51
-        display: inline-block;
52
-        vertical-align: top;
53
-
54
-        &.column-selectors {
55
-            margin-left: 15px;
56
-            width: 45%;
57
-        }
58
-
59
-        &.column-video {
60
-            width: 50%;
61
-        }
62
-    }
63
-
64
-    .device-selection-video-container {
65
-        border-radius: 3px;
66
-        margin-bottom: 5px;
67
-
68
-        .video-input-preview {
69
-            margin-top: 2px;
70
-            position: relative;
71
-
72
-            > video {
73
-                border-radius: 3px;
74
-            }
75
-
76
-            .video-input-preview-error {
77
-                color: $participantNameColor;
78
-                display: none;
79
-                left: 0;
80
-                position: absolute;
81
-                right: 0;
82
-                text-align: center;
83
-                top: 50%;
84
-            }
85
-
86
-            &.video-preview-has-error {
87
-                background: black;
88
-
89
-                .video-input-preview-error {
90
-                    display: block;
91
-                }
92
-            }
93
-
94
-            .video-input-preview-display {
95
-                height: auto;
96
-                overflow: hidden;
97
-                width: 100%;
98
-            }
99
-        }
100
-    }
101
-
102
-    .audio-output-preview {
103
-        font-size: 14px;
104
-
105
-        a {
106
-            color: #6FB1EA;
107
-            cursor: pointer;
108
-            text-decoration: none;
109
-        }
110
-
111
-        a:hover {
112
-            color: #B3D4FF;
113
-        }
114
-    }
115
-
116
-    .audio-input-preview {
117
-        background: #1B2638;
118
-        border-radius: 5px;
119
-        height: 8px;
120
-
121
-        .audio-input-preview-level {
122
-            background: #75B1FF;
123
-            border-radius: 5px;
124
-            height: 100%;
125
-            -webkit-transition: width .1s ease-in-out;
126
-            -moz-transition: width .1s ease-in-out;
127
-            -o-transition: width .1s ease-in-out;
128
-            transition: width .1s ease-in-out;
129
-        }
130
-    }
131
-}
132
-
133
-.device-selection.video-hidden {
134
-    display: flex;
135
-    flex-direction: column;
136
-    width: 100%;
137
-
138
-    .column-selectors {
139
-        width: 100%;
140
-        margin-left: 0;
141
-    }
142
-
143
-    .column-video {
144
-        order: 1;
145
-        width: 100%;
146
-        margin-top: 8px;
147
-    }
148
-}

+ 5
- 2
lang/main.json ファイルの表示

@@ -220,7 +220,7 @@
220 220
         "noPermission": "Permission not granted",
221 221
         "previewUnavailable": "Preview unavailable",
222 222
         "selectADevice": "Select a device",
223
-        "testAudio": "Play a test sound"
223
+        "testAudio": "Test"
224 224
     },
225 225
     "dialIn": {
226 226
         "screenTitle": "Dial-in summary"
@@ -971,6 +971,7 @@
971 971
         "title": "Security Options"
972 972
     },
973 973
     "settings": {
974
+        "audio": "Audio",
974 975
         "buttonLabel": "Settings",
975 976
         "calendar": {
976 977
             "about": "The {{appName}} calendar integration is used to securely access your calendar so it can read upcoming events.",
@@ -1012,7 +1013,8 @@
1012 1013
         "startReactionsMuted": "Mute reaction sounds for everyone",
1013 1014
         "startVideoMuted": "Everyone starts hidden",
1014 1015
         "talkWhileMuted": "Talk while muted",
1015
-        "title": "Settings"
1016
+        "title": "Settings",
1017
+        "video": "Video"
1016 1018
     },
1017 1019
     "settingsView": {
1018 1020
         "advanced": "Advanced",
@@ -1171,6 +1173,7 @@
1171 1173
         "download": "Download our apps",
1172 1174
         "e2ee": "End-to-End Encryption",
1173 1175
         "embedMeeting": "Embed meeting",
1176
+        "enableNoiseSuppression": "Enable noise suppression",
1174 1177
         "endConference": "End meeting for all",
1175 1178
         "enterFullScreen": "View full screen",
1176 1179
         "enterTileView": "Enter tile view",

+ 8
- 4
react/features/base/ui/components/web/DialogWithTabs.tsx ファイルの表示

@@ -89,6 +89,10 @@ const useStyles = makeStyles()(theme => {
89 89
             }
90 90
         },
91 91
 
92
+        closeButtonContainer: {
93
+            paddingBottom: theme.spacing(4)
94
+        },
95
+
92 96
         buttonContainer: {
93 97
             width: '100%',
94 98
             boxSizing: 'border-box',
@@ -138,20 +142,20 @@ interface IObject {
138 142
     [key: string]: string | string[] | boolean | number | number[] | {} | undefined;
139 143
 }
140 144
 
141
-export interface IDialogTab {
145
+export interface IDialogTab<P> {
142 146
     className?: string;
143 147
     component: ComponentType<any>;
144 148
     icon: Function;
145 149
     labelKey: string;
146 150
     name: string;
147 151
     props?: IObject;
148
-    propsUpdateFunction?: (tabState: IObject, newProps: IObject) => IObject;
152
+    propsUpdateFunction?: (tabState: IObject, newProps: P) => P;
149 153
     submit?: Function;
150 154
 }
151 155
 
152 156
 interface IProps extends IBaseProps {
153 157
     defaultTab?: string;
154
-    tabs: IDialogTab[];
158
+    tabs: IDialogTab<any>[];
155 159
 }
156 160
 
157 161
 const DialogWithTabs = ({
@@ -287,7 +291,7 @@ const DialogWithTabs = ({
287 291
             )}
288 292
             {(!isMobile || selectedTab) && (
289 293
                 <div className = { classes.contentContainer }>
290
-                    <div className = { classes.buttonContainer }>
294
+                    <div className = { cx(classes.buttonContainer, classes.closeButtonContainer) }>
291 295
                         {isMobile && (
292 296
                             <span className = { classes.backContainer }>
293 297
                                 <ClickableIcon

+ 1
- 1
react/features/base/ui/components/web/Select.tsx ファイルの表示

@@ -79,7 +79,7 @@ const useStyles = makeStyles()(theme => {
79 79
             width: '100%',
80 80
             ...withPixelLineHeight(theme.typography.bodyShortRegular),
81 81
             color: theme.palette.text01,
82
-            padding: '8px 16px',
82
+            padding: '10px 16px',
83 83
             paddingRight: '42px',
84 84
             border: 0,
85 85
             appearance: 'none',

+ 48
- 16
react/features/device-selection/actions.web.ts ファイルの表示

@@ -7,31 +7,23 @@ import {
7 7
 } from '../base/devices/actions';
8 8
 import { getDeviceLabelById, setAudioOutputDeviceId } from '../base/devices/functions';
9 9
 import { updateSettings } from '../base/settings/actions';
10
+import { toggleNoiseSuppression } from '../noise-suppression/actions';
11
+import { setScreenshareFramerate } from '../screen-share/actions';
10 12
 
11
-import { getDeviceSelectionDialogProps } from './functions';
13
+import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from './functions';
12 14
 import logger from './logger';
13 15
 
14 16
 /**
15
- * Submits the settings related to device selection.
17
+ * Submits the settings related to audio device selection.
16 18
  *
17 19
  * @param {Object} newState - The new settings.
18 20
  * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
19 21
  * welcome page or not.
20 22
  * @returns {Function}
21 23
  */
22
-export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
24
+export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
23 25
     return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
24
-        const currentState = getDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
25
-
26
-        if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
27
-            dispatch(updateSettings({
28
-                userSelectedCameraDeviceId: newState.selectedVideoInputId,
29
-                userSelectedCameraDeviceLabel:
30
-                    getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
31
-            }));
32
-
33
-            dispatch(setVideoInputDevice(newState.selectedVideoInputId));
34
-        }
26
+        const currentState = getAudioDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
35 27
 
36 28
         if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
37 29
             dispatch(updateSettings({
@@ -44,8 +36,8 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
44 36
         }
45 37
 
46 38
         if (newState.selectedAudioOutputId
47
-                && newState.selectedAudioOutputId
48
-                    !== currentState.selectedAudioOutputId) {
39
+            && newState.selectedAudioOutputId
40
+            !== currentState.selectedAudioOutputId) {
49 41
             sendAnalytics(createDeviceChangedEvent('audio', 'output'));
50 42
 
51 43
             setAudioOutputDeviceId(
@@ -62,5 +54,45 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
62 54
                         err);
63 55
                 });
64 56
         }
57
+
58
+        if (newState.noiseSuppressionEnabled !== currentState.noiseSuppressionEnabled) {
59
+            dispatch(toggleNoiseSuppression());
60
+        }
61
+    };
62
+}
63
+
64
+/**
65
+ * Submits the settings related to device selection.
66
+ *
67
+ * @param {Object} newState - The new settings.
68
+ * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
69
+ * welcome page or not.
70
+ * @returns {Function}
71
+ */
72
+export function submitVideoDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
73
+    return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
74
+        const currentState = getVideoDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
75
+
76
+        if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
77
+            dispatch(updateSettings({
78
+                userSelectedCameraDeviceId: newState.selectedVideoInputId,
79
+                userSelectedCameraDeviceLabel:
80
+                    getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
81
+            }));
82
+
83
+            dispatch(setVideoInputDevice(newState.selectedVideoInputId));
84
+        }
85
+
86
+        if (newState.localFlipX !== currentState.localFlipX) {
87
+            dispatch(updateSettings({
88
+                localFlipX: newState.localFlipX
89
+            }));
90
+        }
91
+
92
+        if (newState.currentFramerate !== currentState.currentFramerate) {
93
+            const frameRate = parseInt(newState.currentFramerate, 10);
94
+
95
+            dispatch(setScreenshareFramerate(frameRate));
96
+        }
65 97
     };
66 98
 }

+ 387
- 0
react/features/device-selection/components/AudioDevicesSelection.web.tsx ファイルの表示

@@ -0,0 +1,387 @@
1
+import { Theme } from '@mui/material';
2
+import { withStyles } from '@mui/styles';
3
+import React from 'react';
4
+import { WithTranslation } from 'react-i18next';
5
+import { connect } from 'react-redux';
6
+
7
+import { IReduxState, IStore } from '../../app/types';
8
+import { getAvailableDevices } from '../../base/devices/actions.web';
9
+import AbstractDialogTab, {
10
+    type IProps as AbstractDialogTabProps
11
+} from '../../base/dialog/components/web/AbstractDialogTab';
12
+import { translate } from '../../base/i18n/functions';
13
+import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
14
+import Checkbox from '../../base/ui/components/web/Checkbox';
15
+import logger from '../logger';
16
+
17
+import AudioInputPreview from './AudioInputPreview';
18
+import AudioOutputPreview from './AudioOutputPreview';
19
+import DeviceHidContainer from './DeviceHidContainer.web';
20
+import DeviceSelector from './DeviceSelector.web';
21
+
22
+/**
23
+ * The type of the React {@code Component} props of {@link AudioDevicesSelection}.
24
+ */
25
+interface IProps extends AbstractDialogTabProps, WithTranslation {
26
+
27
+    /**
28
+     * All known audio and video devices split by type. This prop comes from
29
+     * the app state.
30
+     */
31
+    availableDevices: {
32
+        audioInput?: MediaDeviceInfo[];
33
+        audioOutput?: MediaDeviceInfo[];
34
+    };
35
+
36
+    /**
37
+     * CSS classes object.
38
+     */
39
+    classes: any;
40
+
41
+    /**
42
+     * Whether or not the audio selector can be interacted with. If true,
43
+     * the audio input selector will be rendered as disabled. This is
44
+     * specifically used to prevent audio device changing in Firefox, which
45
+     * currently does not work due to a browser-side regression.
46
+     */
47
+    disableAudioInputChange: boolean;
48
+
49
+    /**
50
+     * True if device changing is configured to be disallowed. Selectors
51
+     * will display as disabled.
52
+     */
53
+    disableDeviceChange: boolean;
54
+
55
+    /**
56
+     * Redux dispatch function.
57
+     */
58
+    dispatch: IStore['dispatch'];
59
+
60
+    /**
61
+     * Whether or not the audio permission was granted.
62
+     */
63
+    hasAudioPermission: boolean;
64
+
65
+    /**
66
+     * If true, the audio meter will not display. Necessary for browsers or
67
+     * configurations that do not support local stats to prevent a
68
+     * non-responsive mic preview from displaying.
69
+     */
70
+    hideAudioInputPreview: boolean;
71
+
72
+    /**
73
+     * If true, the button to play a test sound on the selected speaker will not be displayed.
74
+     * This needs to be hidden on browsers that do not support selecting an audio output device.
75
+     */
76
+    hideAudioOutputPreview: boolean;
77
+
78
+    /**
79
+     * Whether or not the audio output source selector should display. If
80
+     * true, the audio output selector and test audio link will not be
81
+     * rendered.
82
+     */
83
+    hideAudioOutputSelect: boolean;
84
+
85
+    /**
86
+     * Whether or not the hid device container should display.
87
+     */
88
+    hideDeviceHIDContainer: boolean;
89
+
90
+    /**
91
+     * Whether to hide noise suppression checkbox or not.
92
+     */
93
+    hideNoiseSuppression: boolean;
94
+
95
+    /**
96
+     * Wether noise suppression is on or not.
97
+     */
98
+    noiseSuppressionEnabled: boolean;
99
+
100
+    /**
101
+     * The id of the audio input device to preview.
102
+     */
103
+    selectedAudioInputId: string;
104
+
105
+    /**
106
+     * The id of the audio output device to preview.
107
+     */
108
+    selectedAudioOutputId: string;
109
+}
110
+
111
+/**
112
+ * The type of the React {@code Component} state of {@link AudioDevicesSelection}.
113
+ */
114
+type State = {
115
+
116
+    /**
117
+     * The JitsiTrack to use for previewing audio input.
118
+     */
119
+    previewAudioTrack?: any | null;
120
+};
121
+
122
+const styles = (theme: Theme) => {
123
+    return {
124
+        container: {
125
+            display: 'flex',
126
+            flexDirection: 'column' as const,
127
+            padding: '0 2px',
128
+            width: '100%'
129
+        },
130
+
131
+        inputContainer: {
132
+            marginBottom: theme.spacing(3)
133
+        },
134
+
135
+        outputContainer: {
136
+            margin: `${theme.spacing(5)} 0`,
137
+            display: 'flex',
138
+            alignItems: 'flex-end'
139
+        },
140
+
141
+        outputButton: {
142
+            marginLeft: theme.spacing(3)
143
+        },
144
+
145
+        noiseSuppressionContainer: {
146
+            marginBottom: theme.spacing(5)
147
+        }
148
+    };
149
+};
150
+
151
+/**
152
+ * React {@code Component} for previewing audio and video input/output devices.
153
+ *
154
+ * @augments Component
155
+ */
156
+class AudioDevicesSelection extends AbstractDialogTab<IProps, State> {
157
+
158
+    /**
159
+     * Whether current component is mounted or not.
160
+     *
161
+     * In component did mount we start a Promise to create tracks and
162
+     * set the tracks in the state, if we unmount the component in the meanwhile
163
+     * tracks will be created and will never been disposed (dispose tracks is
164
+     * in componentWillUnmount). When tracks are created and component is
165
+     * unmounted we dispose the tracks.
166
+     */
167
+    _unMounted: boolean;
168
+
169
+    /**
170
+     * Initializes a new DeviceSelection instance.
171
+     *
172
+     * @param {Object} props - The read-only React Component props with which
173
+     * the new instance is to be initialized.
174
+     */
175
+    constructor(props: IProps) {
176
+        super(props);
177
+
178
+        this.state = {
179
+            previewAudioTrack: null
180
+        };
181
+        this._unMounted = true;
182
+    }
183
+
184
+    /**
185
+     * Generate the initial previews for audio input and video input.
186
+     *
187
+     * @inheritdoc
188
+     */
189
+    componentDidMount() {
190
+        this._unMounted = false;
191
+        Promise.all([
192
+            this._createAudioInputTrack(this.props.selectedAudioInputId)
193
+        ])
194
+            .catch(err => logger.warn('Failed to initialize preview tracks', err))
195
+            .then(() => {
196
+                this.props.dispatch(getAvailableDevices());
197
+            });
198
+    }
199
+
200
+    /**
201
+     * Checks if audio / video permissions were granted. Updates audio input and
202
+     * video input previews.
203
+     *
204
+     * @param {Object} prevProps - Previous props this component received.
205
+     * @returns {void}
206
+     */
207
+    componentDidUpdate(prevProps: IProps) {
208
+        if (prevProps.selectedAudioInputId
209
+            !== this.props.selectedAudioInputId) {
210
+            this._createAudioInputTrack(this.props.selectedAudioInputId);
211
+        }
212
+    }
213
+
214
+    /**
215
+     * Ensure preview tracks are destroyed to prevent continued use.
216
+     *
217
+     * @inheritdoc
218
+     */
219
+    componentWillUnmount() {
220
+        this._unMounted = true;
221
+        this._disposeAudioInputPreview();
222
+    }
223
+
224
+    /**
225
+     * Implements React's {@link Component#render()}.
226
+     *
227
+     * @inheritdoc
228
+     */
229
+    render() {
230
+        const {
231
+            classes,
232
+            hasAudioPermission,
233
+            hideAudioInputPreview,
234
+            hideAudioOutputPreview,
235
+            hideDeviceHIDContainer,
236
+            hideNoiseSuppression,
237
+            noiseSuppressionEnabled,
238
+            selectedAudioOutputId,
239
+            t
240
+        } = this.props;
241
+        const { audioInput, audioOutput } = this._getSelectors();
242
+
243
+        return (
244
+            <div className = { classes.container }>
245
+                <div
246
+                    aria-live = 'polite'
247
+                    className = { classes.inputContainer }>
248
+                    {this._renderSelector(audioInput)}
249
+                </div>
250
+                {!hideAudioInputPreview && hasAudioPermission
251
+                        && <AudioInputPreview
252
+                            track = { this.state.previewAudioTrack } />}
253
+                <div
254
+                    aria-live = 'polite'
255
+                    className = { classes.outputContainer }>
256
+                    {this._renderSelector(audioOutput)}
257
+                    {!hideAudioOutputPreview && hasAudioPermission
258
+                        && <AudioOutputPreview
259
+                            className = { classes.outputButton }
260
+                            deviceId = { selectedAudioOutputId } />}
261
+                </div>
262
+                {!hideNoiseSuppression && (
263
+                    <div className = { classes.noiseSuppressionContainer }>
264
+                        <Checkbox
265
+                            checked = { noiseSuppressionEnabled }
266
+                            label = { t('toolbar.enableNoiseSuppression') }
267
+                            // eslint-disable-next-line react/jsx-no-bind
268
+                            onChange = { () => super._onChange({
269
+                                noiseSuppressionEnabled: !noiseSuppressionEnabled
270
+                            }) } />
271
+                    </div>
272
+                )}
273
+                {!hideDeviceHIDContainer
274
+                    && <DeviceHidContainer />}
275
+            </div>
276
+        );
277
+    }
278
+
279
+    /**
280
+     * Creates the JitsiTrack for the audio input preview.
281
+     *
282
+     * @param {string} deviceId - The id of audio input device to preview.
283
+     * @private
284
+     * @returns {void}
285
+     */
286
+    _createAudioInputTrack(deviceId: string) {
287
+        const { hideAudioInputPreview } = this.props;
288
+
289
+        if (hideAudioInputPreview) {
290
+            return;
291
+        }
292
+
293
+        return this._disposeAudioInputPreview()
294
+            .then(() => createLocalTrack('audio', deviceId, 5000))
295
+            .then(jitsiLocalTrack => {
296
+                if (this._unMounted) {
297
+                    jitsiLocalTrack.dispose();
298
+
299
+                    return;
300
+                }
301
+
302
+                this.setState({
303
+                    previewAudioTrack: jitsiLocalTrack
304
+                });
305
+            })
306
+            .catch(() => {
307
+                this.setState({
308
+                    previewAudioTrack: null
309
+                });
310
+            });
311
+    }
312
+
313
+    /**
314
+     * Utility function for disposing the current audio input preview.
315
+     *
316
+     * @private
317
+     * @returns {Promise}
318
+     */
319
+    _disposeAudioInputPreview(): Promise<any> {
320
+        return this.state.previewAudioTrack
321
+            ? this.state.previewAudioTrack.dispose() : Promise.resolve();
322
+    }
323
+
324
+    /**
325
+     * Creates a DeviceSelector instance based on the passed in configuration.
326
+     *
327
+     * @private
328
+     * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
329
+     * @returns {ReactElement}
330
+     */
331
+    _renderSelector(deviceSelectorProps: any) {
332
+        return deviceSelectorProps ? (
333
+            <DeviceSelector
334
+                { ...deviceSelectorProps }
335
+                key = { deviceSelectorProps.id } />
336
+        ) : null;
337
+    }
338
+
339
+    /**
340
+     * Returns object configurations for audio input and output.
341
+     *
342
+     * @private
343
+     * @returns {Object} Configurations.
344
+     */
345
+    _getSelectors() {
346
+        const { availableDevices, hasAudioPermission } = this.props;
347
+
348
+        const audioInput = {
349
+            devices: availableDevices.audioInput,
350
+            hasPermission: hasAudioPermission,
351
+            icon: 'icon-microphone',
352
+            isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
353
+            key: 'audioInput',
354
+            id: 'audioInput',
355
+            label: 'settings.selectMic',
356
+            onSelect: (selectedAudioInputId: string) => super._onChange({ selectedAudioInputId }),
357
+            selectedDeviceId: this.state.previewAudioTrack
358
+                ? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
359
+        };
360
+        let audioOutput;
361
+
362
+        if (!this.props.hideAudioOutputSelect) {
363
+            audioOutput = {
364
+                devices: availableDevices.audioOutput,
365
+                hasPermission: hasAudioPermission,
366
+                icon: 'icon-speaker',
367
+                isDisabled: this.props.disableDeviceChange,
368
+                key: 'audioOutput',
369
+                id: 'audioOutput',
370
+                label: 'settings.selectAudioOutput',
371
+                onSelect: (selectedAudioOutputId: string) => super._onChange({ selectedAudioOutputId }),
372
+                selectedDeviceId: this.props.selectedAudioOutputId
373
+            };
374
+        }
375
+
376
+        return { audioInput,
377
+            audioOutput };
378
+    }
379
+}
380
+
381
+const mapStateToProps = (state: IReduxState) => {
382
+    return {
383
+        availableDevices: state['features/base/devices'].availableDevices ?? {}
384
+    };
385
+};
386
+
387
+export default connect(mapStateToProps)(withStyles(styles)(translate(AudioDevicesSelection)));

+ 0
- 150
react/features/device-selection/components/AudioInputPreview.js ファイルの表示

@@ -1,150 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
6
-
7
-const JitsiTrackEvents = JitsiMeetJS.events.track;
8
-
9
-/**
10
- * The type of the React {@code Component} props of {@link AudioInputPreview}.
11
- */
12
-type Props = {
13
-
14
-    /**
15
-     * The JitsiLocalTrack to show an audio level meter for.
16
-     */
17
-    track: Object
18
-};
19
-
20
-/**
21
- * The type of the React {@code Component} props of {@link AudioInputPreview}.
22
- */
23
-type State = {
24
-
25
-    /**
26
-     * The current audio input level being received, from 0 to 1.
27
-     */
28
-    audioLevel: number
29
-};
30
-
31
-/**
32
- * React component for displaying a audio level meter for a JitsiLocalTrack.
33
- */
34
-class AudioInputPreview extends Component<Props, State> {
35
-    /**
36
-     * Initializes a new AudioInputPreview instance.
37
-     *
38
-     * @param {Object} props - The read-only React Component props with which
39
-     * the new instance is to be initialized.
40
-     */
41
-    constructor(props: Props) {
42
-        super(props);
43
-
44
-        this.state = {
45
-            audioLevel: 0
46
-        };
47
-
48
-        this._updateAudioLevel = this._updateAudioLevel.bind(this);
49
-    }
50
-
51
-    /**
52
-     * Starts listening for audio level updates after the initial render.
53
-     *
54
-     * @inheritdoc
55
-     * @returns {void}
56
-     */
57
-    componentDidMount() {
58
-        this._listenForAudioUpdates(this.props.track);
59
-    }
60
-
61
-    /**
62
-     * Stops listening for audio level updates on the old track and starts
63
-     * listening instead on the new track.
64
-     *
65
-     * @inheritdoc
66
-     * @returns {void}
67
-     */
68
-    componentDidUpdate(prevProps: Props) {
69
-        if (prevProps.track !== this.props.track) {
70
-            this._listenForAudioUpdates(this.props.track);
71
-            this._updateAudioLevel(0);
72
-        }
73
-    }
74
-
75
-    /**
76
-     * Unsubscribe from audio level updates.
77
-     *
78
-     * @inheritdoc
79
-     * @returns {void}
80
-     */
81
-    componentWillUnmount() {
82
-        this._stopListeningForAudioUpdates();
83
-    }
84
-
85
-    /**
86
-     * Implements React's {@link Component#render()}.
87
-     *
88
-     * @inheritdoc
89
-     * @returns {ReactElement}
90
-     */
91
-    render() {
92
-        const audioMeterFill = {
93
-            width: `${Math.floor(this.state.audioLevel * 100)}%`
94
-        };
95
-
96
-        return (
97
-            <div className = 'audio-input-preview' >
98
-                <div
99
-                    className = 'audio-input-preview-level'
100
-                    style = { audioMeterFill } />
101
-            </div>
102
-        );
103
-    }
104
-
105
-    /**
106
-     * Starts listening for audio level updates from the library.
107
-     *
108
-     * @param {JitstiLocalTrack} track - The track to listen to for audio level
109
-     * updates.
110
-     * @private
111
-     * @returns {void}
112
-     */
113
-    _listenForAudioUpdates(track) {
114
-        this._stopListeningForAudioUpdates();
115
-
116
-        track && track.on(
117
-            JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
118
-            this._updateAudioLevel);
119
-    }
120
-
121
-    /**
122
-     * Stops listening to further updates from the current track.
123
-     *
124
-     * @private
125
-     * @returns {void}
126
-     */
127
-    _stopListeningForAudioUpdates() {
128
-        this.props.track && this.props.track.off(
129
-            JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
130
-            this._updateAudioLevel);
131
-    }
132
-
133
-    _updateAudioLevel: (number) => void;
134
-
135
-    /**
136
-     * Updates the internal state of the last know audio level. The level should
137
-     * be between 0 and 1, as the level will be used as a percentage out of 1.
138
-     *
139
-     * @param {number} audioLevel - The new audio level for the track.
140
-     * @private
141
-     * @returns {void}
142
-     */
143
-    _updateAudioLevel(audioLevel) {
144
-        this.setState({
145
-            audioLevel
146
-        });
147
-    }
148
-}
149
-
150
-export default AudioInputPreview;

+ 103
- 0
react/features/device-selection/components/AudioInputPreview.web.tsx ファイルの表示

@@ -0,0 +1,103 @@
1
+import React, { useEffect, useState } from 'react';
2
+import { makeStyles } from 'tss-react/mui';
3
+
4
+// @ts-ignore
5
+import JitsiMeetJS from '../../base/lib-jitsi-meet/_.web';
6
+
7
+const JitsiTrackEvents = JitsiMeetJS.events.track;
8
+
9
+/**
10
+ * The type of the React {@code Component} props of {@link AudioInputPreview}.
11
+ */
12
+interface IProps {
13
+
14
+    /**
15
+     * The JitsiLocalTrack to show an audio level meter for.
16
+     */
17
+    track: any;
18
+}
19
+
20
+const useStyles = makeStyles()(theme => {
21
+    return {
22
+        container: {
23
+            display: 'flex'
24
+        },
25
+
26
+        section: {
27
+            flex: 1,
28
+            height: '4px',
29
+            borderRadius: '1px',
30
+            backgroundColor: theme.palette.ui04,
31
+            marginRight: theme.spacing(1),
32
+
33
+            '&:last-of-type': {
34
+                marginRight: 0
35
+            }
36
+        },
37
+
38
+        activeSection: {
39
+            backgroundColor: theme.palette.success01
40
+        }
41
+    };
42
+});
43
+
44
+const NO_OF_PREVIEW_SECTIONS = 11;
45
+
46
+const AudioInputPreview = (props: IProps) => {
47
+    const [ audioLevel, setAudioLevel ] = useState(0);
48
+    const { classes, cx } = useStyles();
49
+
50
+    /**
51
+     * Starts listening for audio level updates from the library.
52
+     *
53
+     * @param {JitsiLocalTrack} track - The track to listen to for audio level
54
+     * updates.
55
+     * @private
56
+     * @returns {void}
57
+     */
58
+    function _listenForAudioUpdates(track: any) {
59
+        _stopListeningForAudioUpdates();
60
+
61
+        track?.on(
62
+            JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
63
+            setAudioLevel);
64
+    }
65
+
66
+    /**
67
+     * Stops listening to further updates from the current track.
68
+     *
69
+     * @private
70
+     * @returns {void}
71
+     */
72
+    function _stopListeningForAudioUpdates() {
73
+        props.track?.off(
74
+            JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
75
+            setAudioLevel);
76
+    }
77
+
78
+    useEffect(() => {
79
+        _listenForAudioUpdates(props.track);
80
+
81
+        return _stopListeningForAudioUpdates;
82
+    }, []);
83
+
84
+    useEffect(() => {
85
+        _listenForAudioUpdates(props.track);
86
+        setAudioLevel(0);
87
+    }, [ props.track ]);
88
+
89
+    const audioMeterFill = Math.ceil(Math.floor(audioLevel * 100) / (100 / NO_OF_PREVIEW_SECTIONS));
90
+
91
+    return (
92
+        <div className = { classes.container } >
93
+            {new Array(NO_OF_PREVIEW_SECTIONS).fill(0)
94
+                    .map((_, idx) =>
95
+                        (<div
96
+                            className = { cx(classes.section, idx < audioMeterFill && classes.activeSection) }
97
+                            key = { idx } />)
98
+                    )}
99
+        </div>
100
+    );
101
+};
102
+
103
+export default AudioInputPreview;

react/features/device-selection/components/AudioOutputPreview.js → react/features/device-selection/components/AudioOutputPreview.web.tsx ファイルの表示

@@ -1,35 +1,38 @@
1
-/* @flow */
2
-
3 1
 import React, { Component } from 'react';
2
+import { WithTranslation } from 'react-i18next';
4 3
 
5 4
 import { translate } from '../../base/i18n/functions';
6
-import Audio from '../../base/media/components/Audio';
5
+// eslint-disable-next-line lines-around-comment
6
+// @ts-ignore
7
+import Audio from '../../base/media/components/Audio.web';
8
+import Button from '../../base/ui/components/web/Button';
9
+import { BUTTON_TYPES } from '../../base/ui/constants.any';
7 10
 
8 11
 const TEST_SOUND_PATH = 'sounds/ring.mp3';
9 12
 
10 13
 /**
11 14
  * The type of the React {@code Component} props of {@link AudioOutputPreview}.
12 15
  */
13
-type Props = {
16
+interface IProps extends WithTranslation {
14 17
 
15 18
     /**
16
-     * The device id of the audio output device to use.
19
+     * Button className.
17 20
      */
18
-    deviceId: string,
21
+    className?: string;
19 22
 
20 23
     /**
21
-     * Invoked to obtain translated strings.
24
+     * The device id of the audio output device to use.
22 25
      */
23
-    t: Function
24
-};
26
+    deviceId: string;
27
+}
25 28
 
26 29
 /**
27 30
  * React component for playing a test sound through a specified audio device.
28 31
  *
29 32
  * @augments Component
30 33
  */
31
-class AudioOutputPreview extends Component<Props> {
32
-    _audioElement: ?Object;
34
+class AudioOutputPreview extends Component<IProps> {
35
+    _audioElement: HTMLAudioElement | null;
33 36
 
34 37
     /**
35 38
      * Initializes a new AudioOutputPreview instance.
@@ -37,7 +40,7 @@ class AudioOutputPreview extends Component<Props> {
37 40
      * @param {Object} props - The read-only React Component props with which
38 41
      * the new instance is to be initialized.
39 42
      */
40
-    constructor(props: Props) {
43
+    constructor(props: IProps) {
41 44
         super(props);
42 45
 
43 46
         this._audioElement = null;
@@ -66,24 +69,21 @@ class AudioOutputPreview extends Component<Props> {
66 69
      */
67 70
     render() {
68 71
         return (
69
-            <div className = 'audio-output-preview'>
70
-                <a
71
-                    aria-label = { this.props.t('deviceSelection.testAudio') }
72
+            <>
73
+                <Button
74
+                    accessibilityLabel = { this.props.t('deviceSelection.testAudio') }
75
+                    className = { this.props.className }
76
+                    labelKey = 'deviceSelection.testAudio'
72 77
                     onClick = { this._onClick }
73 78
                     onKeyPress = { this._onKeyPress }
74
-                    role = 'button'
75
-                    tabIndex = { 0 }>
76
-                    { this.props.t('deviceSelection.testAudio') }
77
-                </a>
79
+                    type = { BUTTON_TYPES.SECONDARY } />
78 80
                 <Audio
79 81
                     setRef = { this._audioElementReady }
80 82
                     src = { TEST_SOUND_PATH } />
81
-            </div>
83
+            </>
82 84
         );
83 85
     }
84 86
 
85
-    _audioElementReady: (Object) => void;
86
-
87 87
     /**
88 88
      * Sets the instance variable for the component's audio element so it can be
89 89
      * accessed directly.
@@ -92,14 +92,12 @@ class AudioOutputPreview extends Component<Props> {
92 92
      * @private
93 93
      * @returns {void}
94 94
      */
95
-    _audioElementReady(element: Object) {
95
+    _audioElementReady(element: HTMLAudioElement) {
96 96
         this._audioElement = element;
97 97
 
98 98
         this._setAudioSink();
99 99
     }
100 100
 
101
-    _onClick: () => void;
102
-
103 101
     /**
104 102
      * Plays a test sound.
105 103
      *
@@ -107,12 +105,9 @@ class AudioOutputPreview extends Component<Props> {
107 105
      * @returns {void}
108 106
      */
109 107
     _onClick() {
110
-        this._audioElement
111
-            && this._audioElement.play();
108
+        this._audioElement?.play();
112 109
     }
113 110
 
114
-    _onKeyPress: (Object) => void;
115
-
116 111
     /**
117 112
      * KeyPress handler for accessibility.
118 113
      *
@@ -120,7 +115,7 @@ class AudioOutputPreview extends Component<Props> {
120 115
      *
121 116
      * @returns {void}
122 117
      */
123
-    _onKeyPress(e) {
118
+    _onKeyPress(e: React.KeyboardEvent) {
124 119
         if (e.key === ' ' || e.key === 'Enter') {
125 120
             e.preventDefault();
126 121
             this._onClick();
@@ -135,7 +130,7 @@ class AudioOutputPreview extends Component<Props> {
135 130
      */
136 131
     _setAudioSink() {
137 132
         this._audioElement
138
-            && this.props.deviceId
133
+            && this.props.deviceId // @ts-ignore
139 134
             && this._audioElement.setSinkId(this.props.deviceId);
140 135
     }
141 136
 }

+ 16
- 10
react/features/device-selection/components/DeviceHidContainer.web.tsx ファイルの表示

@@ -5,33 +5,40 @@ import { makeStyles } from 'tss-react/mui';
5 5
 
6 6
 import Icon from '../../base/icons/components/Icon';
7 7
 import { IconTrash } from '../../base/icons/svg';
8
+import { withPixelLineHeight } from '../../base/styles/functions.web';
8 9
 import Button from '../../base/ui/components/web/Button';
9 10
 import { BUTTON_TYPES } from '../../base/ui/constants.any';
10 11
 import { closeHidDevice, requestHidDevice } from '../../web-hid/actions';
11 12
 import { getDeviceInfo, shouldRequestHIDDevice } from '../../web-hid/functions';
12 13
 
13
-const useStyles = makeStyles()(() => {
14
+const useStyles = makeStyles()(theme => {
14 15
     return {
15 16
         callControlContainer: {
16
-            marginTop: '8px',
17
-            marginBottom: '16px',
18
-            fontSize: '14px',
19
-            '> label': {
20
-                display: 'block',
21
-                marginBottom: '20px'
22
-            }
17
+            display: 'flex',
18
+            flexDirection: 'column',
19
+            alignItems: 'flex-start'
20
+        },
21
+
22
+        label: {
23
+            ...withPixelLineHeight(theme.typography.bodyShortRegular),
24
+            color: theme.palette.text01,
25
+            marginBottom: theme.spacing(2)
23 26
         },
27
+
24 28
         deviceRow: {
25 29
             display: 'flex',
26 30
             justifyContent: 'space-between'
27 31
         },
32
+
28 33
         deleteDevice: {
29 34
             cursor: 'pointer',
30 35
             textAlign: 'center'
31 36
         },
37
+
32 38
         headerConnectedDevice: {
33 39
             fontWeight: 600
34 40
         },
41
+
35 42
         hidContainer: {
36 43
             '> span': {
37 44
                 marginLeft: '16px'
@@ -66,7 +73,7 @@ function DeviceHidContainer() {
66 73
             className = { classes.callControlContainer }
67 74
             key = 'callControl'>
68 75
             <label
69
-                className = 'device-selector-label'
76
+                className = { classes.label }
70 77
                 htmlFor = 'callControl'>
71 78
                 {t('deviceSelection.hid.callControl')}
72 79
             </label>
@@ -77,7 +84,6 @@ function DeviceHidContainer() {
77 84
                     key = 'request-control-btn'
78 85
                     label = { t('deviceSelection.hid.pairDevice') }
79 86
                     onClick = { onRequestControl }
80
-                    size = 'small'
81 87
                     type = { BUTTON_TYPES.SECONDARY } />
82 88
             )}
83 89
             {!showRequestDeviceInfo && (

+ 0
- 429
react/features/device-selection/components/DeviceSelection.js ファイルの表示

@@ -1,429 +0,0 @@
1
-// @flow
2
-
3
-import React from 'react';
4
-
5
-import { getAvailableDevices } from '../../base/devices/actions.web';
6
-import AbstractDialogTab, {
7
-    type Props as AbstractDialogTabProps
8
-} from '../../base/dialog/components/web/AbstractDialogTab';
9
-import { translate } from '../../base/i18n/functions';
10
-import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
11
-import logger from '../logger';
12
-
13
-import AudioInputPreview from './AudioInputPreview';
14
-import AudioOutputPreview from './AudioOutputPreview';
15
-import DeviceHidContainer from './DeviceHidContainer.web';
16
-import DeviceSelector from './DeviceSelector';
17
-import VideoInputPreview from './VideoInputPreview';
18
-
19
-/**
20
- * The type of the React {@code Component} props of {@link DeviceSelection}.
21
- */
22
-export type Props = {
23
-    ...$Exact<AbstractDialogTabProps>,
24
-
25
-    /**
26
-     * All known audio and video devices split by type. This prop comes from
27
-     * the app state.
28
-     */
29
-    availableDevices: Object,
30
-
31
-    /**
32
-     * Whether or not the audio selector can be interacted with. If true,
33
-     * the audio input selector will be rendered as disabled. This is
34
-     * specifically used to prevent audio device changing in Firefox, which
35
-     * currently does not work due to a browser-side regression.
36
-     */
37
-    disableAudioInputChange: boolean,
38
-
39
-    /**
40
-     * True if device changing is configured to be disallowed. Selectors
41
-     * will display as disabled.
42
-     */
43
-    disableDeviceChange: boolean,
44
-
45
-    /**
46
-     * Whether video input dropdown should be enabled or not.
47
-     */
48
-    disableVideoInputSelect: boolean,
49
-
50
-    /**
51
-     * Whether or not the audio permission was granted.
52
-     */
53
-    hasAudioPermission: boolean,
54
-
55
-    /**
56
-     * Whether or not the audio permission was granted.
57
-     */
58
-    hasVideoPermission: boolean,
59
-
60
-    /**
61
-     * If true, the audio meter will not display. Necessary for browsers or
62
-     * configurations that do not support local stats to prevent a
63
-     * non-responsive mic preview from displaying.
64
-     */
65
-    hideAudioInputPreview: boolean,
66
-
67
-    /**
68
-     * If true, the button to play a test sound on the selected speaker will not be displayed.
69
-     * This needs to be hidden on browsers that do not support selecting an audio output device.
70
-     */
71
-    hideAudioOutputPreview: boolean,
72
-
73
-    /**
74
-     * Whether or not the audio output source selector should display. If
75
-     * true, the audio output selector and test audio link will not be
76
-     * rendered.
77
-     */
78
-    hideAudioOutputSelect: boolean,
79
-
80
-    /**
81
-     * Whether or not the hid device container should display.
82
-     */
83
-    hideDeviceHIDContainer: boolean,
84
-
85
-    /**
86
-     * Whether video input preview should be displayed or not.
87
-     * (In the case of iOS Safari).
88
-     */
89
-    hideVideoInputPreview: boolean,
90
-
91
-    /**
92
-     * The id of the audio input device to preview.
93
-     */
94
-    selectedAudioInputId: string,
95
-
96
-    /**
97
-     * The id of the audio output device to preview.
98
-     */
99
-    selectedAudioOutputId: string,
100
-
101
-    /**
102
-     * The id of the video input device to preview.
103
-     */
104
-    selectedVideoInputId: string,
105
-
106
-    /**
107
-     * Invoked to obtain translated strings.
108
-     */
109
-    t: Function
110
-};
111
-
112
-/**
113
- * The type of the React {@code Component} state of {@link DeviceSelection}.
114
- */
115
-type State = {
116
-
117
-    /**
118
-     * The JitsiTrack to use for previewing audio input.
119
-     */
120
-    previewAudioTrack: ?Object,
121
-
122
-    /**
123
-     * The JitsiTrack to use for previewing video input.
124
-     */
125
-    previewVideoTrack: ?Object,
126
-
127
-    /**
128
-     * The error message from trying to use a video input device.
129
-     */
130
-    previewVideoTrackError: ?string
131
-};
132
-
133
-/**
134
- * React {@code Component} for previewing audio and video input/output devices.
135
- *
136
- * @augments Component
137
- */
138
-class DeviceSelection extends AbstractDialogTab<Props, State> {
139
-
140
-    /**
141
-     * Whether current component is mounted or not.
142
-     *
143
-     * In component did mount we start a Promise to create tracks and
144
-     * set the tracks in the state, if we unmount the component in the meanwhile
145
-     * tracks will be created and will never been disposed (dispose tracks is
146
-     * in componentWillUnmount). When tracks are created and component is
147
-     * unmounted we dispose the tracks.
148
-     */
149
-    _unMounted: boolean;
150
-
151
-    /**
152
-     * Initializes a new DeviceSelection instance.
153
-     *
154
-     * @param {Object} props - The read-only React Component props with which
155
-     * the new instance is to be initialized.
156
-     */
157
-    constructor(props: Props) {
158
-        super(props);
159
-
160
-        this.state = {
161
-            previewAudioTrack: null,
162
-            previewVideoTrack: null,
163
-            previewVideoTrackError: null
164
-        };
165
-        this._unMounted = true;
166
-    }
167
-
168
-    /**
169
-     * Generate the initial previews for audio input and video input.
170
-     *
171
-     * @inheritdoc
172
-     */
173
-    componentDidMount() {
174
-        this._unMounted = false;
175
-        Promise.all([
176
-            this._createAudioInputTrack(this.props.selectedAudioInputId),
177
-            this._createVideoInputTrack(this.props.selectedVideoInputId)
178
-        ])
179
-        .catch(err => logger.warn('Failed to initialize preview tracks', err))
180
-            .then(() => getAvailableDevices());
181
-    }
182
-
183
-    /**
184
-     * Checks if audio / video permissions were granted. Updates audio input and
185
-     * video input previews.
186
-     *
187
-     * @param {Object} prevProps - Previous props this component received.
188
-     * @returns {void}
189
-     */
190
-    componentDidUpdate(prevProps) {
191
-        if (prevProps.selectedAudioInputId
192
-            !== this.props.selectedAudioInputId) {
193
-            this._createAudioInputTrack(this.props.selectedAudioInputId);
194
-        }
195
-
196
-        if (prevProps.selectedVideoInputId
197
-            !== this.props.selectedVideoInputId) {
198
-            this._createVideoInputTrack(this.props.selectedVideoInputId);
199
-        }
200
-    }
201
-
202
-    /**
203
-     * Ensure preview tracks are destroyed to prevent continued use.
204
-     *
205
-     * @inheritdoc
206
-     */
207
-    componentWillUnmount() {
208
-        this._unMounted = true;
209
-        this._disposeAudioInputPreview();
210
-        this._disposeVideoInputPreview();
211
-    }
212
-
213
-    /**
214
-     * Implements React's {@link Component#render()}.
215
-     *
216
-     * @inheritdoc
217
-     */
218
-    render() {
219
-        const {
220
-            hideAudioInputPreview,
221
-            hideAudioOutputPreview,
222
-            hideDeviceHIDContainer,
223
-            hideVideoInputPreview,
224
-            selectedAudioOutputId
225
-        } = this.props;
226
-
227
-        return (
228
-            <div className = { `device-selection${hideVideoInputPreview ? ' video-hidden' : ''}` }>
229
-                <div className = 'device-selection-column column-video'>
230
-                    { !hideVideoInputPreview
231
-                        && <div className = 'device-selection-video-container'>
232
-                            <VideoInputPreview
233
-                                error = { this.state.previewVideoTrackError }
234
-                                track = { this.state.previewVideoTrack } />
235
-                        </div>
236
-                    }
237
-                    { !hideAudioInputPreview
238
-                        && <AudioInputPreview
239
-                            track = { this.state.previewAudioTrack } /> }
240
-                </div>
241
-                <div className = 'device-selection-column column-selectors'>
242
-                    <div
243
-                        aria-live = 'polite all'
244
-                        className = 'device-selectors'>
245
-                        { this._renderSelectors() }
246
-                    </div>
247
-                    { !hideAudioOutputPreview
248
-                        && <AudioOutputPreview
249
-                            deviceId = { selectedAudioOutputId } /> }
250
-                    { !hideDeviceHIDContainer
251
-                        && <DeviceHidContainer /> }
252
-                </div>
253
-            </div>
254
-        );
255
-    }
256
-
257
-    /**
258
-     * Creates the JitiTrack for the audio input preview.
259
-     *
260
-     * @param {string} deviceId - The id of audio input device to preview.
261
-     * @private
262
-     * @returns {void}
263
-     */
264
-    _createAudioInputTrack(deviceId) {
265
-        const { hideAudioInputPreview } = this.props;
266
-
267
-        if (hideAudioInputPreview) {
268
-            return;
269
-        }
270
-
271
-        return this._disposeAudioInputPreview()
272
-            .then(() => createLocalTrack('audio', deviceId, 5000))
273
-            .then(jitsiLocalTrack => {
274
-                if (this._unMounted) {
275
-                    jitsiLocalTrack.dispose();
276
-
277
-                    return;
278
-                }
279
-
280
-                this.setState({
281
-                    previewAudioTrack: jitsiLocalTrack
282
-                });
283
-            })
284
-            .catch(() => {
285
-                this.setState({
286
-                    previewAudioTrack: null
287
-                });
288
-            });
289
-    }
290
-
291
-    /**
292
-     * Creates the JitiTrack for the video input preview.
293
-     *
294
-     * @param {string} deviceId - The id of video device to preview.
295
-     * @private
296
-     * @returns {void}
297
-     */
298
-    _createVideoInputTrack(deviceId) {
299
-        const { hideVideoInputPreview } = this.props;
300
-
301
-        if (hideVideoInputPreview) {
302
-            return;
303
-        }
304
-
305
-        return this._disposeVideoInputPreview()
306
-            .then(() => createLocalTrack('video', deviceId, 5000))
307
-            .then(jitsiLocalTrack => {
308
-                if (!jitsiLocalTrack) {
309
-                    return Promise.reject();
310
-                }
311
-
312
-                if (this._unMounted) {
313
-                    jitsiLocalTrack.dispose();
314
-
315
-                    return;
316
-                }
317
-
318
-                this.setState({
319
-                    previewVideoTrack: jitsiLocalTrack,
320
-                    previewVideoTrackError: null
321
-                });
322
-            })
323
-            .catch(() => {
324
-                this.setState({
325
-                    previewVideoTrack: null,
326
-                    previewVideoTrackError:
327
-                        this.props.t('deviceSelection.previewUnavailable')
328
-                });
329
-            });
330
-    }
331
-
332
-    /**
333
-     * Utility function for disposing the current audio input preview.
334
-     *
335
-     * @private
336
-     * @returns {Promise}
337
-     */
338
-    _disposeAudioInputPreview(): Promise<*> {
339
-        return this.state.previewAudioTrack
340
-            ? this.state.previewAudioTrack.dispose() : Promise.resolve();
341
-    }
342
-
343
-    /**
344
-     * Utility function for disposing the current video input preview.
345
-     *
346
-     * @private
347
-     * @returns {Promise}
348
-     */
349
-    _disposeVideoInputPreview(): Promise<*> {
350
-        return this.state.previewVideoTrack
351
-            ? this.state.previewVideoTrack.dispose() : Promise.resolve();
352
-    }
353
-
354
-    /**
355
-     * Creates a DeviceSelector instance based on the passed in configuration.
356
-     *
357
-     * @private
358
-     * @param {Object} deviceSelectorProps - The props for the DeviceSelector.
359
-     * @returns {ReactElement}
360
-     */
361
-    _renderSelector(deviceSelectorProps) {
362
-        return (
363
-            <div key = { deviceSelectorProps.label }>
364
-                <label
365
-                    className = 'device-selector-label'
366
-                    htmlFor = { deviceSelectorProps.id }>
367
-                    { this.props.t(deviceSelectorProps.label) }
368
-                </label>
369
-                <DeviceSelector { ...deviceSelectorProps } />
370
-            </div>
371
-        );
372
-    }
373
-
374
-    /**
375
-     * Creates DeviceSelector instances for video output, audio input, and audio
376
-     * output.
377
-     *
378
-     * @private
379
-     * @returns {Array<ReactElement>} DeviceSelector instances.
380
-     */
381
-    _renderSelectors() {
382
-        const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
383
-
384
-        const configurations = [
385
-            {
386
-                devices: availableDevices.audioInput,
387
-                hasPermission: hasAudioPermission,
388
-                icon: 'icon-microphone',
389
-                isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
390
-                key: 'audioInput',
391
-                id: 'audioInput',
392
-                label: 'settings.selectMic',
393
-                onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
394
-                selectedDeviceId: this.state.previewAudioTrack
395
-                    ? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
396
-            },
397
-            {
398
-                devices: availableDevices.videoInput,
399
-                hasPermission: hasVideoPermission,
400
-                icon: 'icon-camera',
401
-                isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
402
-                key: 'videoInput',
403
-                id: 'videoInput',
404
-                label: 'settings.selectCamera',
405
-                onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
406
-                selectedDeviceId: this.state.previewVideoTrack
407
-                    ? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
408
-            }
409
-        ];
410
-
411
-        if (!this.props.hideAudioOutputSelect) {
412
-            configurations.push({
413
-                devices: availableDevices.audioOutput,
414
-                hasPermission: hasAudioPermission || hasVideoPermission,
415
-                icon: 'icon-speaker',
416
-                isDisabled: this.props.disableDeviceChange,
417
-                key: 'audioOutput',
418
-                id: 'audioOutput',
419
-                label: 'settings.selectAudioOutput',
420
-                onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
421
-                selectedDeviceId: this.props.selectedAudioOutputId
422
-            });
423
-        }
424
-
425
-        return configurations.map(config => this._renderSelector(config));
426
-    }
427
-}
428
-
429
-export default translate(DeviceSelection);

react/features/device-selection/components/DeviceSelector.web.js → react/features/device-selection/components/DeviceSelector.web.tsx ファイルの表示

@@ -1,58 +1,76 @@
1
-/* @flow */
1
+import { Theme } from '@mui/material';
2
+import { withStyles } from '@mui/styles';
2 3
 import React, { Component } from 'react';
4
+import { WithTranslation } from 'react-i18next';
3 5
 
4 6
 import { translate } from '../../base/i18n/functions';
7
+import { withPixelLineHeight } from '../../base/styles/functions.web';
5 8
 import Select from '../../base/ui/components/web/Select';
6 9
 
7 10
 /**
8 11
  * The type of the React {@code Component} props of {@link DeviceSelector}.
9 12
  */
10
-type Props = {
13
+interface IProps extends WithTranslation {
14
+
15
+    /**
16
+     * CSS classes object.
17
+     */
18
+    classes: any;
11 19
 
12 20
     /**
13 21
      * MediaDeviceInfos used for display in the select element.
14 22
      */
15
-    devices: Array<Object>,
23
+    devices: Array<MediaDeviceInfo> | undefined;
16 24
 
17 25
     /**
18 26
      * If false, will return a selector with no selection options.
19 27
      */
20
-    hasPermission: boolean,
28
+    hasPermission: boolean;
21 29
 
22 30
     /**
23 31
      * CSS class for the icon to the left of the dropdown trigger.
24 32
      */
25
-    icon: string,
33
+    icon: string;
34
+
35
+    /**
36
+     * The id of the dropdown element.
37
+     */
38
+    id: string;
26 39
 
27 40
     /**
28 41
      * If true, will render the selector disabled with a default selection.
29 42
      */
30
-    isDisabled: boolean,
43
+    isDisabled: boolean;
31 44
 
32 45
     /**
33 46
      * The translation key to display as a menu label.
34 47
      */
35
-    label: string,
48
+    label: string;
36 49
 
37 50
     /**
38 51
      * The callback to invoke when a selection is made.
39 52
      */
40
-    onSelect: Function,
53
+    onSelect: Function;
41 54
 
42 55
     /**
43 56
      * The default device to display as selected.
44 57
      */
45
-    selectedDeviceId: string,
46
-
47
-    /**
48
-     * Invoked to obtain translated strings.
49
-     */
50
-    t: Function,
58
+    selectedDeviceId: string;
59
+}
51 60
 
52
-    /**
53
-     * The id of the dropdown element.
54
-     */
55
-    id: string
61
+const styles = (theme: Theme) => {
62
+    return {
63
+        textSelector: {
64
+            width: '100%',
65
+            boxSizing: 'border-box',
66
+            borderRadius: theme.shape.borderRadius,
67
+            backgroundColor: theme.palette.uiBackground,
68
+            padding: '10px 16px',
69
+            textAlign: 'center',
70
+            ...withPixelLineHeight(theme.typography.bodyShortRegular),
71
+            border: `1px solid ${theme.palette.ui03}`
72
+        }
73
+    };
56 74
 };
57 75
 
58 76
 /**
@@ -61,17 +79,18 @@ type Props = {
61 79
  *
62 80
  * @augments Component
63 81
  */
64
-class DeviceSelector extends Component<Props> {
82
+class DeviceSelector extends Component<IProps> {
65 83
     /**
66 84
      * Initializes a new DeviceSelector instance.
67 85
      *
68 86
      * @param {Object} props - The read-only React Component props with which
69 87
      * the new instance is to be initialized.
70 88
      */
71
-    constructor(props) {
89
+    constructor(props: IProps) {
72 90
         super(props);
73 91
 
74 92
         this._onSelect = this._onSelect.bind(this);
93
+        this._createDropdown = this._createDropdown.bind(this);
75 94
     }
76 95
 
77 96
     /**
@@ -111,25 +130,6 @@ class DeviceSelector extends Component<Props> {
111 130
         });
112 131
     }
113 132
 
114
-    /**
115
-     * Creates a React Element for displaying the passed in text surrounded by
116
-     * two icons. The left icon is the icon class passed in through props and
117
-     * the right icon is AtlasKit ExpandIcon.
118
-     *
119
-     * @param {string} triggerText - The text to display within the element.
120
-     * @private
121
-     * @returns {ReactElement}
122
-     */
123
-    _createDropdownTrigger(triggerText) {
124
-        return (
125
-            <div className = 'device-selector-trigger'>
126
-                <span className = 'device-selector-trigger-text'>
127
-                    { triggerText }
128
-                </span>
129
-            </div>
130
-        );
131
-    }
132
-
133 133
     /**
134 134
      * Creates a AKDropdownMenu Component using passed in props and options. If
135 135
      * the dropdown needs to be disabled, then only the AKDropdownMenu trigger
@@ -146,32 +146,30 @@ class DeviceSelector extends Component<Props> {
146 146
      * @private
147 147
      * @returns {ReactElement}
148 148
      */
149
-    _createDropdown(options) {
149
+    _createDropdown(options: { defaultSelected?: MediaDeviceInfo; isDisabled: boolean;
150
+        items?: Array<{ label: string; value: string; }>; placeholder: string; }) {
150 151
         const triggerText
151 152
             = (options.defaultSelected && (options.defaultSelected.label || options.defaultSelected.deviceId))
152 153
                 || options.placeholder;
153
-        const trigger = this._createDropdownTrigger(triggerText);
154
+        const { classes } = this.props;
154 155
 
155
-        if (options.isDisabled || !options.items.length) {
156
+        if (options.isDisabled || !options.items?.length) {
156 157
             return (
157
-                <div className = 'device-selector-trigger-disabled'>
158
-                    { trigger }
158
+                <div className = { classes.textSelector }>
159
+                    {triggerText}
159 160
                 </div>
160 161
             );
161 162
         }
162 163
 
163 164
         return (
164
-            <div className = 'dropdown-menu'>
165
-                <Select
166
-                    onChange = { this._onSelect }
167
-                    options = { options.items }
168
-                    value = { this.props.selectedDeviceId } />
169
-            </div>
165
+            <Select
166
+                label = { this.props.t(this.props.label) }
167
+                onChange = { this._onSelect }
168
+                options = { options.items }
169
+                value = { this.props.selectedDeviceId } />
170 170
         );
171 171
     }
172 172
 
173
-    _onSelect: (Object) => void;
174
-
175 173
     /**
176 174
      * Invokes the passed in callback to notify of selection changes.
177 175
      *
@@ -180,7 +178,7 @@ class DeviceSelector extends Component<Props> {
180 178
      * @private
181 179
      * @returns {void}
182 180
      */
183
-    _onSelect(e) {
181
+    _onSelect(e: React.ChangeEvent<HTMLSelectElement>) {
184 182
         const deviceId = e.target.value;
185 183
 
186 184
         if (this.props.selectedDeviceId !== deviceId) {
@@ -217,4 +215,4 @@ class DeviceSelector extends Component<Props> {
217 215
     }
218 216
 }
219 217
 
220
-export default translate(DeviceSelector);
218
+export default withStyles(styles)(translate(DeviceSelector));

+ 368
- 0
react/features/device-selection/components/VideoDeviceSelection.web.tsx ファイルの表示

@@ -0,0 +1,368 @@
1
+import { Theme } from '@mui/material';
2
+import { withStyles } from '@mui/styles';
3
+import React from 'react';
4
+import { WithTranslation } from 'react-i18next';
5
+import { connect } from 'react-redux';
6
+
7
+import { IReduxState, IStore } from '../../app/types';
8
+import { getAvailableDevices } from '../../base/devices/actions.web';
9
+import AbstractDialogTab, {
10
+    type IProps as AbstractDialogTabProps
11
+} from '../../base/dialog/components/web/AbstractDialogTab';
12
+import { translate } from '../../base/i18n/functions';
13
+import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
14
+import Checkbox from '../../base/ui/components/web/Checkbox';
15
+import Select from '../../base/ui/components/web/Select';
16
+import { SS_DEFAULT_FRAME_RATE } from '../../settings/constants';
17
+import logger from '../logger';
18
+
19
+import DeviceSelector from './DeviceSelector.web';
20
+import VideoInputPreview from './VideoInputPreview';
21
+
22
+/**
23
+ * The type of the React {@code Component} props of {@link VideoDeviceSelection}.
24
+ */
25
+export interface IProps extends AbstractDialogTabProps, WithTranslation {
26
+
27
+    /**
28
+     * All known audio and video devices split by type. This prop comes from
29
+     * the app state.
30
+     */
31
+    availableDevices: { videoInput?: MediaDeviceInfo[]; };
32
+
33
+    /**
34
+     * CSS classes object.
35
+     */
36
+    classes: any;
37
+
38
+    /**
39
+     * The currently selected desktop share frame rate in the frame rate select dropdown.
40
+     */
41
+    currentFramerate: string;
42
+
43
+    /**
44
+     * All available desktop capture frame rates.
45
+     */
46
+    desktopShareFramerates: Array<number>;
47
+
48
+    /**
49
+     * True if device changing is configured to be disallowed. Selectors
50
+     * will display as disabled.
51
+     */
52
+    disableDeviceChange: boolean;
53
+
54
+    /**
55
+     * Whether video input dropdown should be enabled or not.
56
+     */
57
+    disableVideoInputSelect: boolean;
58
+
59
+    /**
60
+     * Redux dispatch.
61
+     */
62
+    dispatch: IStore['dispatch'];
63
+
64
+    /**
65
+     * Whether or not the audio permission was granted.
66
+     */
67
+    hasVideoPermission: boolean;
68
+
69
+    /**
70
+     * Whether to hide the additional settings or not.
71
+     */
72
+    hideAdditionalSettings: boolean;
73
+
74
+    /**
75
+     * Whether video input preview should be displayed or not.
76
+     * (In the case of iOS Safari).
77
+     */
78
+    hideVideoInputPreview: boolean;
79
+
80
+    /**
81
+     * Whether or not the local video is flipped.
82
+     */
83
+    localFlipX: boolean;
84
+
85
+    /**
86
+     * The id of the video input device to preview.
87
+     */
88
+    selectedVideoInputId: string;
89
+}
90
+
91
+/**
92
+ * The type of the React {@code Component} state of {@link VideoDeviceSelection}.
93
+ */
94
+type State = {
95
+
96
+    /**
97
+     * The JitsiTrack to use for previewing video input.
98
+     */
99
+    previewVideoTrack: any | null;
100
+
101
+    /**
102
+     * The error message from trying to use a video input device.
103
+     */
104
+    previewVideoTrackError: string | null;
105
+};
106
+
107
+const styles = (theme: Theme) => {
108
+    return {
109
+        container: {
110
+            display: 'flex',
111
+            flexDirection: 'column' as const,
112
+            padding: '0 2px',
113
+            width: '100%'
114
+        },
115
+
116
+        checkboxContainer: {
117
+            margin: `${theme.spacing(4)} 0`
118
+        }
119
+    };
120
+};
121
+
122
+/**
123
+ * React {@code Component} for previewing audio and video input/output devices.
124
+ *
125
+ * @augments Component
126
+ */
127
+class VideoDeviceSelection extends AbstractDialogTab<IProps, State> {
128
+
129
+    /**
130
+     * Whether current component is mounted or not.
131
+     *
132
+     * In component did mount we start a Promise to create tracks and
133
+     * set the tracks in the state, if we unmount the component in the meanwhile
134
+     * tracks will be created and will never been disposed (dispose tracks is
135
+     * in componentWillUnmount). When tracks are created and component is
136
+     * unmounted we dispose the tracks.
137
+     */
138
+    _unMounted: boolean;
139
+
140
+    /**
141
+     * Initializes a new DeviceSelection instance.
142
+     *
143
+     * @param {Object} props - The read-only React Component props with which
144
+     * the new instance is to be initialized.
145
+     */
146
+    constructor(props: IProps) {
147
+        super(props);
148
+
149
+        this.state = {
150
+            previewVideoTrack: null,
151
+            previewVideoTrackError: null
152
+        };
153
+        this._unMounted = true;
154
+
155
+        this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
156
+    }
157
+
158
+    /**
159
+     * Generate the initial previews for audio input and video input.
160
+     *
161
+     * @inheritdoc
162
+     */
163
+    componentDidMount() {
164
+        this._unMounted = false;
165
+        Promise.all([
166
+            this._createVideoInputTrack(this.props.selectedVideoInputId)
167
+        ])
168
+        .catch(err => logger.warn('Failed to initialize preview tracks', err))
169
+            .then(() => {
170
+                this.props.dispatch(getAvailableDevices());
171
+            });
172
+    }
173
+
174
+    /**
175
+     * Checks if audio / video permissions were granted. Updates audio input and
176
+     * video input previews.
177
+     *
178
+     * @param {Object} prevProps - Previous props this component received.
179
+     * @returns {void}
180
+     */
181
+    componentDidUpdate(prevProps: IProps) {
182
+
183
+        if (prevProps.selectedVideoInputId
184
+            !== this.props.selectedVideoInputId) {
185
+            this._createVideoInputTrack(this.props.selectedVideoInputId);
186
+        }
187
+    }
188
+
189
+    /**
190
+     * Ensure preview tracks are destroyed to prevent continued use.
191
+     *
192
+     * @inheritdoc
193
+     */
194
+    componentWillUnmount() {
195
+        this._unMounted = true;
196
+        this._disposeVideoInputPreview();
197
+    }
198
+
199
+    /**
200
+     * Implements React's {@link Component#render()}.
201
+     *
202
+     * @inheritdoc
203
+     */
204
+    render() {
205
+        const {
206
+            classes,
207
+            hideAdditionalSettings,
208
+            hideVideoInputPreview,
209
+            localFlipX,
210
+            t
211
+        } = this.props;
212
+
213
+        return (
214
+            <div className = { classes.container }>
215
+                { !hideVideoInputPreview
216
+                    && <VideoInputPreview
217
+                        error = { this.state.previewVideoTrackError }
218
+                        localFlipX = { localFlipX }
219
+                        track = { this.state.previewVideoTrack } />
220
+                }
221
+                <div
222
+                    aria-live = 'polite'>
223
+                    {this._renderVideoSelector()}
224
+                </div>
225
+                {!hideAdditionalSettings && (
226
+                    <>
227
+                        <div className = { classes.checkboxContainer }>
228
+                            <Checkbox
229
+                                checked = { localFlipX }
230
+                                label = { t('videothumbnail.mirrorVideo') }
231
+                                // eslint-disable-next-line react/jsx-no-bind
232
+                                onChange = { () => super._onChange({ localFlipX: !localFlipX }) } />
233
+                        </div>
234
+                        {this._renderFramerateSelect()}
235
+                    </>
236
+                )}
237
+            </div>
238
+        );
239
+    }
240
+
241
+    /**
242
+     * Creates the JitsiTrack for the video input preview.
243
+     *
244
+     * @param {string} deviceId - The id of video device to preview.
245
+     * @private
246
+     * @returns {void}
247
+     */
248
+    _createVideoInputTrack(deviceId: string) {
249
+        const { hideVideoInputPreview } = this.props;
250
+
251
+        if (hideVideoInputPreview) {
252
+            return;
253
+        }
254
+
255
+        return this._disposeVideoInputPreview()
256
+            .then(() => createLocalTrack('video', deviceId, 5000))
257
+            .then(jitsiLocalTrack => {
258
+                if (!jitsiLocalTrack) {
259
+                    return Promise.reject();
260
+                }
261
+
262
+                if (this._unMounted) {
263
+                    jitsiLocalTrack.dispose();
264
+
265
+                    return;
266
+                }
267
+
268
+                this.setState({
269
+                    previewVideoTrack: jitsiLocalTrack,
270
+                    previewVideoTrackError: null
271
+                });
272
+            })
273
+            .catch(() => {
274
+                this.setState({
275
+                    previewVideoTrack: null,
276
+                    previewVideoTrackError:
277
+                        this.props.t('deviceSelection.previewUnavailable')
278
+                });
279
+            });
280
+    }
281
+
282
+    /**
283
+     * Utility function for disposing the current video input preview.
284
+     *
285
+     * @private
286
+     * @returns {Promise}
287
+     */
288
+    _disposeVideoInputPreview(): Promise<any> {
289
+        return this.state.previewVideoTrack
290
+            ? this.state.previewVideoTrack.dispose() : Promise.resolve();
291
+    }
292
+
293
+    /**
294
+     * Creates a DeviceSelector instance based on the passed in configuration.
295
+     *
296
+     * @private
297
+     * @returns {ReactElement}
298
+     */
299
+    _renderVideoSelector() {
300
+        const { availableDevices, hasVideoPermission } = this.props;
301
+
302
+        const videoConfig = {
303
+            devices: availableDevices.videoInput,
304
+            hasPermission: hasVideoPermission,
305
+            icon: 'icon-camera',
306
+            isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
307
+            key: 'videoInput',
308
+            id: 'videoInput',
309
+            label: 'settings.selectCamera',
310
+            onSelect: (selectedVideoInputId: string) => super._onChange({ selectedVideoInputId }),
311
+            selectedDeviceId: this.state.previewVideoTrack
312
+                ? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
313
+        };
314
+
315
+        return (
316
+            <DeviceSelector
317
+                { ...videoConfig }
318
+                key = { videoConfig.id } />
319
+        );
320
+    }
321
+
322
+    /**
323
+     * Callback invoked to select a frame rate from the select dropdown.
324
+     *
325
+     * @param {Object} e - The key event to handle.
326
+     * @private
327
+     * @returns {void}
328
+     */
329
+    _onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
330
+        const frameRate = e.target.value;
331
+
332
+        super._onChange({ currentFramerate: frameRate });
333
+    }
334
+
335
+    /**
336
+     * Returns the React Element for the desktop share frame rate dropdown.
337
+     *
338
+     * @returns {JSX}
339
+     */
340
+    _renderFramerateSelect() {
341
+        const { currentFramerate, desktopShareFramerates, t } = this.props;
342
+        const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
343
+            return {
344
+                value: frameRate,
345
+                label: `${frameRate} ${t('settings.framesPerSecond')}`
346
+            };
347
+        });
348
+
349
+        return (
350
+            <Select
351
+                bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
352
+                    ? t('settings.desktopShareHighFpsWarning')
353
+                    : t('settings.desktopShareWarning') }
354
+                label = { t('settings.desktopShareFramerate') }
355
+                onChange = { this._onFramerateItemSelect }
356
+                options = { frameRateItems }
357
+                value = { currentFramerate } />
358
+        );
359
+    }
360
+}
361
+
362
+const mapStateToProps = (state: IReduxState) => {
363
+    return {
364
+        availableDevices: state['features/base/devices'].availableDevices ?? {}
365
+    };
366
+};
367
+
368
+export default connect(mapStateToProps)(withStyles(styles)(translate(VideoDeviceSelection)));

+ 0
- 58
react/features/device-selection/components/VideoInputPreview.js ファイルの表示

@@ -1,58 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-import Video from '../../base/media/components/Video';
6
-
7
-const VIDEO_ERROR_CLASS = 'video-preview-has-error';
8
-
9
-/**
10
- * The type of the React {@code Component} props of {@link VideoInputPreview}.
11
- */
12
-type Props = {
13
-
14
-    /**
15
-     * An error message to display instead of a preview. Displaying an error
16
-     * will take priority over displaying a video preview.
17
-     */
18
-    error: ?string,
19
-
20
-    /**
21
-     * The JitsiLocalTrack to display.
22
-     */
23
-    track: Object
24
-};
25
-
26
-/**
27
- * React component for displaying video. This component defers to lib-jitsi-meet
28
- * logic for rendering the video.
29
- *
30
- * @augments Component
31
- */
32
-class VideoInputPreview extends Component<Props> {
33
-    /**
34
-     * Implements React's {@link Component#render()}.
35
-     *
36
-     * @inheritdoc
37
-     * @returns {ReactElement}
38
-     */
39
-    render() {
40
-        const { error } = this.props;
41
-        const errorClass = error ? VIDEO_ERROR_CLASS : '';
42
-        const className = `video-input-preview ${errorClass}`;
43
-
44
-        return (
45
-            <div className = { className }>
46
-                <Video
47
-                    className = 'video-input-preview-display flipVideoX'
48
-                    playsinline = { true }
49
-                    videoTrack = {{ jitsiTrack: this.props.track }} />
50
-                <div className = 'video-input-preview-error'>
51
-                    { error || '' }
52
-                </div>
53
-            </div>
54
-        );
55
-    }
56
-}
57
-
58
-export default VideoInputPreview;

+ 73
- 0
react/features/device-selection/components/VideoInputPreview.web.tsx ファイルの表示

@@ -0,0 +1,73 @@
1
+import React from 'react';
2
+import { makeStyles } from 'tss-react/mui';
3
+
4
+import Video from '../../base/media/components/Video.web';
5
+
6
+/**
7
+ * The type of the React {@code Component} props of {@link VideoInputPreview}.
8
+ */
9
+interface IProps {
10
+
11
+    /**
12
+     * An error message to display instead of a preview. Displaying an error
13
+     * will take priority over displaying a video preview.
14
+     */
15
+    error: string | null;
16
+
17
+    /**
18
+     * Whether or not the local video is flipped.
19
+     */
20
+    localFlipX: boolean;
21
+
22
+    /**
23
+     * The JitsiLocalTrack to display.
24
+     */
25
+    track: Object;
26
+}
27
+
28
+const useStyles = makeStyles()(theme => {
29
+    return {
30
+        container: {
31
+            position: 'relative',
32
+            borderRadius: '3px',
33
+            overflow: 'hidden',
34
+            marginBottom: theme.spacing(4),
35
+            backgroundColor: theme.palette.uiBackground
36
+        },
37
+
38
+        video: {
39
+            height: 'auto',
40
+            width: '100%',
41
+            overflow: 'hidden'
42
+        },
43
+
44
+        errorText: {
45
+            color: theme.palette.text01,
46
+            left: 0,
47
+            position: 'absolute',
48
+            right: 0,
49
+            textAlign: 'center',
50
+            top: '50%'
51
+        }
52
+    };
53
+});
54
+
55
+const VideoInputPreview = ({ error, localFlipX, track }: IProps) => {
56
+    const { classes, cx } = useStyles();
57
+
58
+    return (
59
+        <div className = { classes.container }>
60
+            <Video
61
+                className = { cx(classes.video, localFlipX && 'flipVideoX') }
62
+                playsinline = { true }
63
+                videoTrack = {{ jitsiTrack: track }} />
64
+            {error && (
65
+                <div className = { classes.errorText }>
66
+                    {error}
67
+                </div>
68
+            )}
69
+        </div>
70
+    );
71
+};
72
+
73
+export default VideoInputPreview;

+ 0
- 4
react/features/device-selection/components/index.js ファイルの表示

@@ -1,4 +0,0 @@
1
-// @flow
2
-
3
-export { default as DeviceSelection } from './DeviceSelection';
4
-export type { Props as DeviceSelectionProps } from './DeviceSelection';

+ 60
- 13
react/features/device-selection/functions.web.ts ファイルの表示

@@ -21,18 +21,21 @@ import {
21 21
     getUserSelectedMicDeviceId,
22 22
     getUserSelectedOutputDeviceId
23 23
 } from '../base/settings/functions.web';
24
+import { isNoiseSuppressionEnabled } from '../noise-suppression/functions';
25
+import { isPrejoinPageVisible } from '../prejoin/functions';
26
+import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from '../settings/constants';
24 27
 import { isDeviceHidSupported } from '../web-hid/functions';
25 28
 
26 29
 /**
27
- * Returns the properties for the device selection dialog from Redux state.
30
+ * Returns the properties for the audio device selection dialog from Redux state.
28 31
  *
29 32
  * @param {IStateful} stateful -The (whole) redux state, or redux's
30 33
  * {@code getState} function to be used to retrieve the state.
31 34
  * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
32 35
  * welcome page or not.
33
- * @returns {Object} - The properties for the device selection dialog.
36
+ * @returns {Object} - The properties for the audio device selection dialog.
34 37
  */
35
-export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
38
+export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
36 39
     // On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
37 40
     // by the browser when a new track is created for preview. That's why we are disabling all previews.
38 41
     const disablePreviews = isIosMobileBrowser();
@@ -42,18 +45,17 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
42 45
     const { permissions } = state['features/base/devices'];
43 46
     const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
44 47
     const speakerChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output');
45
-    const userSelectedCamera = getUserSelectedCameraDeviceId(state);
46 48
     const userSelectedMic = getUserSelectedMicDeviceId(state);
47 49
     const deviceHidSupported = isDeviceHidSupported();
50
+    const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
51
+    const hideNoiseSuppression = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
48 52
 
49 53
     // When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
50 54
     // case for Safari on iOS.
51 55
     let disableAudioInputChange
52 56
         = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported() && !(disablePreviews && inputDeviceChangeSupported);
53
-    let disableVideoInputSelect = !inputDeviceChangeSupported;
54 57
     let selectedAudioInputId = settings.micDeviceId;
55 58
     let selectedAudioOutputId = getAudioOutputDeviceId();
56
-    let selectedVideoInputId = settings.cameraDeviceId;
57 59
 
58 60
     // audio input change will be a problem only when we are in a
59 61
     // conference and this is not supported, when we open device selection on
@@ -61,28 +63,73 @@ export function getDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOn
61 63
     // on welcome page we also show only what we have saved as user selected devices
62 64
     if (isDisplayedOnWelcomePage) {
63 65
         disableAudioInputChange = false;
64
-        disableVideoInputSelect = false;
65 66
         selectedAudioInputId = userSelectedMic;
66 67
         selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
67
-        selectedVideoInputId = userSelectedCamera;
68 68
     }
69 69
 
70 70
     // we fill the device selection dialog with the devices that are currently
71 71
     // used or if none are currently used with what we have in settings(user selected)
72 72
     return {
73
-        availableDevices: state['features/base/devices'].availableDevices,
74 73
         disableAudioInputChange,
75 74
         disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
76
-        disableVideoInputSelect,
77 75
         hasAudioPermission: permissions.audio,
78
-        hasVideoPermission: permissions.video,
79 76
         hideAudioInputPreview: disableAudioInputChange || !JitsiMeetJS.isCollectingLocalStats() || disablePreviews,
80 77
         hideAudioOutputPreview: !speakerChangeSupported || disablePreviews,
81 78
         hideAudioOutputSelect: !speakerChangeSupported,
82 79
         hideDeviceHIDContainer: !deviceHidSupported,
83
-        hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews,
80
+        hideNoiseSuppression,
81
+        noiseSuppressionEnabled,
84 82
         selectedAudioInputId,
85
-        selectedAudioOutputId,
83
+        selectedAudioOutputId
84
+    };
85
+}
86
+
87
+/**
88
+ * Returns the properties for the device selection dialog from Redux state.
89
+ *
90
+ * @param {IStateful} stateful -The (whole) redux state, or redux's
91
+ * {@code getState} function to be used to retrieve the state.
92
+ * @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
93
+ * welcome page or not.
94
+ * @returns {Object} - The properties for the device selection dialog.
95
+ */
96
+export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
97
+    // On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
98
+    // by the browser when a new track is created for preview. That's why we are disabling all previews.
99
+    const disablePreviews = isIosMobileBrowser();
100
+
101
+    const state = toState(stateful);
102
+    const settings = state['features/base/settings'];
103
+    const { permissions } = state['features/base/devices'];
104
+    const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input');
105
+    const userSelectedCamera = getUserSelectedCameraDeviceId(state);
106
+    const { localFlipX } = state['features/base/settings'];
107
+    const hideAdditionalSettings = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
108
+    const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
109
+
110
+    let disableVideoInputSelect = !inputDeviceChangeSupported;
111
+    let selectedVideoInputId = settings.cameraDeviceId;
112
+
113
+    // audio input change will be a problem only when we are in a
114
+    // conference and this is not supported, when we open device selection on
115
+    // welcome page changing input devices will not be a problem
116
+    // on welcome page we also show only what we have saved as user selected devices
117
+    if (isDisplayedOnWelcomePage) {
118
+        disableVideoInputSelect = false;
119
+        selectedVideoInputId = userSelectedCamera;
120
+    }
121
+
122
+    // we fill the device selection dialog with the devices that are currently
123
+    // used or if none are currently used with what we have in settings(user selected)
124
+    return {
125
+        currentFramerate: framerate,
126
+        desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
127
+        disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
128
+        disableVideoInputSelect,
129
+        hasVideoPermission: permissions.video,
130
+        hideAdditionalSettings,
131
+        hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews,
132
+        localFlipX: Boolean(localFlipX),
86 133
         selectedVideoInputId
87 134
     };
88 135
 }

+ 0
- 1
react/features/screen-share/actions.native.ts ファイルの表示

@@ -1 +0,0 @@
1
-export * from './actions.any';

+ 0
- 7
react/features/settings/actions.ts ファイルの表示

@@ -11,7 +11,6 @@ import {
11 11
 import { openDialog } from '../base/dialog/actions';
12 12
 import i18next from '../base/i18n/i18next';
13 13
 import { updateSettings } from '../base/settings/actions';
14
-import { setScreenshareFramerate } from '../screen-share/actions';
15 14
 
16 15
 import {
17 16
     SET_AUDIO_SETTINGS_VISIBILITY,
@@ -99,12 +98,6 @@ export function submitMoreTab(newState: any) {
99 98
             }));
100 99
         }
101 100
 
102
-        if (newState.currentFramerate !== currentState.currentFramerate) {
103
-            const frameRate = parseInt(newState.currentFramerate, 10);
104
-
105
-            dispatch(setScreenshareFramerate(frameRate));
106
-        }
107
-
108 101
         if (newState.maxStageParticipants !== currentState.maxStageParticipants) {
109 102
             dispatch(updateSettings({ maxStageParticipants: Number(newState.maxStageParticipants) }));
110 103
         }

+ 0
- 58
react/features/settings/components/web/MoreTab.tsx ファイルの表示

@@ -8,23 +8,12 @@ import { translate } from '../../../base/i18n/functions';
8 8
 import Checkbox from '../../../base/ui/components/web/Checkbox';
9 9
 import Select from '../../../base/ui/components/web/Select';
10 10
 import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip/constants';
11
-import { SS_DEFAULT_FRAME_RATE } from '../../constants';
12 11
 
13 12
 /**
14 13
  * The type of the React {@code Component} props of {@link MoreTab}.
15 14
  */
16 15
 export type Props = AbstractDialogTabProps & WithTranslation & {
17 16
 
18
-    /**
19
-     * The currently selected desktop share frame rate in the frame rate select dropdown.
20
-     */
21
-    currentFramerate: string;
22
-
23
-    /**
24
-     * All available desktop capture frame rates.
25
-     */
26
-    desktopShareFramerates: Array<number>;
27
-
28 17
     /**
29 18
      * Whether or not follow me is currently active (enabled by some other participant).
30 19
      */
@@ -77,7 +66,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
77 66
         super(props);
78 67
 
79 68
         // Bind event handler so it is only bound once for every instance.
80
-        this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
81 69
         this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
82 70
         this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this);
83 71
         this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this);
@@ -104,19 +92,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
104 92
         );
105 93
     }
106 94
 
107
-    /**
108
-     * Callback invoked to select a frame rate from the select dropdown.
109
-     *
110
-     * @param {Object} e - The key event to handle.
111
-     * @private
112
-     * @returns {void}
113
-     */
114
-    _onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
115
-        const frameRate = e.target.value;
116
-
117
-        super._onChange({ currentFramerate: frameRate });
118
-    }
119
-
120 95
     /**
121 96
      * Callback invoked to select if the lobby
122 97
      * should be shown.
@@ -142,38 +117,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
142 117
         super._onChange({ maxStageParticipants: maxParticipants });
143 118
     }
144 119
 
145
-    /**
146
-     * Returns the React Element for the desktop share frame rate dropdown.
147
-     *
148
-     * @returns {ReactElement}
149
-     */
150
-    _renderFramerateSelect() {
151
-        const { currentFramerate, desktopShareFramerates, t } = this.props;
152
-        const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
153
-            return {
154
-                value: frameRate,
155
-                label: `${frameRate} ${t('settings.framesPerSecond')}`
156
-            };
157
-        });
158
-
159
-        return (
160
-            <div
161
-                className = 'settings-sub-pane-element'
162
-                key = 'frameRate'>
163
-                <div className = 'dropdown-menu'>
164
-                    <Select
165
-                        bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
166
-                            ? t('settings.desktopShareHighFpsWarning')
167
-                            : t('settings.desktopShareWarning') }
168
-                        label = { t('settings.desktopShareFramerate') }
169
-                        onChange = { this._onFramerateItemSelect }
170
-                        options = { frameRateItems }
171
-                        value = { currentFramerate } />
172
-                </div>
173
-            </div>
174
-        );
175
-    }
176
-
177 120
     /**
178 121
      * Returns the React Element for modifying prejoin screen settings.
179 122
      *
@@ -244,7 +187,6 @@ class MoreTab extends AbstractDialogTab<Props, any> {
244 187
             <div
245 188
                 className = 'settings-sub-pane right'
246 189
                 key = 'settings-sub-pane-right'>
247
-                { this._renderFramerateSelect() }
248 190
                 { this._renderMaxStageParticipantsSelect() }
249 191
             </div>
250 192
         );

+ 1
- 1
react/features/settings/components/web/SettingsButton.js ファイルの表示

@@ -46,7 +46,7 @@ class SettingsButton extends AbstractButton<Props, *> {
46 46
      * @returns {void}
47 47
      */
48 48
     _handleClick() {
49
-        const { defaultTab = SETTINGS_TABS.DEVICES, dispatch, isDisplayedOnWelcomePage = false } = this.props;
49
+        const { defaultTab = SETTINGS_TABS.AUDIO, dispatch, isDisplayedOnWelcomePage = false } = this.props;
50 50
 
51 51
         sendAnalytics(createToolbarEvent('settings'));
52 52
         dispatch(openSettingsDialog(defaultTab, isDisplayedOnWelcomePage));

+ 44
- 30
react/features/settings/components/web/SettingsDialog.tsx ファイルの表示

@@ -1,4 +1,3 @@
1
-/* eslint-disable lines-around-comment */
2 1
 import { Theme } from '@mui/material';
3 2
 import { withStyles } from '@mui/styles';
4 3
 import React, { Component } from 'react';
@@ -11,18 +10,20 @@ import {
11 10
     IconHost,
12 11
     IconShortcuts,
13 12
     IconUser,
13
+    IconVideo,
14 14
     IconVolumeUp
15 15
 } from '../../../base/icons/svg';
16 16
 import { connect } from '../../../base/redux/functions';
17 17
 import { withPixelLineHeight } from '../../../base/styles/functions.web';
18 18
 import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs';
19 19
 import { isCalendarEnabled } from '../../../calendar-sync/functions.web';
20
+import { submitAudioDeviceSelectionTab, submitVideoDeviceSelectionTab } from '../../../device-selection/actions.web';
21
+import AudioDevicesSelection from '../../../device-selection/components/AudioDevicesSelection';
22
+import VideoDeviceSelection from '../../../device-selection/components/VideoDeviceSelection';
20 23
 import {
21
-    DeviceSelection,
22
-    getDeviceSelectionDialogProps,
23
-    submitDeviceSelectionTab
24
-    // @ts-ignore
25
-} from '../../../device-selection';
24
+    getAudioDeviceSelectionDialogProps,
25
+    getVideoDeviceSelectionDialogProps
26
+} from '../../../device-selection/functions.web';
26 27
 import {
27 28
     submitModeratorTab,
28 29
     submitMoreTab,
@@ -47,7 +48,6 @@ import MoreTab from './MoreTab';
47 48
 import NotificationsTab from './NotificationsTab';
48 49
 import ProfileTab from './ProfileTab';
49 50
 import ShortcutsTab from './ShortcutsTab';
50
-/* eslint-enable lines-around-comment */
51 51
 
52 52
 /**
53 53
  * The type of the React {@code Component} props of
@@ -58,7 +58,7 @@ interface IProps {
58 58
     /**
59 59
      * Information about the tabs to be rendered.
60 60
      */
61
-    _tabs: IDialogTab[];
61
+    _tabs: IDialogTab<any>[];
62 62
 
63 63
     /**
64 64
      * An object containing the CSS classes.
@@ -100,10 +100,6 @@ const styles = (theme: Theme) => {
100 100
                 marginBottom: theme.spacing(1)
101 101
             },
102 102
 
103
-            '& .calendar-tab, & .device-selection': {
104
-                marginTop: '20px'
105
-            },
106
-
107 103
             '& .mock-atlaskit-label': {
108 104
                 color: '#b8c7e0',
109 105
                 fontSize: '12px',
@@ -168,7 +164,8 @@ const styles = (theme: Theme) => {
168 164
                 flexDirection: 'column',
169 165
                 fontSize: '14px',
170 166
                 minHeight: '100px',
171
-                textAlign: 'center'
167
+                textAlign: 'center',
168
+                marginTop: '20px'
172 169
             },
173 170
 
174 171
             '& .calendar-tab-sign-in': {
@@ -185,11 +182,6 @@ const styles = (theme: Theme) => {
185 182
             },
186 183
 
187 184
             '@media only screen and (max-width: 700px)': {
188
-                '& .device-selection': {
189
-                    display: 'flex',
190
-                    flexDirection: 'column'
191
-                },
192
-
193 185
                 '& .more-tab': {
194 186
                     flexDirection: 'column'
195 187
                 }
@@ -262,15 +254,15 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
262 254
     const showSoundsSettings = configuredTabs.includes('sounds');
263 255
     const enabledNotifications = getNotificationsMap(state);
264 256
     const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
265
-    const tabs: IDialogTab[] = [];
257
+    const tabs: IDialogTab<any>[] = [];
266 258
 
267 259
     if (showDeviceSettings) {
268 260
         tabs.push({
269
-            name: SETTINGS_TABS.DEVICES,
270
-            component: DeviceSelection,
271
-            labelKey: 'settings.devices',
272
-            props: getDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
273
-            propsUpdateFunction: (tabState: any, newProps: any) => {
261
+            name: SETTINGS_TABS.AUDIO,
262
+            component: AudioDevicesSelection,
263
+            labelKey: 'settings.audio',
264
+            props: getAudioDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
265
+            propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getAudioDeviceSelectionDialogProps>) => {
274 266
                 // Ensure the device selection tab gets updated when new devices
275 267
                 // are found by taking the new props and only preserving the
276 268
                 // current user selected devices. If this were not done, the
@@ -279,15 +271,38 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
279 271
 
280 272
                 return {
281 273
                     ...newProps,
274
+                    noiseSuppressionEnabled: tabState.noiseSuppressionEnabled,
282 275
                     selectedAudioInputId: tabState.selectedAudioInputId,
283
-                    selectedAudioOutputId: tabState.selectedAudioOutputId,
284
-                    selectedVideoInputId: tabState.selectedVideoInputId
276
+                    selectedAudioOutputId: tabState.selectedAudioOutputId
285 277
                 };
286 278
             },
287 279
             className: `settings-pane ${classes.settingsDialog} devices-pane`,
288
-            submit: (newState: any) => submitDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
280
+            submit: (newState: any) => submitAudioDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
289 281
             icon: IconVolumeUp
290 282
         });
283
+        tabs.push({
284
+            name: SETTINGS_TABS.VIDEO,
285
+            component: VideoDeviceSelection,
286
+            labelKey: 'settings.video',
287
+            props: getVideoDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
288
+            propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getVideoDeviceSelectionDialogProps>) => {
289
+                // Ensure the device selection tab gets updated when new devices
290
+                // are found by taking the new props and only preserving the
291
+                // current user selected devices. If this were not done, the
292
+                // tab would keep using a copy of the initial props it received,
293
+                // leaving the device list to become stale.
294
+
295
+                return {
296
+                    ...newProps,
297
+                    currentFramerate: tabState?.currentFramerate,
298
+                    localFlipX: tabState.localFlipX,
299
+                    selectedVideoInputId: tabState.selectedVideoInputId
300
+                };
301
+            },
302
+            className: `settings-pane ${classes.settingsDialog} devices-pane`,
303
+            submit: (newState: any) => submitVideoDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
304
+            icon: IconVideo
305
+        });
291 306
     }
292 307
 
293 308
     if (showSoundsSettings || showNotificationsSettings) {
@@ -314,7 +329,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
314 329
             component: ModeratorTab,
315 330
             labelKey: 'settings.moderator',
316 331
             props: moderatorTabProps,
317
-            propsUpdateFunction: (tabState: any, newProps: any) => {
332
+            propsUpdateFunction: (tabState: any, newProps: typeof moderatorTabProps) => {
318 333
                 // Updates tab props, keeping users selection
319 334
 
320 335
                 return {
@@ -379,12 +394,11 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
379 394
             component: MoreTab,
380 395
             labelKey: 'settings.more',
381 396
             props: moreTabProps,
382
-            propsUpdateFunction: (tabState: any, newProps: any) => {
397
+            propsUpdateFunction: (tabState: any, newProps: typeof moreTabProps) => {
383 398
                 // Updates tab props, keeping users selection
384 399
 
385 400
                 return {
386 401
                     ...newProps,
387
-                    currentFramerate: tabState?.currentFramerate,
388 402
                     currentLanguage: tabState?.currentLanguage,
389 403
                     hideSelfView: tabState?.hideSelfView,
390 404
                     showPrejoinPage: tabState?.showPrejoinPage,

+ 3
- 2
react/features/settings/constants.ts ファイルの表示

@@ -1,11 +1,12 @@
1 1
 export const SETTINGS_TABS = {
2
+    AUDIO: 'audio_tab',
2 3
     CALENDAR: 'calendar_tab',
3
-    DEVICES: 'devices_tab',
4 4
     MORE: 'more_tab',
5 5
     MODERATOR: 'moderator-tab',
6 6
     NOTIFICATIONS: 'notifications_tab',
7 7
     PROFILE: 'profile_tab',
8
-    SHORTCUTS: 'shortcuts_tab'
8
+    SHORTCUTS: 'shortcuts_tab',
9
+    VIDEO: 'video_tab'
9 10
 };
10 11
 
11 12
 /**

+ 0
- 5
react/features/settings/functions.any.ts ファイルの表示

@@ -19,8 +19,6 @@ import { getParticipantsPaneConfig } from '../participants-pane/functions';
19 19
 import { isPrejoinPageVisible } from '../prejoin/functions';
20 20
 import { isReactionsEnabled } from '../reactions/functions.any';
21 21
 
22
-import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
23
-
24 22
 /**
25 23
  * Used for web. Indicates if the setting section is enabled.
26 24
  *
@@ -118,12 +116,9 @@ export function getNotificationsMap(stateful: IStateful) {
118 116
  */
119 117
 export function getMoreTabProps(stateful: IStateful) {
120 118
     const state = toState(stateful);
121
-    const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
122 119
     const stageFilmstripEnabled = isStageFilmstripEnabled(state);
123 120
 
124 121
     return {
125
-        currentFramerate: framerate,
126
-        desktopShareFramerates: SS_SUPPORTED_FRAMERATES,
127 122
         showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
128 123
         showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled,
129 124
         maxStageParticipants: state['features/base/settings'].maxStageParticipants,

読み込み中…
キャンセル
保存