Quellcode durchsuchen

feat: new device selection modal with previews

The Device Selection modal consists of:
- DeviceSelection, an overly smart component responsible for
  triggering stream creation and cleanup.
- DeviceSelector for selector elements.
- VideoInputPreview for displaying a video preview.
- AudioInputPreview for displaying a volume meter.
- AudioOutputPreview for a test sound output link.

Store changes include is primarily storing the list of
available devices in redux. Other app state has been left
alone for future refactoring.
j8
Leonard Kim vor 8 Jahren
Ursprung
Commit
2f994b1227

+ 22
- 1
conference.js Datei anzeigen

@@ -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) {
@@ -1756,8 +1777,8 @@ export default {
1756 1777
                 }
1757 1778
 
1758 1779
                 mediaDeviceHelper.setCurrentMediaDevices(devices);
1759
-
1760 1780
                 APP.UI.onAvailableDevicesChanged(devices);
1781
+                APP.store.dispatch(updateDeviceList(devices));
1761 1782
             });
1762 1783
 
1763 1784
             this.deviceChangeListener = (devices) =>

+ 1
- 0
css/main.scss Datei anzeigen

@@ -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';

+ 84
- 0
css/modals/device-selection/_device-selection.scss Datei anzeigen

@@ -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 Datei anzeigen

@@ -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
- 0
modules/UI/UI.js Datei anzeigen

@@ -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";
@@ -1081,6 +1085,7 @@ UI.onLocalRaiseHandChanged = function (isRaisedHand) {
1081 1085
  */
1082 1086
 UI.onAvailableDevicesChanged = function (devices) {
1083 1087
     SettingsMenu.changeDevicesList(devices);
1088
+    APP.store.dispatch(updateDeviceList(devices));
1084 1089
 };
1085 1090
 
1086 1091
 /**

+ 1
- 0
package.json Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

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

+ 1
- 0
react/features/device-selection/index.js Datei anzeigen

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

Laden…
Abbrechen
Speichern