瀏覽代碼

Merge pull request #1447 from virtuacoplenny/device-picker

New device selection modal
j8
yanas 8 年之前
父節點
當前提交
98004c2328

+ 22
- 4
conference.js 查看文件

@@ -30,6 +30,9 @@ import {
30 30
     conferenceLeft,
31 31
     EMAIL_COMMAND
32 32
 } from './react/features/base/conference';
33
+import {
34
+    updateDeviceList
35
+} from './react/features/base/devices';
33 36
 import {
34 37
     isFatalJitsiConnectionError
35 38
 } from './react/features/base/lib-jitsi-meet';
@@ -1029,6 +1032,15 @@ export default {
1029 1032
             });
1030 1033
     },
1031 1034
 
1035
+    /**
1036
+     * Returns the current local video track in use.
1037
+     *
1038
+     * @returns {JitsiLocalTrack}
1039
+     */
1040
+    getLocalVideoTrack() {
1041
+        return room.getLocalVideoTrack();
1042
+    },
1043
+
1032 1044
     /**
1033 1045
      * Start using provided audio stream.
1034 1046
      * Stops previous audio stream.
@@ -1058,6 +1070,15 @@ export default {
1058 1070
             });
1059 1071
     },
1060 1072
 
1073
+    /**
1074
+     * Returns the current local audio track in use.
1075
+     *
1076
+     * @returns {JitsiLocalTrack}
1077
+     */
1078
+    getLocalAudioTrack() {
1079
+        return room.getLocalAudioTrack();
1080
+    },
1081
+
1061 1082
     videoSwitchInProgress: false,
1062 1083
     toggleScreenSharing (shareScreen = !this.isSharingScreen) {
1063 1084
         if (this.videoSwitchInProgress) {
@@ -1622,7 +1643,6 @@ export default {
1622 1643
                 })
1623 1644
                 .catch((err) => {
1624 1645
                     APP.UI.showDeviceErrorDialog(null, err);
1625
-                    APP.UI.setSelectedCameraFromSettings();
1626 1646
                 });
1627 1647
             }
1628 1648
         );
@@ -1644,7 +1664,6 @@ export default {
1644 1664
                 })
1645 1665
                 .catch((err) => {
1646 1666
                     APP.UI.showDeviceErrorDialog(err, null);
1647
-                    APP.UI.setSelectedMicFromSettings();
1648 1667
                 });
1649 1668
             }
1650 1669
         );
@@ -1660,7 +1679,6 @@ export default {
1660 1679
                         logger.warn('Failed to change audio output device. ' +
1661 1680
                             'Default or previously set audio output device ' +
1662 1681
                             'will be used instead.', err);
1663
-                        APP.UI.setSelectedAudioOutputFromSettings();
1664 1682
                     });
1665 1683
             }
1666 1684
         );
@@ -1756,8 +1774,8 @@ export default {
1756 1774
                 }
1757 1775
 
1758 1776
                 mediaDeviceHelper.setCurrentMediaDevices(devices);
1759
-
1760 1777
                 APP.UI.onAvailableDevicesChanged(devices);
1778
+                APP.store.dispatch(updateDeviceList(devices));
1761 1779
             });
1762 1780
 
1763 1781
             this.deviceChangeListener = (devices) =>

+ 0
- 31
css/_device_settings_dialog.scss 查看文件

@@ -1,31 +0,0 @@
1
-.settingsContent {
2
-    display: flex;
3
-    display: -webkit-flex;
4
-
5
-    #localVideoPreview {
6
-        width: 50%;
7
-        align-self: baseline;
8
-    }
9
-
10
-    .deviceSelection {
11
-        display: flex;
12
-        display: -webkit-flex;
13
-        -webkit-flex: 1;
14
-        flex: 1;
15
-        flex-direction: column;
16
-        flex-wrap: nowrap;
17
-        justify-content: flex-start;
18
-        align-items: left;
19
-        margin-left: 10px;
20
-
21
-        .device {
22
-            display: flex;
23
-            margin-bottom: 5px;
24
-
25
-            select {
26
-                flex: 1;
27
-                margin_right: 5px;
28
-            }
29
-        }
30
-    }
31
-}

+ 6
- 0
css/_side_toolbar_container.scss 查看文件

@@ -113,6 +113,12 @@
113 113
     text-align: center;
114 114
 }
115 115
 
116
+#deviceOptionsWrapper {
117
+    button {
118
+        float: none;
119
+    }
120
+}
121
+
116 122
 /**
117 123
  * Profile
118 124
  */

+ 1
- 1
css/main.scss 查看文件

@@ -38,6 +38,7 @@
38 38
 @import 'inlay';
39 39
 @import 'reload_overlay/reload_overlay';
40 40
 @import 'modals/desktop-picker/desktop-picker';
41
+@import 'modals/device-selection/device-selection';
41 42
 @import 'modals/dialog';
42 43
 @import 'modals/feedback/feedback';
43 44
 @import 'modals/speaker_stats/speaker_stats';
@@ -54,7 +55,6 @@
54 55
 @import 'welcome_page';
55 56
 @import 'toolbars';
56 57
 @import 'side_toolbar_container';
57
-@import 'device_settings_dialog';
58 58
 @import 'jquery.contextMenu';
59 59
 @import 'keyboard-shortcuts';
60 60
 @import 'redirect_page';

+ 84
- 0
css/modals/device-selection/_device-selection.scss 查看文件

@@ -0,0 +1,84 @@
1
+.device-selection {
2
+    color: $feedbackInputTextColor;
3
+
4
+    .device-selectors {
5
+        font-size: 14px;
6
+
7
+        > div {
8
+            margin-bottom: 10px;
9
+        }
10
+
11
+        > div:last-child {
12
+            margin-bottom: 5px;
13
+        }
14
+    }
15
+
16
+    .device-selection-column-selectors,
17
+    .device-selection-column-video {
18
+        padding: 10px;
19
+        display: inline-block;
20
+        vertical-align: top;
21
+    }
22
+    .device-selection-column-selectors {
23
+        width: 46%;
24
+    }
25
+    .device-selection-column-video {
26
+        width: 49%;
27
+        padding: 10px 0;
28
+    }
29
+
30
+    .device-selection-video-container {
31
+        background: black;
32
+        height: 156px;
33
+        margin: 15px 0 5px;
34
+
35
+        .video-input-preview {
36
+            position: relative;
37
+
38
+            .video-input-preview-muted {
39
+                color: $participantNameColor;
40
+                display: none;
41
+                left: 0;
42
+                position: absolute;
43
+                right: 0;
44
+                text-align: center;
45
+                top: 50%;
46
+            }
47
+
48
+            &.video-muted .video-input-preview-muted {
49
+                display: block;
50
+            }
51
+
52
+            .video-input-preview-display {
53
+                height: 100%;
54
+                overflow: hidden;
55
+                width: 100%;
56
+            }
57
+        }
58
+    }
59
+
60
+    .audio-output-preview {
61
+        text-align: right;
62
+
63
+        a {
64
+            cursor: pointer;
65
+            text-decoration: none;
66
+        }
67
+    }
68
+
69
+    .audio-input-preview {
70
+        background: #f4f5f7;
71
+        border-radius: 5px;
72
+        height: 6px;
73
+
74
+        .audio-input-preview-level {
75
+            background: #0052cc;
76
+            border-radius: 5px;
77
+            height: 100%;
78
+            -webkit-transition: width .1s ease-in-out;
79
+            -moz-transition: width .1s ease-in-out;
80
+            -o-transition: width .1s ease-in-out;
81
+            transition: width .1s ease-in-out;
82
+        }
83
+    }
84
+}

+ 6
- 0
lang/main.json 查看文件

@@ -422,5 +422,11 @@
422 422
         "seconds": "__count__s",
423 423
         "speakerStats": "Speaker Stats",
424 424
         "speakerTime": "Speaker Time"
425
+    },
426
+    "deviceSelection": {
427
+        "deviceSettings": "Device settings",
428
+        "noOtherDevices": "No other devices available",
429
+        "selectADevice": "Select a device",
430
+        "testAudio": "Test sound"
425 431
     }
426 432
 }

+ 5
- 23
modules/UI/UI.js 查看文件

@@ -4,6 +4,10 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
4 4
 
5 5
 var UI = {};
6 6
 
7
+import {
8
+    updateDeviceList
9
+} from '../../react/features/base/devices';
10
+
7 11
 import Chat from "./side_pannels/chat/Chat";
8 12
 import SidePanels from "./side_pannels/SidePanels";
9 13
 import Avatar from "./avatar/Avatar";
@@ -1080,29 +1084,7 @@ UI.onLocalRaiseHandChanged = function (isRaisedHand) {
1080 1084
  * @param {object[]} devices new list of available devices
1081 1085
  */
1082 1086
 UI.onAvailableDevicesChanged = function (devices) {
1083
-    SettingsMenu.changeDevicesList(devices);
1084
-};
1085
-
1086
-/**
1087
- * Sets microphone's <select> element to select microphone ID from settings.
1088
- */
1089
-UI.setSelectedMicFromSettings = function () {
1090
-    SettingsMenu.setSelectedMicFromSettings();
1091
-};
1092
-
1093
-/**
1094
- * Sets camera's <select> element to select camera ID from settings.
1095
- */
1096
-UI.setSelectedCameraFromSettings = function () {
1097
-    SettingsMenu.setSelectedCameraFromSettings();
1098
-};
1099
-
1100
-/**
1101
- * Sets audio outputs's <select> element to select audio output ID from
1102
- * settings.
1103
- */
1104
-UI.setSelectedAudioOutputFromSettings = function () {
1105
-    SettingsMenu.setSelectedAudioOutputFromSettings();
1087
+    APP.store.dispatch(updateDeviceList(devices));
1106 1088
 };
1107 1089
 
1108 1090
 /**

+ 42
- 161
modules/UI/side_pannels/settings/SettingsMenu.js 查看文件

@@ -1,12 +1,15 @@
1 1
 /* global $, APP, AJS, interfaceConfig, JitsiMeetJS */
2
-
2
+import { openDialog } from '../../../../react/features/base/dialog';
3 3
 import { LANGUAGES } from "../../../../react/features/base/i18n";
4
+import { DeviceSelectionDialog }
5
+    from '../../../../react/features/device-selection';
4 6
 
5 7
 import UIUtil from "../../util/UIUtil";
6 8
 import UIEvents from "../../../../service/UI/UIEvents";
7
-import Settings from '../../../settings/Settings';
8 9
 
9 10
 const sidePanelsContainerId = 'sideToolbarContainer';
11
+const deviceSelectionButtonClasses
12
+    = 'button-control button-control_primary button-control_full-width';
10 13
 const htmlStr = `
11 14
     <div id="settings_container" class="sideToolbarContainer__inner">
12 15
         <div class="title" data-i18n="settings.title"></div>
@@ -19,17 +22,11 @@ const htmlStr = `
19 22
                 <div id="deviceOptionsTitle" class="subTitle hide" 
20 23
                     data-i18n="settings.audioVideo"></div>
21 24
                 <div class="sideToolbarBlock first">
22
-                    <label class="first" data-i18n="settings.selectCamera">
23
-                    </label>
24
-                    <select id="selectCamera"></select>
25
-                </div>
26
-                <div class="sideToolbarBlock">
27
-                    <label data-i18n="settings.selectMic"></label>
28
-                    <select id="selectMic"></select>
29
-                </div>
30
-                <div class="sideToolbarBlock">
31
-                    <label data-i18n="settings.selectAudioOutput"></label>
32
-                    <select id="selectAudioOutput"></select>
25
+                    <button
26
+                        class="${deviceSelectionButtonClasses}"
27
+                        data-i18n="deviceSelection.deviceSettings"
28
+                        id="deviceSelection"
29
+                        type="button"></button>
33 30
                 </div>
34 31
             </div>
35 32
             <div id="moderatorOptionsWrapper" class="hide">
@@ -89,40 +86,6 @@ function generateLanguagesOptions(items, currentLang) {
89 86
     }).join('');
90 87
 }
91 88
 
92
-/**
93
- * Generate html select options for available physical devices.
94
- *
95
- * @param {{ deviceId, label }[]} items available devices
96
- * @param {string} [selectedId] id of selected device
97
- * @param {boolean} permissionGranted if permission to use selected device type
98
- *      is granted
99
- * @returns {string}
100
- */
101
-function generateDevicesOptions(items, selectedId, permissionGranted) {
102
-    if (!permissionGranted && items.length) {
103
-        return '<option data-i18n="settings.noPermission"></option>';
104
-    }
105
-
106
-    var options = items.map(function (item) {
107
-        let attrs = {
108
-            value: item.deviceId
109
-        };
110
-
111
-        if (item.deviceId === selectedId) {
112
-            attrs.selected = 'selected';
113
-        }
114
-
115
-        let attrsStr = UIUtil.attrsToString(attrs);
116
-        return `<option ${attrsStr}>${item.label}</option>`;
117
-    });
118
-
119
-    if (!items.length) {
120
-        options.unshift('<option data-i18n="settings.noDevice"></option>');
121
-    }
122
-
123
-    return options.join('');
124
-}
125
-
126 89
 /**
127 90
  * Replace html select element to select2 custom dropdown
128 91
  *
@@ -138,6 +101,34 @@ function initSelect2($el, onSelectedCb) {
138 101
     }
139 102
 }
140 103
 
104
+/**
105
+ * Open DeviceSelectionDialog with a configuration based on the environment's
106
+ * supported abilities.
107
+ *
108
+ * @param {boolean} isDeviceListAvailable - Whether or not device enumeration
109
+ * is possible. This is a value obtained through an async operation whereas all
110
+ * other configurations for the modal are obtained synchronously.
111
+ * @private
112
+ * @returns {void}
113
+ */
114
+function _openDeviceSelectionModal(isDeviceListAvailable) {
115
+    APP.store.dispatch(openDialog(DeviceSelectionDialog, {
116
+        currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
117
+        currentAudioTrack: APP.conference.getLocalAudioTrack(),
118
+        currentVideoTrack: APP.conference.getLocalVideoTrack(),
119
+        disableAudioInputChange: !JitsiMeetJS.isMultipleAudioInputSupported(),
120
+        disableDeviceChange: !isDeviceListAvailable
121
+            || !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
122
+        hasAudioPermission: JitsiMeetJS.mediaDevices
123
+            .isDevicePermissionGranted('audio'),
124
+        hasVideoPermission: JitsiMeetJS.mediaDevices
125
+            .isDevicePermissionGranted('video'),
126
+        hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
127
+        hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
128
+            .isDeviceChangeAvailable('output')
129
+    }));
130
+}
131
+
141 132
 export default {
142 133
     init (emitter) {
143 134
         initHTML();
@@ -181,11 +172,11 @@ export default {
181 172
 
182 173
             JitsiMeetJS.mediaDevices.isDeviceListAvailable()
183 174
                 .then((isDeviceListAvailable) => {
184
-                    if (isDeviceListAvailable &&
185
-                        JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) {
186
-                        this._initializeDeviceSelectionSettings(emitter);
187
-                    }
175
+                    $('#deviceSelection').on('click', () => {
176
+                        _openDeviceSelectionModal(isDeviceListAvailable);
177
+                    });
188 178
                 });
179
+
189 180
             // Only show the subtitle if this isn't the only setting section.
190 181
             if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
191 182
                 UIUtil.setVisible("deviceOptionsTitle", true);
@@ -219,30 +210,6 @@ export default {
219 210
         }
220 211
     },
221 212
 
222
-    _initializeDeviceSelectionSettings(emitter) {
223
-        this.changeDevicesList([]);
224
-
225
-        $('#selectCamera').change(function () {
226
-            let cameraDeviceId = $(this).val();
227
-            if (cameraDeviceId !== Settings.getCameraDeviceId()) {
228
-                emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId);
229
-            }
230
-        });
231
-        $('#selectMic').change(function () {
232
-            let micDeviceId = $(this).val();
233
-            if (micDeviceId !== Settings.getMicDeviceId()) {
234
-                emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId);
235
-            }
236
-        });
237
-        $('#selectAudioOutput').change(function () {
238
-            let audioOutputDeviceId = $(this).val();
239
-            if (audioOutputDeviceId !== Settings.getAudioOutputDeviceId()) {
240
-                emitter.emit(
241
-                    UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, audioOutputDeviceId);
242
-            }
243
-        });
244
-    },
245
-
246 213
     /**
247 214
      * If start audio muted/start video muted options should be visible or not.
248 215
      * @param {boolean} show
@@ -286,91 +253,5 @@ export default {
286 253
      */
287 254
     isVisible () {
288 255
         return UIUtil.isVisible(document.getElementById("settings_container"));
289
-    },
290
-
291
-    /**
292
-     * Sets microphone's <select> element to select microphone ID from settings.
293
-     */
294
-    setSelectedMicFromSettings () {
295
-        $('#selectMic').val(Settings.getMicDeviceId());
296
-    },
297
-
298
-    /**
299
-     * Sets camera's <select> element to select camera ID from settings.
300
-     */
301
-    setSelectedCameraFromSettings () {
302
-        $('#selectCamera').val(Settings.getCameraDeviceId());
303
-    },
304
-
305
-    /**
306
-     * Sets audio outputs's <select> element to select audio output ID from
307
-     * settings.
308
-     */
309
-    setSelectedAudioOutputFromSettings () {
310
-        $('#selectAudioOutput').val(Settings.getAudioOutputDeviceId());
311
-    },
312
-
313
-    /**
314
-     * Change available cameras/microphones or hide selects completely if
315
-     * no devices available.
316
-     * @param {{ deviceId, label, kind }[]} devices list of available devices
317
-     */
318
-    changeDevicesList (devices) {
319
-        let $selectCamera= AJS.$('#selectCamera'),
320
-            $selectMic = AJS.$('#selectMic'),
321
-            $selectAudioOutput = AJS.$('#selectAudioOutput'),
322
-            $selectAudioOutputParent = $selectAudioOutput.parent();
323
-
324
-        let audio = devices.filter(device => device.kind === 'audioinput'),
325
-            video = devices.filter(device => device.kind === 'videoinput'),
326
-            audioOutput = devices
327
-                .filter(device => device.kind === 'audiooutput'),
328
-            selectedAudioDevice = audio.find(
329
-                d => d.deviceId === Settings.getMicDeviceId()) || audio[0],
330
-            selectedVideoDevice = video.find(
331
-                d => d.deviceId === Settings.getCameraDeviceId()) || video[0],
332
-            selectedAudioOutputDevice = audioOutput.find(
333
-                    d => d.deviceId === Settings.getAudioOutputDeviceId()),
334
-            videoPermissionGranted =
335
-                JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video'),
336
-            audioPermissionGranted =
337
-                JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio');
338
-
339
-        $selectCamera
340
-            .html(generateDevicesOptions(
341
-                video,
342
-                selectedVideoDevice ? selectedVideoDevice.deviceId : '',
343
-                videoPermissionGranted))
344
-            .prop('disabled', !video.length || !videoPermissionGranted);
345
-
346
-        initSelect2($selectCamera);
347
-
348
-        $selectMic
349
-            .html(generateDevicesOptions(
350
-                audio,
351
-                selectedAudioDevice ? selectedAudioDevice.deviceId : '',
352
-                audioPermissionGranted))
353
-            .prop('disabled', !audio.length || !audioPermissionGranted);
354
-
355
-        initSelect2($selectMic);
356
-
357
-        if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
358
-            $selectAudioOutput
359
-                .html(generateDevicesOptions(
360
-                    audioOutput,
361
-                    selectedAudioOutputDevice
362
-                        ? selectedAudioOutputDevice.deviceId
363
-                        : 'default',
364
-                    videoPermissionGranted || audioPermissionGranted))
365
-                .prop('disabled', !audioOutput.length ||
366
-                    (!videoPermissionGranted && !audioPermissionGranted));
367
-            initSelect2($selectAudioOutput);
368
-
369
-            $selectAudioOutputParent.show();
370
-        } else {
371
-            $selectAudioOutputParent.hide();
372
-        }
373
-
374
-        APP.translation.translateElement($('#settings_container option'));
375 256
     }
376 257
 };

+ 1
- 0
package.json 查看文件

@@ -21,6 +21,7 @@
21 21
     "@atlaskit/button-group": "1.0.0",
22 22
     "@atlaskit/field-text": "2.0.3",
23 23
     "@atlaskit/modal-dialog": "1.2.4",
24
+    "@atlaskit/single-select": "1.6.1",
24 25
     "@atlaskit/tabs": "1.2.5",
25 26
     "async": "0.9.0",
26 27
     "autosize": "1.18.13",

+ 45
- 0
react/features/base/devices/actionTypes.js 查看文件

@@ -0,0 +1,45 @@
1
+import { Symbol } from '../react';
2
+
3
+/**
4
+ * The type of Redux action which signals that the currently used audio
5
+ * input device should be changed.
6
+ *
7
+ * {
8
+ *     type: SET_AUDIO_INPUT_DEVICE,
9
+ *     deviceId: string,
10
+ * }
11
+ */
12
+export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE');
13
+
14
+/**
15
+ * The type of Redux action which signals that the currently used audio
16
+ * output device should be changed.
17
+ *
18
+ * {
19
+ *     type: SET_AUDIO_OUTPUT_DEVICE,
20
+ *     deviceId: string,
21
+ * }
22
+ */
23
+export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE');
24
+
25
+/**
26
+ * The type of Redux action which signals that the currently used video
27
+ * input device should be changed.
28
+ *
29
+ * {
30
+ *     type: SET_VIDEO_INPUT_DEVICE,
31
+ *     deviceId: string,
32
+ * }
33
+ */
34
+export const SET_VIDEO_INPUT_DEVICE = Symbol('SET_VIDEO_INPUT_DEVICE');
35
+
36
+/**
37
+ * The type of Redux action which signals that the list of known available
38
+ * audio and video sources has changed.
39
+ *
40
+ * {
41
+ *     type: UPDATE_DEVICE_LIST,
42
+ *     devices: Array<MediaDeviceInfo>,
43
+ * }
44
+ */
45
+export const UPDATE_DEVICE_LIST = Symbol('UPDATE_DEVICE_LIST');

+ 71
- 0
react/features/base/devices/actions.js 查看文件

@@ -0,0 +1,71 @@
1
+import {
2
+    SET_AUDIO_INPUT_DEVICE,
3
+    SET_AUDIO_OUTPUT_DEVICE,
4
+    SET_VIDEO_INPUT_DEVICE,
5
+    UPDATE_DEVICE_LIST
6
+} from './actionTypes';
7
+
8
+/**
9
+ * Signals to update the currently used audio input device.
10
+ *
11
+ * @param {string} deviceId - The id of the new audio input device.
12
+ * @returns {{
13
+ *      type: SET_AUDIO_INPUT_DEVICE,
14
+ *      deviceId: string
15
+ * }}
16
+ */
17
+export function setAudioInputDevice(deviceId) {
18
+    return {
19
+        type: SET_AUDIO_INPUT_DEVICE,
20
+        deviceId
21
+    };
22
+}
23
+
24
+/**
25
+ * Signals to update the currently used audio output device.
26
+ *
27
+ * @param {string} deviceId - The id of the new audio ouput device.
28
+ * @returns {{
29
+ *      type: SET_AUDIO_OUTPUT_DEVICE,
30
+ *      deviceId: string
31
+ * }}
32
+ */
33
+export function setAudioOutputDevice(deviceId) {
34
+    return {
35
+        type: SET_AUDIO_OUTPUT_DEVICE,
36
+        deviceId
37
+    };
38
+}
39
+
40
+/**
41
+ * Signals to update the currently used video input device.
42
+ *
43
+ * @param {string} deviceId - The id of the new video input device.
44
+ * @returns {{
45
+ *      type: SET_VIDEO_INPUT_DEVICE,
46
+ *      deviceId: string
47
+ * }}
48
+ */
49
+export function setVideoInputDevice(deviceId) {
50
+    return {
51
+        type: SET_VIDEO_INPUT_DEVICE,
52
+        deviceId
53
+    };
54
+}
55
+
56
+/**
57
+ * Signals to update the list of known audio and video devices.
58
+ *
59
+ * @param {Array<MediaDeviceInfo>} devices - All known available audio input,
60
+ * audio output, and video input devices.
61
+ * @returns {{
62
+ *      type: UPDATE_DEVICE_LIST,
63
+ *      devices: Array<MediaDeviceInfo>
64
+ * }}
65
+ */
66
+export function updateDeviceList(devices) {
67
+    return {
68
+        type: UPDATE_DEVICE_LIST,
69
+        devices
70
+    };
71
+}

+ 5
- 0
react/features/base/devices/index.js 查看文件

@@ -0,0 +1,5 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+
4
+import './middleware';
5
+import './reducer';

+ 34
- 0
react/features/base/devices/middleware.js 查看文件

@@ -0,0 +1,34 @@
1
+/* global APP */
2
+
3
+import UIEvents from '../../../../service/UI/UIEvents';
4
+
5
+import { MiddlewareRegistry } from '../redux';
6
+
7
+import {
8
+    SET_AUDIO_INPUT_DEVICE,
9
+    SET_AUDIO_OUTPUT_DEVICE,
10
+    SET_VIDEO_INPUT_DEVICE
11
+} from './actionTypes';
12
+
13
+/**
14
+ * Implements the middleware of the feature base/devices.
15
+ *
16
+ * @param {Store} store - Redux store.
17
+ * @returns {Function}
18
+ */
19
+// eslint-disable-next-line no-unused-vars
20
+MiddlewareRegistry.register(store => next => action => {
21
+    switch (action.type) {
22
+    case SET_AUDIO_INPUT_DEVICE:
23
+        APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
24
+        break;
25
+    case SET_AUDIO_OUTPUT_DEVICE:
26
+        APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId);
27
+        break;
28
+    case SET_VIDEO_INPUT_DEVICE:
29
+        APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
30
+        break;
31
+    }
32
+
33
+    return next(action);
34
+});

+ 64
- 0
react/features/base/devices/reducer.js 查看文件

@@ -0,0 +1,64 @@
1
+import {
2
+    SET_AUDIO_INPUT_DEVICE,
3
+    SET_AUDIO_OUTPUT_DEVICE,
4
+    SET_VIDEO_INPUT_DEVICE,
5
+    UPDATE_DEVICE_LIST
6
+} from './actionTypes';
7
+
8
+import { ReducerRegistry } from '../redux';
9
+
10
+const DEFAULT_STATE = {
11
+    audioInput: [],
12
+    audioOutput: [],
13
+    videoInput: []
14
+};
15
+
16
+/**
17
+ * Listen for actions which changes the state of known and used devices.
18
+ *
19
+ * @param {Object} state - The Redux state of the feature features/base/devices.
20
+ * @param {Object} action - Action object.
21
+ * @param {string} action.type - Type of action.
22
+ * @param {Array<MediaDeviceInfo>} action.devices - All available audio and
23
+ * video devices.
24
+ * @returns {Object}
25
+ */
26
+ReducerRegistry.register(
27
+    'features/base/devices',
28
+    (state = DEFAULT_STATE, action) => {
29
+        switch (action.type) {
30
+        case UPDATE_DEVICE_LIST: {
31
+            const deviceList = _groupDevicesByKind(action.devices);
32
+
33
+            return {
34
+                ...deviceList
35
+            };
36
+        }
37
+
38
+        // TODO: Changing of current audio and video device id is currently
39
+        // handled outside of react/redux. Fall through to default logic for
40
+        // now.
41
+        case SET_AUDIO_INPUT_DEVICE:
42
+        case SET_VIDEO_INPUT_DEVICE:
43
+        case SET_AUDIO_OUTPUT_DEVICE:
44
+        default:
45
+            return state;
46
+        }
47
+    });
48
+
49
+/**
50
+ * Converts an array of media devices into an object organized by device kind.
51
+ *
52
+ * @param {Array<MediaDeviceInfo>} devices - Available media devices.
53
+ * @private
54
+ * @returns {Object} An object with the media devices split by type. The keys
55
+ * are device type and the values are arrays with devices matching the device
56
+ * type.
57
+ */
58
+function _groupDevicesByKind(devices) {
59
+    return {
60
+        audioInput: devices.filter(device => device.kind === 'audioinput'),
61
+        audioOutput: devices.filter(device => device.kind === 'audiooutput'),
62
+        videoInput: devices.filter(device => device.kind === 'videoinput')
63
+    };
64
+}

+ 21
- 0
react/features/base/lib-jitsi-meet/functions.js 查看文件

@@ -60,3 +60,24 @@ export function loadConfig(host: string, path: string = '/config.js') {
60 60
             throw err;
61 61
         });
62 62
 }
63
+
64
+/**
65
+ * Creates a JitsiLocalTrack model from the given device id.
66
+ *
67
+ * @param {string} type - The media type of track being created. Expected values
68
+ * are "video" or "audio".
69
+ * @param {string} deviceId - The id of the target media source.
70
+ * @returns {Promise<JitsiLocalTrack>}
71
+ */
72
+export function createLocalTrack(type, deviceId) {
73
+    return JitsiMeetJS
74
+        .createLocalTracks({
75
+            devices: [ type ],
76
+            micDeviceId: deviceId,
77
+            cameraDeviceId: deviceId,
78
+
79
+            // eslint-disable-next-line camelcase
80
+            firefox_fake_device: window.config
81
+                && window.config.firefox_fake_device
82
+        }).then(([ jitsiLocalTrack ]) => jitsiLocalTrack);
83
+}

+ 132
- 0
react/features/device-selection/components/AudioInputPreview.js 查看文件

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

+ 122
- 0
react/features/device-selection/components/AudioOutputPreview.js 查看文件

@@ -0,0 +1,122 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+const TEST_SOUND_PATH = 'sounds/ring.wav';
6
+
7
+/**
8
+ * React component for playing a test sound through a specified audio device.
9
+ *
10
+ * @extends Component
11
+ */
12
+class AudioOutputPreview extends Component {
13
+    /**
14
+     * AudioOutputPreview component's property types.
15
+     *
16
+     * @static
17
+     */
18
+    static propTypes = {
19
+        /**
20
+         * The device id of the audio output device to use.
21
+         */
22
+        deviceId: React.PropTypes.string,
23
+
24
+        /**
25
+         * Invoked to obtain translated strings.
26
+         */
27
+        t: React.PropTypes.func
28
+    }
29
+
30
+    /**
31
+     * Initializes a new AudioOutputPreview instance.
32
+     *
33
+     * @param {Object} props - The read-only React Component props with which
34
+     * the new instance is to be initialized.
35
+     */
36
+    constructor(props) {
37
+        super(props);
38
+
39
+        this._audioElement = null;
40
+
41
+        this._onClick = this._onClick.bind(this);
42
+        this._setAudioElement = this._setAudioElement.bind(this);
43
+    }
44
+
45
+    /**
46
+     * Sets the target output device on the component's audio element after
47
+     * initial render.
48
+     *
49
+     * @inheritdoc
50
+     * @returns {void}
51
+     */
52
+    componentDidMount() {
53
+        this._setAudioSink();
54
+    }
55
+
56
+    /**
57
+     * Updates the audio element when the target output device changes and the
58
+     * audio element has re-rendered.
59
+     *
60
+     * @inheritdoc
61
+     * @returns {void}
62
+     */
63
+    componentDidUpdate() {
64
+        this._setAudioSink();
65
+    }
66
+
67
+    /**
68
+     * Implements React's {@link Component#render()}.
69
+     *
70
+     * @inheritdoc
71
+     * @returns {ReactElement}
72
+     */
73
+    render() {
74
+        return (
75
+            <div className = 'audio-output-preview'>
76
+                <a onClick = { this._onClick }>
77
+                    { this.props.t('deviceSelection.testAudio') }
78
+                </a>
79
+                <audio
80
+                    preload = 'auto'
81
+                    ref = { this._setAudioElement }
82
+                    src = { TEST_SOUND_PATH } />
83
+            </div>
84
+        );
85
+    }
86
+
87
+    /**
88
+     * Plays a test sound.
89
+     *
90
+     * @private
91
+     * @returns {void}
92
+     */
93
+    _onClick() {
94
+        this._audioElement
95
+        && this._audioElement.play();
96
+    }
97
+
98
+    /**
99
+     * Sets the instance variable for the component's audio element so it can be
100
+     * accessed directly.
101
+     *
102
+     * @param {Object} element - The DOM element for the component's audio.
103
+     * @private
104
+     * @returns {void}
105
+     */
106
+    _setAudioElement(element) {
107
+        this._audioElement = element;
108
+    }
109
+
110
+    /**
111
+     * Updates the target output device for playing the test sound.
112
+     *
113
+     * @private
114
+     * @returns {void}
115
+     */
116
+    _setAudioSink() {
117
+        this._audioElement
118
+        && this._audioElement.setSinkId(this.props.deviceId);
119
+    }
120
+}
121
+
122
+export default translate(AudioOutputPreview);

+ 597
- 0
react/features/device-selection/components/DeviceSelectionDialog.js 查看文件

@@ -0,0 +1,597 @@
1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import {
5
+    setAudioInputDevice,
6
+    setAudioOutputDevice,
7
+    setVideoInputDevice
8
+} from '../../base/devices';
9
+import {
10
+    Dialog,
11
+    hideDialog
12
+} from '../../base/dialog';
13
+import { translate } from '../../base/i18n';
14
+import { createLocalTrack } from '../../base/lib-jitsi-meet';
15
+
16
+import AudioInputPreview from './AudioInputPreview';
17
+import AudioOutputPreview from './AudioOutputPreview';
18
+import DeviceSelector from './DeviceSelector';
19
+import VideoInputPreview from './VideoInputPreview';
20
+
21
+/**
22
+ * React component for previewing and selecting new audio and video sources.
23
+ *
24
+ * @extends Component
25
+ */
26
+class DeviceSelectionDialog extends Component {
27
+    /**
28
+     * DeviceSelectionDialog component's property types.
29
+     *
30
+     * @static
31
+     */
32
+    static propTypes = {
33
+       /**
34
+         * All known audio and video devices split by type. This prop comes from
35
+         * the app state.
36
+         */
37
+        _devices: React.PropTypes.object,
38
+
39
+        /**
40
+         * Device id for the current audio output device.
41
+         */
42
+        currentAudioOutputId: React.PropTypes.string,
43
+
44
+        /**
45
+         * JitsiLocalTrack for the current local audio.
46
+         *
47
+         * JitsiLocalTracks for the current audio and video, if any, should be
48
+         * passed in for re-use in the previews. This is needed for Internet
49
+         * Explorer, which cannot get multiple tracks from the same device, even
50
+         * across tabs.
51
+         */
52
+        currentAudioTrack: React.PropTypes.object,
53
+
54
+        /**
55
+         * JitsiLocalTrack for the current local video.
56
+         *
57
+         * Needed for reuse. See comment for propTypes.currentAudioTrack.
58
+         */
59
+        currentVideoTrack: React.PropTypes.object,
60
+
61
+        /**
62
+         * Whether or not the audio selector can be interacted with. If true,
63
+         * the audio input selector will be rendered as disabled. This is
64
+         * specifically used to prevent audio device changing in Firefox, which
65
+         * currently does not work due to a browser-side regression.
66
+         */
67
+        disableAudioInputChange: React.PropTypes.bool,
68
+
69
+        /**
70
+         * True if device changing is configured to be disallowed. Selectors
71
+         * will display as disabled.
72
+         */
73
+        disableDeviceChange: React.PropTypes.bool,
74
+
75
+        /**
76
+         * Invoked to notify the store of app state changes.
77
+         */
78
+        dispatch: React.PropTypes.func,
79
+
80
+        /**
81
+         * Whether or not new audio input source can be selected.
82
+         */
83
+        hasAudioPermission: React.PropTypes.bool,
84
+
85
+        /**
86
+         * Whether or not new video input sources can be selected.
87
+         */
88
+        hasVideoPermission: React.PropTypes.bool,
89
+
90
+        /**
91
+         * If true, the audio meter will not display. Necessary for browsers or
92
+         * configurations that do not support local stats to prevent a
93
+         * non-responsive mic preview from displaying.
94
+         */
95
+        hideAudioInputPreview: React.PropTypes.bool,
96
+
97
+        /**
98
+         * Whether or not the audio output source selector should display. If
99
+         * true, the audio output selector and test audio link will not be
100
+         * rendered. This is specifically used for hiding audio output on
101
+         * temasys browsers which do not support such change.
102
+         */
103
+        hideAudioOutputSelect: React.PropTypes.bool,
104
+
105
+        /**
106
+         * Invoked to obtain translated strings.
107
+         */
108
+        t: React.PropTypes.func
109
+    }
110
+
111
+    /**
112
+     * Initializes a new DeviceSelectionDialog instance.
113
+     *
114
+     * @param {Object} props - The read-only React Component props with which
115
+     * the new instance is to be initialized.
116
+     */
117
+    constructor(props) {
118
+        super(props);
119
+
120
+        this.state = {
121
+            // JitsiLocalTracks to use for live previewing.
122
+            previewAudioTrack: null,
123
+            previewVideoTrack: null,
124
+
125
+            // Device ids to keep track of new selections.
126
+            videInput: null,
127
+            audioInput: null,
128
+            audioOutput: null
129
+        };
130
+
131
+        // Preventing closing while cleaning up previews is important for
132
+        // supporting temasys video cleanup. Temasys requires its video object
133
+        // to be in the dom and visible for proper detaching of tracks. Delaying
134
+        // closure until cleanup is complete ensures no errors in the process.
135
+        this._isClosing = false;
136
+
137
+        this._closeModal = this._closeModal.bind(this);
138
+        this._getAndSetAudioOutput = this._getAndSetAudioOutput.bind(this);
139
+        this._getAndSetAudioTrack = this._getAndSetAudioTrack.bind(this);
140
+        this._getAndSetVideoTrack = this._getAndSetVideoTrack.bind(this);
141
+        this._onCancel = this._onCancel.bind(this);
142
+        this._onSubmit = this._onSubmit.bind(this);
143
+    }
144
+
145
+    /**
146
+     * Clean up any preview tracks that might not have been cleaned up already.
147
+     *
148
+     * @inheritdoc
149
+     */
150
+    componentWillUnmount() {
151
+        // This handles the case where neither submit nor cancel were triggered,
152
+        // such as on modal switch. In that case, make a dying attempt to clean
153
+        // up previews.
154
+        if (!this._isClosing) {
155
+            this._attemptPreviewTrackCleanup();
156
+        }
157
+    }
158
+
159
+    /**
160
+     * Implements React's {@link Component#render()}.
161
+     *
162
+     * @inheritdoc
163
+     */
164
+    render() {
165
+        return (
166
+            <Dialog
167
+                cancelTitleKey = { 'dialog.Cancel' }
168
+                okTitleKey = { 'dialog.Save' }
169
+                onCancel = { this._onCancel }
170
+                onSubmit = { this._onSubmit }
171
+                titleKey = 'deviceSelection.deviceSettings' >
172
+                <div className = 'device-selection'>
173
+                    <div className = 'device-selection-column-selectors'>
174
+                        <div className = 'device-selectors'>
175
+                            { this._renderSelectors() }
176
+                        </div>
177
+                        { this._renderAudioOutputPreview() }
178
+                    </div>
179
+                    <div className = 'device-selection-column-video'>
180
+                        <div className = 'device-selection-video-container'>
181
+                            <VideoInputPreview
182
+                                track = { this.state.previewVideoTrack
183
+                                    || this.props.currentVideoTrack } />
184
+                        </div>
185
+                        { this._renderAudioInputPreview() }
186
+                    </div>
187
+                </div>
188
+            </Dialog>
189
+        );
190
+    }
191
+
192
+    /**
193
+     * Cleans up preview tracks if they are not active tracks.
194
+     *
195
+     * @private
196
+     * @returns {Array<Promise>} Zero to two promises will be returned. One
197
+     * promise can be for video cleanup and another for audio cleanup.
198
+     */
199
+    _attemptPreviewTrackCleanup() {
200
+        const cleanupPromises = [];
201
+
202
+        if (!this._isPreviewingCurrentVideoTrack()) {
203
+            cleanupPromises.push(this._disposeVideoPreview());
204
+        }
205
+
206
+        if (!this._isPreviewingCurrentAudioTrack()) {
207
+            cleanupPromises.push(this._disposeAudioPreview());
208
+        }
209
+
210
+        return cleanupPromises;
211
+    }
212
+
213
+    /**
214
+     * Signals to close DeviceSelectionDialog.
215
+     *
216
+     * @private
217
+     * @returns {void}
218
+     */
219
+    _closeModal() {
220
+        this.props.dispatch(hideDialog());
221
+    }
222
+
223
+    /**
224
+     * Utility function for disposing the current audio preview.
225
+     *
226
+     * @private
227
+     * @returns {Promise}
228
+     */
229
+    _disposeAudioPreview() {
230
+        return this.state.previewAudioTrack
231
+            ? this.state.previewAudioTrack.dispose() : Promise.resolve();
232
+    }
233
+
234
+    /**
235
+     * Utility function for disposing the current video preview.
236
+     *
237
+     * @private
238
+     * @returns {Promise}
239
+     */
240
+    _disposeVideoPreview() {
241
+        return this.state.previewVideoTrack
242
+            ? this.state.previewVideoTrack.dispose() : Promise.resolve();
243
+    }
244
+
245
+    /**
246
+     * Callback invoked when a new audio output device has been selected.
247
+     * Updates the internal state of the user's selection.
248
+     *
249
+     * @param {string} deviceId - The id of the chosen audio output device.
250
+     * @private
251
+     * @returns {void}
252
+     */
253
+    _getAndSetAudioOutput(deviceId) {
254
+        this.setState({
255
+            audioOutput: deviceId
256
+        });
257
+    }
258
+
259
+    /**
260
+     * Callback invoked when a new audio input device has been selected.
261
+     * Updates the internal state of the user's selection as well as the audio
262
+     * track that should display in the preview. Will reuse the current local
263
+     * audio track if it has been selected.
264
+     *
265
+     * @param {string} deviceId - The id of the chosen audio input device.
266
+     * @private
267
+     * @returns {void}
268
+     */
269
+    _getAndSetAudioTrack(deviceId) {
270
+        this.setState({
271
+            audioInput: deviceId
272
+        }, () => {
273
+            const cleanupPromise = this._isPreviewingCurrentAudioTrack()
274
+                ? Promise.resolve() : this._disposeAudioPreview();
275
+
276
+            if (this._isCurrentAudioTrack(deviceId)) {
277
+                cleanupPromise
278
+                    .then(() => {
279
+                        this.setState({
280
+                            previewAudioTrack: this.props.currentAudioTrack
281
+                        });
282
+                    });
283
+            } else {
284
+                cleanupPromise
285
+                    .then(() => createLocalTrack('audio', deviceId))
286
+                    .then(jitsiLocalTrack => {
287
+                        this.setState({
288
+                            previewAudioTrack: jitsiLocalTrack
289
+                        });
290
+                    });
291
+            }
292
+        });
293
+    }
294
+
295
+    /**
296
+     * Callback invoked when a new video input device has been selected. Updates
297
+     * the internal state of the user's selection as well as the video track
298
+     * that should display in the preview. Will reuse the current local video
299
+     * track if it has been selected.
300
+     *
301
+     * @param {string} deviceId - The id of the chosen video input device.
302
+     * @private
303
+     * @returns {void}
304
+     */
305
+    _getAndSetVideoTrack(deviceId) {
306
+        this.setState({
307
+            videoInput: deviceId
308
+        }, () => {
309
+            const cleanupPromise = this._isPreviewingCurrentVideoTrack()
310
+                ? Promise.resolve() : this._disposeVideoPreview();
311
+
312
+            if (this._isCurrentVideoTrack(deviceId)) {
313
+                cleanupPromise
314
+                    .then(() => {
315
+                        this.setState({
316
+                            previewVideoTrack: this.props.currentVideoTrack
317
+                        });
318
+                    });
319
+            } else {
320
+                cleanupPromise
321
+                    .then(() => createLocalTrack('video', deviceId))
322
+                    .then(jitsiLocalTrack => {
323
+                        this.setState({
324
+                            previewVideoTrack: jitsiLocalTrack
325
+                        });
326
+                    });
327
+            }
328
+        });
329
+    }
330
+
331
+    /**
332
+     * Utility function for determining if the current local audio track has the
333
+     * passed in device id.
334
+     *
335
+     * @param {string} deviceId - The device id to match against.
336
+     * @private
337
+     * @returns {boolean} True if the device id is being used by the local audio
338
+     * track.
339
+     */
340
+    _isCurrentAudioTrack(deviceId) {
341
+        return this.props.currentAudioTrack
342
+            && this.props.currentAudioTrack.getDeviceId() === deviceId;
343
+    }
344
+
345
+    /**
346
+     * Utility function for determining if the current local video track has the
347
+     * passed in device id.
348
+     *
349
+     * @param {string} deviceId - The device id to match against.
350
+     * @private
351
+     * @returns {boolean} True if the device id is being used by the local
352
+     * video track.
353
+     */
354
+    _isCurrentVideoTrack(deviceId) {
355
+        return this.props.currentVideoTrack
356
+            && this.props.currentVideoTrack.getDeviceId() === deviceId;
357
+    }
358
+
359
+    /**
360
+     * Utility function for detecting if the current audio preview track is not
361
+     * the currently used audio track.
362
+     *
363
+     * @private
364
+     * @returns {boolean} True if the current audio track is being used for
365
+     * the preview.
366
+     */
367
+    _isPreviewingCurrentAudioTrack() {
368
+        return !this.state.previewAudioTrack
369
+            || this.state.previewAudioTrack === this.props.currentAudioTrack;
370
+    }
371
+
372
+    /**
373
+     * Utility function for detecting if the current video preview track is not
374
+     * the currently used video track.
375
+     *
376
+     * @private
377
+     * @returns {boolean} True if the current video track is being used as the
378
+     * preview.
379
+     */
380
+    _isPreviewingCurrentVideoTrack() {
381
+        return !this.state.previewVideoTrack
382
+            || this.state.previewVideoTrack === this.props.currentVideoTrack;
383
+    }
384
+
385
+    /**
386
+     * Cleans existing preview tracks and signal to closeDeviceSelectionDialog.
387
+     *
388
+     * @private
389
+     * @returns {boolean} Returns false to prevent closure until cleanup is
390
+     * complete.
391
+     */
392
+    _onCancel() {
393
+        if (this._isClosing) {
394
+            return false;
395
+        }
396
+
397
+        this._isClosing = true;
398
+
399
+        const cleanupPromises = this._attemptPreviewTrackCleanup();
400
+
401
+        Promise.all(cleanupPromises)
402
+            .then(this._closeModal)
403
+            .catch(this._closeModal);
404
+
405
+        return false;
406
+    }
407
+
408
+    /**
409
+     * Identify changes to the preferred input/output devices and perform
410
+     * necessary cleanup and requests to use those devices. Closes the modal
411
+     * after cleanup and device change requests complete.
412
+     *
413
+     * @private
414
+     * @returns {boolean} Returns false to prevent closure until cleanup is
415
+     * complete.
416
+     */
417
+    _onSubmit() {
418
+        if (this._isClosing) {
419
+            return false;
420
+        }
421
+
422
+        this._isClosing = true;
423
+
424
+        const deviceChangePromises = [];
425
+
426
+        if (this.state.videoInput && !this._isPreviewingCurrentVideoTrack()) {
427
+            const changeVideoPromise = this._disposeVideoPreview()
428
+                .then(() => {
429
+                    this.props.dispatch(setVideoInputDevice(
430
+                        this.state.videoInput));
431
+                });
432
+
433
+            deviceChangePromises.push(changeVideoPromise);
434
+        }
435
+
436
+        if (this.state.audioInput && !this._isPreviewingCurrentAudioTrack()) {
437
+            const changeAudioPromise = this._disposeAudioPreview()
438
+                .then(() => {
439
+                    this.props.dispatch(setAudioInputDevice(
440
+                        this.state.audioInput));
441
+                });
442
+
443
+            deviceChangePromises.push(changeAudioPromise);
444
+        }
445
+
446
+        if (this.state.audioOutput
447
+            && this.state.audioOutput !== this.props.currentAudioOutputId) {
448
+            this.props.dispatch(setAudioOutputDevice(this.state.audioOutput));
449
+        }
450
+
451
+        Promise.all(deviceChangePromises)
452
+            .then(this._closeModal)
453
+            .catch(this._closeModal);
454
+
455
+        return false;
456
+    }
457
+
458
+    /**
459
+     * Creates an AudioInputPreview for previewing if audio is being received.
460
+     * Null will be returned if local stats for tracking audio input levels
461
+     * cannot be obtained.
462
+     *
463
+     * @private
464
+     * @returns {ReactComponent|null}
465
+     */
466
+    _renderAudioInputPreview() {
467
+        if (this.props.hideAudioInputPreview) {
468
+            return null;
469
+        }
470
+
471
+        return (
472
+            <AudioInputPreview
473
+                track = { this.state.previewAudioTrack
474
+                    || this.props.currentAudioTrack } />
475
+        );
476
+    }
477
+
478
+    /**
479
+     * Creates an AudioOutputPreview instance for playing a test sound with the
480
+     * passed in device id. Null will be returned if hideAudioOutput is truthy.
481
+     *
482
+     * @private
483
+     * @returns {ReactComponent|null}
484
+     */
485
+    _renderAudioOutputPreview() {
486
+        if (this.props.hideAudioOutputSelect) {
487
+            return null;
488
+        }
489
+
490
+        return (
491
+            <AudioOutputPreview
492
+                deviceId = { this.state.audioOutput
493
+                    || this.props.currentAudioOutputId } />
494
+        );
495
+    }
496
+
497
+    /**
498
+     * Creates a DeviceSelector instance based on the passed in configuration.
499
+     *
500
+     * @private
501
+     * @param {Object} props - The props for the DeviceSelector.
502
+     * @returns {ReactElement}
503
+     */
504
+    _renderSelector(props) {
505
+        return (
506
+            <DeviceSelector { ...props } />
507
+        );
508
+    }
509
+
510
+    /**
511
+     * Creates DeviceSelector instances for video output, audio input, and audio
512
+     * output.
513
+     *
514
+     * @private
515
+     * @returns {Array<ReactElement>} DeviceSelector instances.
516
+     */
517
+    _renderSelectors() {
518
+        const availableDevices = this.props._devices;
519
+        const currentAudioId = this.state.audioInput
520
+            || (this.props.currentAudioTrack
521
+                && this.props.currentAudioTrack.getDeviceId());
522
+        const currentAudioOutId = this.state.audioOutput
523
+            || this.props.currentAudioOutputId;
524
+
525
+        // FIXME: On temasys, without a device selected and put into local
526
+        // storage as the default device to use, the current video device id is
527
+        // a blank string. This is because the library gets a local video track
528
+        // and then maps the track's device id by matching the track's label to
529
+        // the MediaDeviceInfos returned from enumerateDevices. In WebRTC, the
530
+        // track label is expected to return the camera device label. However,
531
+        // temasys video track labels refer to track id, not device label, so
532
+        // the library cannot match the track to a device. The workaround of
533
+        // defaulting to the first videoInput available has been re-used from
534
+        // the previous device settings implementation.
535
+        const currentVideoId = this.state.videoInput
536
+            || (this.props.currentVideoTrack
537
+                && this.props.currentVideoTrack.getDeviceId())
538
+            || (availableDevices.videoInput[0]
539
+                && availableDevices.videoInput[0].deviceId)
540
+            || ''; // DeviceSelector expects a string for prop selectedDeviceId.
541
+
542
+        const configurations = [
543
+            {
544
+                devices: availableDevices.videoInput,
545
+                hasPermission: this.props.hasVideoPermission,
546
+                isDisabled: this.props.disableDeviceChange,
547
+                key: 'videoInput',
548
+                label: 'settings.selectCamera',
549
+                onSelect: this._getAndSetVideoTrack,
550
+                selectedDeviceId: currentVideoId
551
+            },
552
+            {
553
+                devices: availableDevices.audioInput,
554
+                hasPermission: this.props.hasAudioPermission,
555
+                isDisabled: this.props.disableAudioInputChange
556
+                    || this.props.disableDeviceChange,
557
+                key: 'audioInput',
558
+                label: 'settings.selectMic',
559
+                onSelect: this._getAndSetAudioTrack,
560
+                selectedDeviceId: currentAudioId
561
+            }
562
+        ];
563
+
564
+        if (!this.props.hideAudioOutputSelect) {
565
+            configurations.push({
566
+                devices: availableDevices.audioOutput,
567
+                hasPermission: this.props.hasAudioPermission
568
+                    || this.props.hasVideoPermission,
569
+                isDisabled: this.props.disableDeviceChange,
570
+                key: 'audioOutput',
571
+                label: 'settings.selectAudioOutput',
572
+                onSelect: this._getAndSetAudioOutput,
573
+                selectedDeviceId: currentAudioOutId
574
+            });
575
+        }
576
+
577
+        return configurations.map(this._renderSelector);
578
+    }
579
+}
580
+
581
+/**
582
+ * Maps (parts of) the Redux state to the associated DeviceSelectionDialog's
583
+ * props.
584
+ *
585
+ * @param {Object} state - The Redux state.
586
+ * @private
587
+ * @returns {{
588
+ *     _devices: Object
589
+ * }}
590
+ */
591
+function _mapStateToProps(state) {
592
+    return {
593
+        _devices: state['features/base/devices']
594
+    };
595
+}
596
+
597
+export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));

+ 180
- 0
react/features/device-selection/components/DeviceSelector.js 查看文件

@@ -0,0 +1,180 @@
1
+import Select from '@atlaskit/single-select';
2
+import React, { Component } from 'react';
3
+
4
+import { translate } from '../../base/i18n';
5
+
6
+/**
7
+ * React component for selecting a device from a select element. Wraps Select
8
+ * with device selection specific logic.
9
+ *
10
+ * @extends Component
11
+ */
12
+class DeviceSelector extends Component {
13
+    /**
14
+     * DeviceSelector component's property types.
15
+     *
16
+     * @static
17
+     */
18
+    static propTypes = {
19
+        /**
20
+         * MediaDeviceInfos used for display in the select element.
21
+         */
22
+        devices: React.PropTypes.array,
23
+
24
+        /**
25
+         * If false, will return a selector with no selection options.
26
+         */
27
+        hasPermission: React.PropTypes.bool,
28
+
29
+        /**
30
+         * If true, will render the selector disabled with a default selection.
31
+         */
32
+        isDisabled: React.PropTypes.bool,
33
+
34
+        /**
35
+         * The translation key to display as a menu label.
36
+         */
37
+        label: React.PropTypes.string,
38
+
39
+        /**
40
+         * The callback to invoke when a selection is made.
41
+         */
42
+        onSelect: React.PropTypes.func,
43
+
44
+        /**
45
+         * The default device to display as selected.
46
+         */
47
+        selectedDeviceId: React.PropTypes.string,
48
+
49
+        /**
50
+         * Invoked to obtain translated strings.
51
+         */
52
+        t: React.PropTypes.func
53
+    }
54
+
55
+    /**
56
+     * Initializes a new DeviceSelector instance.
57
+     *
58
+     * @param {Object} props - The read-only React Component props with which
59
+     * the new instance is to be initialized.
60
+     */
61
+    constructor(props) {
62
+        super(props);
63
+
64
+        this._onSelect = this._onSelect.bind(this);
65
+    }
66
+
67
+    /**
68
+     * Implements React's {@link Component#render()}.
69
+     *
70
+     * @inheritdoc
71
+     * @returns {ReactElement}
72
+     */
73
+    render() {
74
+        if (!this.props.hasPermission) {
75
+            return this._renderNoPermission();
76
+        }
77
+
78
+        if (!this.props.devices.length) {
79
+            return this._renderNoDevices();
80
+        }
81
+
82
+        const items = this.props.devices.map(this._createSelectItem);
83
+        const defaultSelected = items.find(item =>
84
+            item.value === this.props.selectedDeviceId
85
+        );
86
+
87
+        return this._createSelector({
88
+            defaultSelected,
89
+            isDisabled: this.props.isDisabled,
90
+            items,
91
+            placeholder: 'deviceSelection.selectADevice'
92
+        });
93
+    }
94
+
95
+    /**
96
+     * Creates an object in the format expected by Select for an option element.
97
+     *
98
+     * @param {MediaDeviceInfo} device - An object with a label and a deviceId.
99
+     * @private
100
+     * @returns {Object} The passed in media device description converted to a
101
+     * format recognized as a valid Select item.
102
+     */
103
+    _createSelectItem(device) {
104
+        return {
105
+            content: device.label,
106
+            value: device.deviceId
107
+        };
108
+    }
109
+
110
+    /**
111
+     * Creates a Select Component using passed in props and options.
112
+     *
113
+     * @param {Object} options - Additional configuration for display Select.
114
+     * @param {Object} options.defaultSelected - The option that should be set
115
+     * as currently chosen.
116
+     * @param {boolean} options.isDisabled - If true Select will not open on
117
+     * click.
118
+     * @param {Array} options.items - All the selectable options to display.
119
+     * @param {string} options.placeholder - The translation key to display when
120
+     * no selection has been made.
121
+     * @private
122
+     * @returns {ReactElement}
123
+     */
124
+    _createSelector(options) {
125
+        return (
126
+            <Select
127
+                defaultSelected = { options.defaultSelected }
128
+                isDisabled = { options.isDisabled }
129
+                isFirstChild = { true }
130
+                items = { [ { items: options.items || [] } ] }
131
+                label = { this.props.t(this.props.label) }
132
+                noMatchesFound
133
+                    = { this.props.t('deviceSelection.noOtherDevices') }
134
+                onSelected = { this._onSelect }
135
+                placeholder = { this.props.t(options.placeholder) }
136
+                shouldFitContainer = { true } />
137
+        );
138
+    }
139
+
140
+    /**
141
+     * Invokes the passed in callback to notify of selection changes.
142
+     *
143
+     * @param {Object} selection - Event returned from Select.
144
+     * @private
145
+     * @returns {void}
146
+     */
147
+    _onSelect(selection) {
148
+        this.props.onSelect(selection.item.value);
149
+    }
150
+
151
+    /**
152
+     * Creates a Select Component that is disabled and has a placeholder
153
+     * indicating there are no devices to select.
154
+     *
155
+     * @private
156
+     * @returns {ReactElement}
157
+     */
158
+    _renderNoDevices() {
159
+        return this._createSelector({
160
+            isDisabled: true,
161
+            placeholder: 'settings.noDevice'
162
+        });
163
+    }
164
+
165
+    /**
166
+     * Creates a Select Component that is disabled and has a placeholder stating
167
+     * there is no permission to display the devices.
168
+     *
169
+     * @private
170
+     * @returns {ReactElement}
171
+     */
172
+    _renderNoPermission() {
173
+        return this._createSelector({
174
+            isDisabled: true,
175
+            placeholder: 'settings.noPermission'
176
+        });
177
+    }
178
+}
179
+
180
+export default translate(DeviceSelector);

+ 203
- 0
react/features/device-selection/components/VideoInputPreview.js 查看文件

@@ -0,0 +1,203 @@
1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+const VIDEO_MUTE_CLASS = 'video-muted';
6
+
7
+/**
8
+ * React component for displaying video. This component defers to lib-jitsi-meet
9
+ * logic for rendering the video.
10
+ *
11
+ * @extends Component
12
+ */
13
+class VideoInputPreview extends Component {
14
+    /**
15
+     * VideoInputPreview component's property types.
16
+     *
17
+     * @static
18
+     */
19
+    static propTypes = {
20
+        /**
21
+         * Invoked to obtain translated strings.
22
+         */
23
+        t: React.PropTypes.func,
24
+
25
+        /*
26
+         * The JitsiLocalTrack to display.
27
+         */
28
+        track: React.PropTypes.object
29
+    }
30
+
31
+    /**
32
+     * Initializes a new VideoInputPreview instance.
33
+     *
34
+     * @param {Object} props - The read-only React Component props with which
35
+     * the new instance is to be initialized.
36
+     */
37
+    constructor(props) {
38
+        super(props);
39
+
40
+        this._rootElement = null;
41
+        this._videoElement = null;
42
+
43
+        this._setRootElement = this._setRootElement.bind(this);
44
+        this._setVideoElement = this._setVideoElement.bind(this);
45
+    }
46
+
47
+    /**
48
+     * Invokes the library for rendering the video on initial display.
49
+     *
50
+     * @inheritdoc
51
+     * @returns {void}
52
+     */
53
+    componentDidMount() {
54
+        this._attachTrack(this.props.track);
55
+    }
56
+
57
+    /**
58
+     * Remove any existing associations between the current previewed track and
59
+     * the component's video element.
60
+     *
61
+     * @inheritdoc
62
+     * @returns {void}
63
+     */
64
+    componentWillUnmount() {
65
+        this._detachTrack(this.props.track);
66
+    }
67
+
68
+    /**
69
+     * Implements React's {@link Component#render()}.
70
+     *
71
+     * @inheritdoc
72
+     * @returns {ReactElement}
73
+     */
74
+    render() {
75
+        return (
76
+            <div
77
+                className = 'video-input-preview'
78
+                ref = { this._setRootElement }>
79
+                <video
80
+                    autoPlay = { true }
81
+                    className = 'video-input-preview-display flipVideoX'
82
+                    ref = { this._setVideoElement } />
83
+                <div className = 'video-input-preview-muted'>
84
+                    { this.props.t('videothumbnail.muted') }
85
+                </div>
86
+            </div>
87
+        );
88
+    }
89
+
90
+    /**
91
+     * Only update when the deviceId has changed. This component is somewhat
92
+     * black-boxed from React's rendering so lib-jitsi-meet can instead handle
93
+     * updating of the video preview, which takes browser differences into
94
+     * consideration. For example, temasys's video object must be visible to
95
+     * update the displayed track, but React's re-rendering could potentially
96
+     * remove the video object from the page.
97
+     *
98
+     * @inheritdoc
99
+     * @returns {void}
100
+     */
101
+    shouldComponentUpdate(nextProps) {
102
+        if (nextProps.track !== this.props.track) {
103
+            this._detachTrack(this.props.track);
104
+            this._attachTrack(nextProps.track);
105
+        }
106
+
107
+        return false;
108
+    }
109
+
110
+    /**
111
+     * Calls into the passed in track to associate the track with the
112
+     * component's video element and render video. Also sets the instance
113
+     * variable for the video element as the element the track attached to,
114
+     * which could be an Object if on a temasys supported browser.
115
+     *
116
+     * @param {JitsiLocalTrack} track - The library's track model which will be
117
+     * displayed.
118
+     * @private
119
+     * @returns {void}
120
+     */
121
+    _attachTrack(track) {
122
+        if (!track) {
123
+            return;
124
+        }
125
+
126
+        // Do not attempt to display a preview if the track is muted, as the
127
+        // library will simply return a falsy value for the element anyway.
128
+        if (track.isMuted()) {
129
+            this._showMuteOverlay(true);
130
+        } else {
131
+            this._showMuteOverlay(false);
132
+
133
+            const updatedVideoElement = track.attach(this._videoElement);
134
+
135
+            this._setVideoElement(updatedVideoElement);
136
+        }
137
+    }
138
+
139
+    /**
140
+     * Removes the association to the component's video element from the passed
141
+     * in JitsiLocalTrack to stop the track from rendering. With temasys, the
142
+     * video element must still be visible for detaching to complete.
143
+     *
144
+     * @param {JitsiLocalTrack} track - The library's track model which needs
145
+     * to stop previewing in the video element.
146
+     * @private
147
+     * @returns {void}
148
+     */
149
+    _detachTrack(track) {
150
+        // Detach the video element from the track only if it has already
151
+        // been attached. This accounts for a special case with temasys
152
+        // where if detach is being called before attach, the video
153
+        // element is converted to Object without updating this
154
+        // component's reference to the video element.
155
+        if (this._videoElement
156
+            && track
157
+            && track.containers.includes(this._videoElement)) {
158
+            track.detach(this._videoElement);
159
+        }
160
+    }
161
+
162
+    /**
163
+     * Sets the component's root element.
164
+     *
165
+     * @param {Object} element - The highest DOM element in the component.
166
+     * @private
167
+     * @returns {void}
168
+     */
169
+    _setRootElement(element) {
170
+        this._rootElement = element;
171
+    }
172
+
173
+    /**
174
+     * Sets an instance variable for the component's video element so it can be
175
+     * referenced later for attaching and detaching a JitsiLocalTrack.
176
+     *
177
+     * @param {Object} element - DOM element for the component's video display.
178
+     * @private
179
+     * @returns {void}
180
+     */
181
+    _setVideoElement(element) {
182
+        this._videoElement = element;
183
+    }
184
+
185
+    /**
186
+     * Adds or removes a class to the component's parent node to indicate mute
187
+     * status.
188
+     *
189
+     * @param {boolean} shouldShow - True if the mute class should be added and
190
+     * false if the class should be removed.
191
+     * @private
192
+     * @returns {void}
193
+     */
194
+    _showMuteOverlay(shouldShow) {
195
+        if (shouldShow) {
196
+            this._rootElement.classList.add(VIDEO_MUTE_CLASS);
197
+        } else {
198
+            this._rootElement.classList.remove(VIDEO_MUTE_CLASS);
199
+        }
200
+    }
201
+}
202
+
203
+export default translate(VideoInputPreview);

+ 1
- 0
react/features/device-selection/components/index.js 查看文件

@@ -0,0 +1 @@
1
+export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';

+ 1
- 0
react/features/device-selection/index.js 查看文件

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

Loading…
取消
儲存