浏览代码

fix: Add GUM timeout & improve device permissions

master
Hristo Terezov 4 年前
父节点
当前提交
a6c6cd6c56

+ 34
- 5
conference.js 查看文件

@@ -504,6 +504,8 @@ export default {
504 504
 
505 505
         let tryCreateLocalTracks;
506 506
 
507
+        const timeout = browser.isElectron() ? 15000 : 60000;
508
+
507 509
         // FIXME is there any simpler way to rewrite this spaghetti below ?
508 510
         if (options.startScreenSharing) {
509 511
             tryCreateLocalTracks = this._createDesktopTrack()
@@ -512,7 +514,10 @@ export default {
512 514
                         return [ desktopStream ];
513 515
                     }
514 516
 
515
-                    return createLocalTracksF({ devices: [ 'audio' ] }, true)
517
+                    return createLocalTracksF({
518
+                        devices: [ 'audio' ],
519
+                        timeout
520
+                    }, true)
516 521
                         .then(([ audioStream ]) =>
517 522
                             [ desktopStream, audioStream ])
518 523
                         .catch(error => {
@@ -526,7 +531,10 @@ export default {
526 531
                     errors.screenSharingError = error;
527 532
 
528 533
                     return requestedAudio
529
-                        ? createLocalTracksF({ devices: [ 'audio' ] }, true)
534
+                        ? createLocalTracksF({
535
+                            devices: [ 'audio' ],
536
+                            timeout
537
+                        }, true)
530 538
                         : [];
531 539
                 })
532 540
                 .catch(error => {
@@ -538,15 +546,33 @@ export default {
538 546
             // Resolve with no tracks
539 547
             tryCreateLocalTracks = Promise.resolve([]);
540 548
         } else {
541
-            tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
549
+            tryCreateLocalTracks = createLocalTracksF({
550
+                devices: initialDevices,
551
+                timeout
552
+            }, true)
542 553
                 .catch(err => {
543 554
                     if (requestedAudio && requestedVideo) {
544 555
 
545 556
                         // Try audio only...
546 557
                         errors.audioAndVideoError = err;
547 558
 
559
+                        if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
560
+                            // In this case we expect that the permission prompt is still visible. There is no point of
561
+                            // executing GUM with different source. Also at the time of writting the following
562
+                            // inconsistency have been noticed in some browsers - if the permissions prompt is visible
563
+                            // and another GUM is executed the prompt does not change its content but if the user
564
+                            // clicks allow the user action isassociated with the latest GUM call.
565
+                            errors.audioOnlyError = err;
566
+                            errors.videoOnlyError = err;
567
+
568
+                            return [];
569
+                        }
570
+
548 571
                         return (
549
-                            createLocalTracksF({ devices: [ 'audio' ] }, true));
572
+                            createLocalTracksF({
573
+                                devices: [ 'audio' ],
574
+                                timeout
575
+                            }, true));
550 576
                     } else if (requestedAudio && !requestedVideo) {
551 577
                         errors.audioOnlyError = err;
552 578
 
@@ -567,7 +593,10 @@ export default {
567 593
 
568 594
                     // Try video only...
569 595
                     return requestedVideo
570
-                        ? createLocalTracksF({ devices: [ 'video' ] }, true)
596
+                        ? createLocalTracksF({
597
+                            devices: [ 'video' ],
598
+                            timeout
599
+                        }, true)
571 600
                         : [];
572 601
                 })
573 602
                 .catch(err => {

+ 3
- 1
lang/main.json 查看文件

@@ -180,6 +180,7 @@
180 180
         "cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
181 181
         "cameraNotSendingDataTitle": "Unable to access camera",
182 182
         "cameraPermissionDeniedError": "You have not granted permission to use your camera. You can still join the conference but others won't see you. Use the camera button in the address bar to fix this.",
183
+        "cameraTimeoutError": "Could not start video source. Timeout occured!",
183 184
         "cameraUnknownError": "Cannot use camera for an unknown reason.",
184 185
         "cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
185 186
         "Cancel": "Cancel",
@@ -233,6 +234,7 @@
233 234
         "micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
234 235
         "micNotSendingDataTitle": "Your mic is muted by your system settings",
235 236
         "micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
237
+        "micTimeoutError": "Could not start audio source. Timeout occured!",
236 238
         "micUnknownError": "Cannot use microphone for an unknown reason.",
237 239
         "muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
238 240
         "muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
@@ -810,7 +812,7 @@
810 812
         "androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
811 813
         "chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
812 814
         "edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions.",
813
-        "electronGrantPermissions": "Please grant permissions to use your camera and microphone",
815
+        "electronGrantPermissions": "Trying to access your camera and microphone",
814 816
         "firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
815 817
         "iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
816 818
         "nwjsGrantPermissions": "Please grant permissions to use your camera and microphone",

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

@@ -84,3 +84,13 @@ export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS';
84 84
  * }
85 85
  */
86 86
 export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE';
87
+
88
+/**
89
+ * The type of Redux action which signals that the device permissions have changed.
90
+ *
91
+ * {
92
+ *     type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
93
+ *     permissions: Object
94
+ * }
95
+ */
96
+export const DEVICE_PERMISSIONS_CHANGED = 'DEVICE_PERMISSIONS_CHANGED';

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

@@ -7,6 +7,7 @@ import {
7 7
 import {
8 8
     ADD_PENDING_DEVICE_REQUEST,
9 9
     CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
10
+    DEVICE_PERMISSIONS_CHANGED,
10 11
     NOTIFY_CAMERA_ERROR,
11 12
     NOTIFY_MIC_ERROR,
12 13
     REMOVE_PENDING_DEVICE_REQUESTS,
@@ -320,3 +321,19 @@ export function checkAndNotifyForNewDevice(newDevices, oldDevices) {
320 321
         oldDevices
321 322
     };
322 323
 }
324
+
325
+/**
326
+ * Signals that the device permissions have changed.
327
+ *
328
+ * @param {Object} permissions - Object with the permissions.
329
+ * @returns {{
330
+ *      type: DEVICE_PERMISSIONS_CHANGED,
331
+ *      permissions: Object
332
+ * }}
333
+ */
334
+export function devicePermissionsChanged(permissions) {
335
+    return {
336
+        type: DEVICE_PERMISSIONS_CHANGED,
337
+        permissions
338
+    };
339
+}

+ 43
- 3
react/features/base/devices/middleware.js 查看文件

@@ -5,7 +5,8 @@ import { processExternalDeviceRequest } from '../../device-selection';
5 5
 import { showNotification, showWarningNotification } from '../../notifications';
6 6
 import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
7 7
 import { isPrejoinPageVisible } from '../../prejoin/functions';
8
-import { JitsiTrackErrors } from '../lib-jitsi-meet';
8
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
9
+import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
9 10
 import { MiddlewareRegistry } from '../redux';
10 11
 import { updateSettings } from '../settings';
11 12
 
@@ -18,6 +19,7 @@ import {
18 19
     UPDATE_DEVICE_LIST
19 20
 } from './actionTypes';
20 21
 import {
22
+    devicePermissionsChanged,
21 23
     removePendingDeviceRequests,
22 24
     setAudioInputDevice,
23 25
     setVideoInputDevice
@@ -35,17 +37,25 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
35 37
         [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
36 38
         [JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
37 39
         [JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
38
-        [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError'
40
+        [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
41
+        [JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
39 42
     },
40 43
     camera: {
41 44
         [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
42 45
         [JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
43 46
         [JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
44 47
         [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
45
-        [JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError'
48
+        [JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError',
49
+        [JitsiTrackErrors.TIMEOUT]: 'dialog.cameraTimeoutError'
46 50
     }
47 51
 };
48 52
 
53
+
54
+/**
55
+ * A listener for device permissions changed reported from lib-jitsi-meet.
56
+ */
57
+let permissionsListener;
58
+
49 59
 /**
50 60
  * Logs the current device list.
51 61
  *
@@ -73,6 +83,36 @@ function logDeviceList(deviceList) {
73 83
 // eslint-disable-next-line no-unused-vars
74 84
 MiddlewareRegistry.register(store => next => action => {
75 85
     switch (action.type) {
86
+    case APP_WILL_MOUNT: {
87
+        const _permissionsListener = permissions => {
88
+            store.dispatch(devicePermissionsChanged(permissions));
89
+        };
90
+        const { mediaDevices } = JitsiMeetJS;
91
+
92
+        permissionsListener = _permissionsListener;
93
+        mediaDevices.addEventListener(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
94
+        Promise.all([
95
+            mediaDevices.isDevicePermissionGranted('audio'),
96
+            mediaDevices.isDevicePermissionGranted('video')
97
+        ])
98
+        .then(results => {
99
+            _permissionsListener({
100
+                audio: results[0],
101
+                video: results[1]
102
+            });
103
+        })
104
+        .catch(() => {
105
+            // Ignore errors.
106
+        });
107
+        break;
108
+    }
109
+    case APP_WILL_UNMOUNT:
110
+        if (typeof permissionsListener === 'function') {
111
+            JitsiMeetJS.mediaDevices.removeEventListener(
112
+                JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
113
+            permissionsListener = undefined;
114
+        }
115
+        break;
76 116
     case NOTIFY_CAMERA_ERROR: {
77 117
         if (typeof APP !== 'object' || !action.error) {
78 118
             break;

+ 12
- 1
react/features/base/devices/reducer.js 查看文件

@@ -2,6 +2,7 @@ import { ReducerRegistry } from '../redux';
2 2
 
3 3
 import {
4 4
     ADD_PENDING_DEVICE_REQUEST,
5
+    DEVICE_PERMISSIONS_CHANGED,
5 6
     REMOVE_PENDING_DEVICE_REQUESTS,
6 7
     SET_AUDIO_INPUT_DEVICE,
7 8
     SET_VIDEO_INPUT_DEVICE,
@@ -16,7 +17,11 @@ const DEFAULT_STATE = {
16 17
         audioOutput: [],
17 18
         videoInput: []
18 19
     },
19
-    pendingRequests: []
20
+    pendingRequests: [],
21
+    permissions: {
22
+        audio: false,
23
+        video: false
24
+    }
20 25
 };
21 26
 
22 27
 /**
@@ -68,6 +73,12 @@ ReducerRegistry.register(
68 73
 
69 74
             return state;
70 75
         }
76
+        case DEVICE_PERMISSIONS_CHANGED: {
77
+            return {
78
+                ...state,
79
+                permissions: action.permissions
80
+            };
81
+        }
71 82
         default:
72 83
             return state;
73 84
         }

+ 4
- 2
react/features/base/lib-jitsi-meet/functions.any.js 查看文件

@@ -13,9 +13,10 @@ const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
13 13
  * @param {string} type - The media type of track being created. Expected values
14 14
  * are "video" or "audio".
15 15
  * @param {string} deviceId - The id of the target media source.
16
+ * @param {number} [timeout] - A timeout for the JitsiMeetJS.createLocalTracks function call.
16 17
  * @returns {Promise<JitsiLocalTrack>}
17 18
  */
18
-export function createLocalTrack(type: string, deviceId: string) {
19
+export function createLocalTrack(type: string, deviceId: string, timeout: ?number) {
19 20
     return (
20 21
         JitsiMeetJS.createLocalTracks({
21 22
             cameraDeviceId: deviceId,
@@ -24,7 +25,8 @@ export function createLocalTrack(type: string, deviceId: string) {
24 25
             // eslint-disable-next-line camelcase
25 26
             firefox_fake_device:
26 27
                 window.config && window.config.firefox_fake_device,
27
-            micDeviceId: deviceId
28
+            micDeviceId: deviceId,
29
+            timeout
28 30
         })
29 31
             .then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
30 32
 }

+ 6
- 4
react/features/base/tracks/functions.js 查看文件

@@ -62,6 +62,7 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
62 62
  * and/or 'video'.
63 63
  * @param {string|null} [options.micDeviceId] - Microphone device id or
64 64
  * {@code undefined} to use app's settings.
65
+ * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
65 66
  * @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
66 67
  * should check for a {@code getUserMedia} permission prompt and fire a
67 68
  * corresponding event.
@@ -71,6 +72,7 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
71 72
  */
72 73
 export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) {
73 74
     let { cameraDeviceId, micDeviceId } = options;
75
+    const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options;
74 76
 
75 77
     if (typeof APP !== 'undefined') {
76 78
         // TODO The app's settings should go in the redux store and then the
@@ -105,16 +107,16 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
105 107
                     cameraDeviceId,
106 108
                     constraints,
107 109
                     desktopSharingFrameRate,
108
-                    desktopSharingSourceDevice:
109
-                        options.desktopSharingSourceDevice,
110
-                    desktopSharingSources: options.desktopSharingSources,
110
+                    desktopSharingSourceDevice,
111
+                    desktopSharingSources,
111 112
 
112 113
                     // Copy array to avoid mutations inside library.
113 114
                     devices: options.devices.slice(0),
114 115
                     effects,
115 116
                     firefox_fake_device, // eslint-disable-line camelcase
116 117
                     micDeviceId,
117
-                    resolution
118
+                    resolution,
119
+                    timeout
118 120
                 },
119 121
                 firePermissionPromptIsShownEvent)
120 122
             .catch(err => {

+ 14
- 36
react/features/device-selection/components/DeviceSelection.js 查看文件

@@ -6,7 +6,6 @@ import AbstractDialogTab, {
6 6
     type Props as AbstractDialogTabProps
7 7
 } from '../../base/dialog/components/web/AbstractDialogTab';
8 8
 import { translate } from '../../base/i18n/functions';
9
-import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
10 9
 import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
11 10
 import logger from '../logger';
12 11
 
@@ -41,6 +40,16 @@ export type Props = {
41 40
      */
42 41
     disableDeviceChange: boolean,
43 42
 
43
+    /**
44
+     * Whether or not the audio permission was granted.
45
+     */
46
+    hasAudioPermission: boolean,
47
+
48
+    /**
49
+     * Whether or not the audio permission was granted.
50
+     */
51
+    hasVideoPermission: boolean,
52
+
44 53
     /**
45 54
      * If true, the audio meter will not display. Necessary for browsers or
46 55
      * configurations that do not support local stats to prevent a
@@ -87,16 +96,6 @@ export type Props = {
87 96
  */
88 97
 type State = {
89 98
 
90
-    /**
91
-     * Whether or not the audio permission was granted.
92
-     */
93
-    hasAudioPermission: boolean,
94
-
95
-    /**
96
-     * Whether or not the audio permission was granted.
97
-     */
98
-    hasVideoPermission: boolean,
99
-
100 99
     /**
101 100
      * The JitsiTrack to use for previewing audio input.
102 101
      */
@@ -141,8 +140,6 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
141 140
         super(props);
142 141
 
143 142
         this.state = {
144
-            hasAudioPermission: false,
145
-            hasVideoPermission: false,
146 143
             previewAudioTrack: null,
147 144
             previewVideoTrack: null,
148 145
             previewVideoTrackError: null
@@ -170,27 +167,9 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
170 167
      * video input previews.
171 168
      *
172 169
      * @param {Object} prevProps - Previous props this component received.
173
-     * @param {Object} prevState - Previous state this component had.
174 170
      * @returns {void}
175 171
      */
176
-    componentDidUpdate(prevProps, prevState) {
177
-        const { previewAudioTrack, previewVideoTrack } = prevState;
178
-
179
-        if ((!previewAudioTrack && this.state.previewAudioTrack)
180
-                || (!previewVideoTrack && this.state.previewVideoTrack)) {
181
-            Promise.all([
182
-                JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio'),
183
-                JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video')
184
-            ]).then(r => {
185
-                const [ hasAudioPermission, hasVideoPermission ] = r;
186
-
187
-                this.setState({
188
-                    hasAudioPermission,
189
-                    hasVideoPermission
190
-                });
191
-            });
192
-        }
193
-
172
+    componentDidUpdate(prevProps) {
194 173
         if (prevProps.selectedAudioInputId
195 174
             !== this.props.selectedAudioInputId) {
196 175
             this._createAudioInputTrack(this.props.selectedAudioInputId);
@@ -258,7 +237,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
258 237
      */
259 238
     _createAudioInputTrack(deviceId) {
260 239
         return this._disposeAudioInputPreview()
261
-            .then(() => createLocalTrack('audio', deviceId))
240
+            .then(() => createLocalTrack('audio', deviceId, 5000))
262 241
             .then(jitsiLocalTrack => {
263 242
                 if (this._unMounted) {
264 243
                     jitsiLocalTrack.dispose();
@@ -286,7 +265,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
286 265
      */
287 266
     _createVideoInputTrack(deviceId) {
288 267
         return this._disposeVideoInputPreview()
289
-            .then(() => createLocalTrack('video', deviceId))
268
+            .then(() => createLocalTrack('video', deviceId, 5000))
290 269
             .then(jitsiLocalTrack => {
291 270
                 if (!jitsiLocalTrack) {
292 271
                     return Promise.reject();
@@ -360,8 +339,7 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
360 339
      * @returns {Array<ReactElement>} DeviceSelector instances.
361 340
      */
362 341
     _renderSelectors() {
363
-        const { availableDevices } = this.props;
364
-        const { hasAudioPermission, hasVideoPermission } = this.state;
342
+        const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
365 343
 
366 344
         const configurations = [
367 345
             {

+ 3
- 0
react/features/device-selection/functions.js 查看文件

@@ -32,6 +32,7 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
32 32
     const state = toState(stateful);
33 33
     const settings = state['features/base/settings'];
34 34
     const { conference } = state['features/base/conference'];
35
+    const { permissions } = state['features/base/devices'];
35 36
     let disableAudioInputChange = !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
36 37
     let selectedAudioInputId = settings.micDeviceId;
37 38
     let selectedAudioOutputId = getAudioOutputDeviceId();
@@ -55,6 +56,8 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
55 56
         disableAudioInputChange,
56 57
         disableDeviceChange:
57 58
             !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
59
+        hasAudioPermission: permissions.audio,
60
+        hasVideoPermission: permissions.video,
58 61
         hideAudioInputPreview:
59 62
             !JitsiMeetJS.isCollectingLocalStats(),
60 63
         hideAudioOutputSelect: !JitsiMeetJS.mediaDevices

+ 1
- 3
react/features/settings/components/web/audio/AudioSettingsContent.js 查看文件

@@ -182,9 +182,7 @@ class AudioSettingsContent extends Component<Props, State> {
182 182
 
183 183
         this._disposeTracks(this.state.audioTracks);
184 184
 
185
-        const audioTracks = await createLocalAudioTracks(
186
-            this.props.microphoneDevices
187
-        );
185
+        const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000);
188 186
 
189 187
         if (this._componentWasUnmounted) {
190 188
             this._disposeTracks(audioTracks);

+ 1
- 3
react/features/settings/components/web/video/VideoSettingsContent.js 查看文件

@@ -85,9 +85,7 @@ class VideoSettingsContent extends Component<Props, State> {
85 85
     async _setTracks() {
86 86
         this._disposeTracks(this.state.trackData);
87 87
 
88
-        const trackData = await createLocalVideoTracks(
89
-            this.props.videoDeviceIds,
90
-        );
88
+        const trackData = await createLocalVideoTracks(this.props.videoDeviceIds, 5000);
91 89
 
92 90
         // In case the component gets unmounted before the tracks are created
93 91
         // avoid a leak by not setting the state

+ 6
- 4
react/features/settings/functions.js 查看文件

@@ -156,11 +156,12 @@ export function getProfileTabProps(stateful: Object | Function) {
156 156
  * all the video jitsiTracks and appropriate errors for the given device ids.
157 157
  *
158 158
  * @param {string[]} ids - The list of the camera ids for wich to create tracks.
159
+ * @param {number} [timeout] - A timeout for the createLocalTrack function call.
159 160
  *
160 161
  * @returns {Promise<Object[]>}
161 162
  */
162
-export function createLocalVideoTracks(ids: string[]) {
163
-    return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId)
163
+export function createLocalVideoTracks(ids: string[], timeout: ?number) {
164
+    return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
164 165
                    .then(jitsiTrack => {
165 166
                        return {
166 167
                            jitsiTrack,
@@ -182,6 +183,7 @@ export function createLocalVideoTracks(ids: string[]) {
182 183
  * the audio track and the corresponding audio device information.
183 184
  *
184 185
  * @param {Object[]} devices - A list of microphone devices.
186
+ * @param {number} [timeout] - A timeout for the createLocalTrack function call.
185 187
  * @returns {Promise<{
186 188
  *   deviceId: string,
187 189
  *   hasError: boolean,
@@ -189,14 +191,14 @@ export function createLocalVideoTracks(ids: string[]) {
189 191
  *   label: string
190 192
  * }[]>}
191 193
  */
192
-export function createLocalAudioTracks(devices: Object[]) {
194
+export function createLocalAudioTracks(devices: Object[], timeout: ?number) {
193 195
     return Promise.all(
194 196
         devices.map(async ({ deviceId, label }) => {
195 197
             let jitsiTrack = null;
196 198
             let hasError = false;
197 199
 
198 200
             try {
199
-                jitsiTrack = await createLocalTrack('audio', deviceId);
201
+                jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
200 202
             } catch (err) {
201 203
                 hasError = true;
202 204
             }

+ 11
- 79
react/features/toolbox/components/web/AudioSettingsButton.js 查看文件

@@ -7,7 +7,6 @@ import { IconArrowDown } from '../../../base/icons';
7 7
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
8 8
 import { connect } from '../../../base/redux';
9 9
 import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
10
-import { getMediaPermissionPromptVisibility } from '../../../overlay';
11 10
 import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings';
12 11
 import { isAudioSettingsButtonDisabled } from '../../functions';
13 12
 import AudioMuteButton from '../AudioMuteButton';
@@ -15,15 +14,14 @@ import AudioMuteButton from '../AudioMuteButton';
15 14
 type Props = {
16 15
 
17 16
     /**
18
-     * Click handler for the small icon. Opens audio options.
17
+     * Indicates whether audio permissions have been granted or denied.
19 18
      */
20
-    onAudioOptionsClick: Function,
19
+    hasPermissions: boolean,
21 20
 
22 21
     /**
23
-     * Whether the permission prompt is visible or not.
24
-     * Useful for enabling the button on permission grant.
22
+     * Click handler for the small icon. Opens audio options.
25 23
      */
26
-    permissionPromptVisibility: boolean,
24
+    onAudioOptionsClick: Function,
27 25
 
28 26
     /**
29 27
      * If the button should be disabled.
@@ -34,83 +32,15 @@ type Props = {
34 32
      * Flag controlling the visibility of the button.
35 33
      * AudioSettings popup is disabled on mobile browsers.
36 34
      */
37
-    visible: boolean,
35
+    visible: boolean
38 36
 };
39 37
 
40
-type State = {
41
-
42
-    /**
43
-     * If there are permissions for audio devices.
44
-     */
45
-    hasPermissions: boolean,
46
-}
47
-
48 38
 /**
49 39
  * Button used for audio & audio settings.
50 40
  *
51 41
  * @returns {ReactElement}
52 42
  */
53
-class AudioSettingsButton extends Component<Props, State> {
54
-    _isMounted: boolean;
55
-
56
-    /**
57
-     * Initializes a new {@code AudioSettingsButton} instance.
58
-     *
59
-     * @param {Object} props - The read-only properties with which the new
60
-     * instance is to be initialized.
61
-     */
62
-    constructor(props) {
63
-        super(props);
64
-
65
-        this._isMounted = true;
66
-        this.state = {
67
-            hasPermissions: false
68
-        };
69
-    }
70
-
71
-    /**
72
-     * Updates device permissions.
73
-     *
74
-     * @returns {Promise<void>}
75
-     */
76
-    async _updatePermissions() {
77
-        const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
78
-            'audio',
79
-        );
80
-
81
-        this._isMounted && this.setState({
82
-            hasPermissions
83
-        });
84
-    }
85
-
86
-    /**
87
-     * Implements React's {@link Component#componentDidMount}.
88
-     *
89
-     * @inheritdoc
90
-     */
91
-    componentDidMount() {
92
-        this._updatePermissions();
93
-    }
94
-
95
-    /**
96
-     * Implements React's {@link Component#componentDidUpdate}.
97
-     *
98
-     * @inheritdoc
99
-     */
100
-    componentDidUpdate(prevProps) {
101
-        if (this.props.permissionPromptVisibility !== prevProps.permissionPromptVisibility) {
102
-            this._updatePermissions();
103
-        }
104
-    }
105
-
106
-    /**
107
-     * Implements React's {@link Component#componentWillUnmount}.
108
-     *
109
-     * @inheritdoc
110
-     */
111
-    componentWillUnmount() {
112
-        this._isMounted = false;
113
-    }
43
+class AudioSettingsButton extends Component<Props> {
114 44
 
115 45
     /**
116 46
      * Implements React's {@link Component#render}.
@@ -118,8 +48,8 @@ class AudioSettingsButton extends Component<Props, State> {
118 48
      * @inheritdoc
119 49
      */
120 50
     render() {
121
-        const { isDisabled, onAudioOptionsClick, visible } = this.props;
122
-        const settingsDisabled = !this.state.hasPermissions
51
+        const { hasPermissions, isDisabled, onAudioOptionsClick, visible } = this.props;
52
+        const settingsDisabled = !hasPermissions
123 53
             || isDisabled
124 54
             || !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
125 55
 
@@ -143,9 +73,11 @@ class AudioSettingsButton extends Component<Props, State> {
143 73
  * @returns {Object}
144 74
  */
145 75
 function mapStateToProps(state) {
76
+    const { permissions = {} } = state['features/base/devices'];
77
+
146 78
     return {
79
+        hasPermissions: permissions.audio,
147 80
         isDisabled: isAudioSettingsButtonDisabled(state),
148
-        permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
149 81
         visible: !isMobileBrowser()
150 82
     };
151 83
 }

+ 9
- 78
react/features/toolbox/components/web/VideoSettingsButton.js 查看文件

@@ -4,11 +4,9 @@ import React, { Component } from 'react';
4 4
 
5 5
 import { isMobileBrowser } from '../../../base/environment/utils';
6 6
 import { IconArrowDown } from '../../../base/icons';
7
-import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
8 7
 import { connect } from '../../../base/redux';
9 8
 import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
10 9
 import { getLocalJitsiVideoTrack } from '../../../base/tracks';
11
-import { getMediaPermissionPromptVisibility } from '../../../overlay';
12 10
 import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
13 11
 import { isVideoSettingsButtonDisabled } from '../../functions';
14 12
 import VideoMuteButton from '../VideoMuteButton';
@@ -21,10 +19,9 @@ type Props = {
21 19
     onVideoOptionsClick: Function,
22 20
 
23 21
     /**
24
-     * Whether the permission prompt is visible or not.
25
-     * Useful for enabling the button on initial permission grant.
22
+     * Indicates whether video permissions have been granted or denied.
26 23
      */
27
-    permissionPromptVisibility: boolean,
24
+    hasPermissions: boolean,
28 25
 
29 26
     /**
30 27
      * Whether there is a video track or not.
@@ -42,15 +39,7 @@ type Props = {
42 39
      * as mobile devices do not support capture of more than one
43 40
      * camera at a time.
44 41
      */
45
-    visible: boolean,
46
-};
47
-
48
-type State = {
49
-
50
-    /**
51
-     * Whether the app has video permissions or not.
52
-     */
53
-    hasPermissions: boolean,
42
+    visible: boolean
54 43
 };
55 44
 
56 45
 /**
@@ -58,23 +47,7 @@ type State = {
58 47
  *
59 48
  * @returns {ReactElement}
60 49
  */
61
-class VideoSettingsButton extends Component<Props, State> {
62
-    _isMounted: boolean;
63
-
64
-    /**
65
-     * Initializes a new {@code VideoSettingsButton} instance.
66
-     *
67
-     * @param {Object} props - The read-only properties with which the new
68
-     * instance is to be initialized.
69
-     */
70
-    constructor(props) {
71
-        super(props);
72
-
73
-        this._isMounted = true;
74
-        this.state = {
75
-            hasPermissions: false
76
-        };
77
-    }
50
+class VideoSettingsButton extends Component<Props> {
78 51
 
79 52
     /**
80 53
      * Returns true if the settings icon is disabled.
@@ -82,53 +55,9 @@ class VideoSettingsButton extends Component<Props, State> {
82 55
      * @returns {boolean}
83 56
      */
84 57
     _isIconDisabled() {
85
-        const { hasVideoTrack, isDisabled } = this.props;
58
+        const { hasPermissions, hasVideoTrack, isDisabled } = this.props;
86 59
 
87
-        return (!this.state.hasPermissions || isDisabled) && !hasVideoTrack;
88
-    }
89
-
90
-    /**
91
-     * Updates device permissions.
92
-     *
93
-     * @returns {Promise<void>}
94
-     */
95
-    async _updatePermissions() {
96
-        const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
97
-            'video',
98
-        );
99
-
100
-        this._isMounted && this.setState({
101
-            hasPermissions
102
-        });
103
-    }
104
-
105
-    /**
106
-     * Implements React's {@link Component#componentDidMount}.
107
-     *
108
-     * @inheritdoc
109
-     */
110
-    componentDidMount() {
111
-        this._updatePermissions();
112
-    }
113
-
114
-    /**
115
-     * Implements React's {@link Component#componentDidUpdate}.
116
-     *
117
-     * @inheritdoc
118
-     */
119
-    componentDidUpdate(prevProps) {
120
-        if (this.props.permissionPromptVisibility !== prevProps.permissionPromptVisibility) {
121
-            this._updatePermissions();
122
-        }
123
-    }
124
-
125
-    /**
126
-     * Implements React's {@link Component#componentWillUnmount}.
127
-     *
128
-     * @inheritdoc
129
-     */
130
-    componentWillUnmount() {
131
-        this._isMounted = false;
60
+        return (!hasPermissions || isDisabled) && !hasVideoTrack;
132 61
     }
133 62
 
134 63
     /**
@@ -159,10 +88,12 @@ class VideoSettingsButton extends Component<Props, State> {
159 88
  * @returns {Object}
160 89
  */
161 90
 function mapStateToProps(state) {
91
+    const { permissions = {} } = state['features/base/devices'];
92
+
162 93
     return {
94
+        hasPermissions: permissions.video,
163 95
         hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
164 96
         isDisabled: isVideoSettingsButtonDisabled(state),
165
-        permissionPromptVisibility: getMediaPermissionPromptVisibility(state),
166 97
         visible: !isMobileBrowser()
167 98
     };
168 99
 }

正在加载...
取消
保存