Parcourir la source

feat(permissions): refactor keeping track of device permissions

Use the W3C Permissions API where possible. At the time of this writing this is
supported in Chrome >= 64.
release-8443
Saúl Ibarra Corretgé il y a 7 ans
Parent
révision
91f15f86ab
5 fichiers modifiés avec 152 ajouts et 111 suppressions
  1. 133
    59
      JitsiMediaDevices.js
  2. 1
    1
      doc/API.md
  3. 0
    7
      modules/RTC/RTC.js
  4. 11
    44
      modules/RTC/RTCUtils.js
  5. 7
    0
      service/RTC/RTCEvents.js

+ 133
- 59
JitsiMediaDevices.js Voir le fichier

8
 
8
 
9
 import * as JitsiMediaDevicesEvents from './JitsiMediaDevicesEvents';
9
 import * as JitsiMediaDevicesEvents from './JitsiMediaDevicesEvents';
10
 
10
 
11
-const eventEmitter = new EventEmitter();
12
-
13
 /**
11
 /**
14
- * Gathers data and sends it to statistics.
15
- * @param deviceID the device id to log
16
- * @param devices list of devices
12
+ * Media devices utilities for Jitsi.
17
  */
13
  */
18
-function logOutputDevice(deviceID, devices) {
19
-    const device
20
-        = devices.find(
21
-            d => d.kind === 'audiooutput' && d.deviceId === deviceID);
22
-
23
-    if (device) {
24
-        Statistics.sendActiveDeviceListEvent(
25
-            RTC.getEventDataForActiveDevice(device));
14
+class JitsiMediaDevices {
15
+    /**
16
+     * Initializes a {@code JitsiMediaDevices} object. There will be a single
17
+     * instance of this class.
18
+     */
19
+    constructor() {
20
+        this._eventEmitter = new EventEmitter();
21
+        this._grantedPermissions = {};
22
+
23
+        RTC.addListener(
24
+            RTCEvents.DEVICE_LIST_CHANGED,
25
+            devices =>
26
+                this._eventEmitter.emit(
27
+                    JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
28
+                    devices));
29
+        RTC.addListener(
30
+            RTCEvents.DEVICE_LIST_AVAILABLE,
31
+            devices =>
32
+                this._logOutputDevice(
33
+                    this.getAudioOutputDevice(),
34
+                    devices));
35
+        RTC.addListener(
36
+            RTCEvents.GRANTED_PERMISSIONS,
37
+            grantedPermissions =>
38
+                this._handleGrantedPermissions(grantedPermissions));
39
+
40
+        // Test if the W3C Permissions API is implemented and the 'camera' and
41
+        // 'microphone' permissions are implemented. (Testing for at least one
42
+        // of them seems sufficient).
43
+        this._permissionsApiSupported = new Promise(resolve => {
44
+            if (!navigator.permissions) {
45
+                resolve(false);
46
+
47
+                return;
48
+            }
49
+
50
+            navigator.permissions.query({ name: 'camera ' })
51
+                .then(() => resolve(true), () => resolve(false));
52
+        });
53
+    }
54
+
55
+    /**
56
+     * Updated the local granted permissions cache. A permissions might be
57
+     * granted, denied, or undefined. This is represented by having its media
58
+     * type key set to {@code true} or {@code false} respectively.
59
+     *
60
+     * @param {Object} grantedPermissions - Array with the permissions
61
+     * which were granted.
62
+     */
63
+    _handleGrantedPermissions(grantedPermissions) {
64
+        this._grantedPermissions = {
65
+            ...this._grantedPermissions,
66
+            ...grantedPermissions
67
+        };
68
+    }
69
+
70
+    /**
71
+     * Gathers data and sends it to statistics.
72
+     * @param deviceID the device id to log
73
+     * @param devices list of devices
74
+     */
75
+    _logOutputDevice(deviceID, devices) {
76
+        const device
77
+            = devices.find(
78
+                d => d.kind === 'audiooutput' && d.deviceId === deviceID);
79
+
80
+        if (device) {
81
+            Statistics.sendActiveDeviceListEvent(
82
+                RTC.getEventDataForActiveDevice(device));
83
+        }
26
     }
84
     }
27
-}
28
 
85
 
29
-const JitsiMediaDevices = {
30
     /**
86
     /**
31
      * Executes callback with list of media devices connected.
87
      * Executes callback with list of media devices connected.
32
      * @param {function} callback
88
      * @param {function} callback
33
      */
89
      */
34
     enumerateDevices(callback) {
90
     enumerateDevices(callback) {
35
         RTC.enumerateDevices(callback);
91
         RTC.enumerateDevices(callback);
36
-    },
92
+    }
37
 
93
 
38
     /**
94
     /**
39
      * Checks if its possible to enumerate available cameras/micropones.
95
      * Checks if its possible to enumerate available cameras/micropones.
43
      */
99
      */
44
     isDeviceListAvailable() {
100
     isDeviceListAvailable() {
45
         return RTC.isDeviceListAvailable();
101
         return RTC.isDeviceListAvailable();
46
-    },
102
+    }
47
 
103
 
48
     /**
104
     /**
49
      * Returns true if changing the input (camera / microphone) or output
105
      * Returns true if changing the input (camera / microphone) or output
54
      */
110
      */
55
     isDeviceChangeAvailable(deviceType) {
111
     isDeviceChangeAvailable(deviceType) {
56
         return RTC.isDeviceChangeAvailable(deviceType);
112
         return RTC.isDeviceChangeAvailable(deviceType);
57
-    },
113
+    }
58
 
114
 
59
     /**
115
     /**
60
-     * Returns true if user granted permission to media devices.
116
+     * Checks if the permission for the given device was granted.
117
+     *
61
      * @param {'audio'|'video'} [type] - type of devices to check,
118
      * @param {'audio'|'video'} [type] - type of devices to check,
62
      *      undefined stands for both 'audio' and 'video' together
119
      *      undefined stands for both 'audio' and 'video' together
63
-     * @returns {boolean}
120
+     * @returns {Promise<boolean>}
64
      */
121
      */
65
     isDevicePermissionGranted(type) {
122
     isDevicePermissionGranted(type) {
66
-        const permissions = RTC.getDeviceAvailability();
67
-
68
-        switch (type) {
69
-        case MediaType.VIDEO:
70
-            return permissions.video === true;
71
-        case MediaType.AUDIO:
72
-            return permissions.audio === true;
73
-        default:
74
-            return permissions.video === true && permissions.audio === true;
75
-        }
76
-    },
123
+        return new Promise(resolve => {
124
+            // Shortcut: first check if we already know the permission was
125
+            // granted.
126
+            if (type in this._grantedPermissions) {
127
+                resolve(this._grantedPermissions[type]);
128
+
129
+                return;
130
+            }
131
+
132
+            // Check using the Permissions API.
133
+            this._permissionsApiSupported.then(supported => {
134
+                if (!supported) {
135
+                    resolve(false);
136
+
137
+                    return;
138
+                }
139
+
140
+                const promises = [];
141
+
142
+                switch (type) {
143
+                case MediaType.VIDEO:
144
+                    promises.push(
145
+                        navigator.permissions.query({ name: 'camera' }));
146
+                    break;
147
+                case MediaType.AUDIO:
148
+                    promises.push(
149
+                        navigator.permissions.query({ name: 'microphone' }));
150
+                    break;
151
+                default:
152
+                    promises.push(
153
+                        navigator.permissions.query({ name: 'camera' }));
154
+                    promises.push(
155
+                        navigator.permissions.query({ name: 'microphone' }));
156
+                }
157
+
158
+                Promise.all(promises).then(
159
+                    r => resolve(r.every(Boolean)),
160
+                    () => resolve(false)
161
+                );
162
+            });
163
+        });
164
+    }
77
 
165
 
78
     /**
166
     /**
79
      * Returns true if it is possible to be simultaneously capturing audio
167
      * Returns true if it is possible to be simultaneously capturing audio
83
      */
171
      */
84
     isMultipleAudioInputSupported() {
172
     isMultipleAudioInputSupported() {
85
         return !browser.isFirefox();
173
         return !browser.isFirefox();
86
-    },
174
+    }
87
 
175
 
88
     /**
176
     /**
89
      * Returns currently used audio output device id, 'default' stands
177
      * Returns currently used audio output device id, 'default' stands
92
      */
180
      */
93
     getAudioOutputDevice() {
181
     getAudioOutputDevice() {
94
         return RTC.getAudioOutputDevice();
182
         return RTC.getAudioOutputDevice();
95
-    },
183
+    }
96
 
184
 
97
     /**
185
     /**
98
      * Sets current audio output device.
186
      * Sets current audio output device.
103
      *      otherwise
191
      *      otherwise
104
      */
192
      */
105
     setAudioOutputDevice(deviceId) {
193
     setAudioOutputDevice(deviceId) {
106
-
107
         const availableDevices = RTC.getCurrentlyAvailableMediaDevices();
194
         const availableDevices = RTC.getCurrentlyAvailableMediaDevices();
108
 
195
 
109
         if (availableDevices && availableDevices.length > 0) {
196
         if (availableDevices && availableDevices.length > 0) {
110
             // if we have devices info report device to stats
197
             // if we have devices info report device to stats
111
             // normally this will not happen on startup as this method is called
198
             // normally this will not happen on startup as this method is called
112
             // too early. This will happen only on user selection of new device
199
             // too early. This will happen only on user selection of new device
113
-            logOutputDevice(deviceId, RTC.getCurrentlyAvailableMediaDevices());
200
+            this._logOutputDevice(
201
+                deviceId, RTC.getCurrentlyAvailableMediaDevices());
114
         }
202
         }
115
 
203
 
116
         return RTC.setAudioOutputDevice(deviceId);
204
         return RTC.setAudioOutputDevice(deviceId);
117
-    },
205
+    }
118
 
206
 
119
     /**
207
     /**
120
      * Adds an event handler.
208
      * Adds an event handler.
122
      * @param {function} handler - event handler
210
      * @param {function} handler - event handler
123
      */
211
      */
124
     addEventListener(event, handler) {
212
     addEventListener(event, handler) {
125
-        eventEmitter.addListener(event, handler);
126
-    },
213
+        this._eventEmitter.addListener(event, handler);
214
+    }
127
 
215
 
128
     /**
216
     /**
129
      * Removes event handler.
217
      * Removes event handler.
131
      * @param {function} handler - event handler
219
      * @param {function} handler - event handler
132
      */
220
      */
133
     removeEventListener(event, handler) {
221
     removeEventListener(event, handler) {
134
-        eventEmitter.removeListener(event, handler);
135
-    },
222
+        this._eventEmitter.removeListener(event, handler);
223
+    }
136
 
224
 
137
     /**
225
     /**
138
      * Emits an event.
226
      * Emits an event.
139
      * @param {string} event - event name
227
      * @param {string} event - event name
140
      */
228
      */
141
     emitEvent(event, ...args) {
229
     emitEvent(event, ...args) {
142
-        eventEmitter.emit(event, ...args);
143
-    },
230
+        this._eventEmitter.emit(event, ...args);
231
+    }
144
 
232
 
145
     /**
233
     /**
146
      * Returns whether or not the current browser can support capturing video,
234
      * Returns whether or not the current browser can support capturing video,
154
         // JitsiMediaDevices.
242
         // JitsiMediaDevices.
155
         return browser.supportsVideo();
243
         return browser.supportsVideo();
156
     }
244
     }
157
-};
158
-
159
-
160
-RTC.addListener(
161
-    RTCEvents.DEVICE_LIST_CHANGED,
162
-    devices =>
163
-        eventEmitter.emit(
164
-            JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
165
-            devices));
166
-RTC.addListener(
167
-    RTCEvents.DEVICE_LIST_AVAILABLE,
168
-    devices =>
169
-        logOutputDevice(
170
-            JitsiMediaDevices.getAudioOutputDevice(),
171
-            devices));
172
-
173
-export default JitsiMediaDevices;
245
+}
246
+
247
+export default new JitsiMediaDevices();

+ 1
- 1
doc/API.md Voir le fichier

88
         - groupId - group identifier, two devices have the same group identifier if they belong to the same physical device; for example a monitor with both a built-in camera and microphone
88
         - groupId - group identifier, two devices have the same group identifier if they belong to the same physical device; for example a monitor with both a built-in camera and microphone
89
     - ```setAudioOutputDevice(deviceId)``` - sets current audio output device. ```deviceId``` - id of 'audiooutput' device from ```JitsiMeetJS.enumerateDevices()```, '' is for default device.
89
     - ```setAudioOutputDevice(deviceId)``` - sets current audio output device. ```deviceId``` - id of 'audiooutput' device from ```JitsiMeetJS.enumerateDevices()```, '' is for default device.
90
     - ```getAudioOutputDevice()``` - returns currently used audio output device id, '' stands for default device.
90
     - ```getAudioOutputDevice()``` - returns currently used audio output device id, '' stands for default device.
91
-    - ```isDevicePermissionGranted(type)``` - returns true if user granted permission to media devices. ```type``` - 'audio', 'video' or ```undefined```. In case of ```undefined``` will check if both audio and video permissions were granted.
91
+    - ```isDevicePermissionGranted(type)``` - returns a Promise which resolves to true if user granted permission to media devices. ```type``` - 'audio', 'video' or ```undefined```. In case of ```undefined``` will check if both audio and video permissions were granted.
92
     - ```addEventListener(event, handler)``` - attaches an event handler.
92
     - ```addEventListener(event, handler)``` - attaches an event handler.
93
     - ```removeEventListener(event, handler)``` - removes an event handler.
93
     - ```removeEventListener(event, handler)``` - removes an event handler.
94
 
94
 

+ 0
- 7
modules/RTC/RTC.js Voir le fichier

416
         return RTCUtils.init(this.options);
416
         return RTCUtils.init(this.options);
417
     }
417
     }
418
 
418
 
419
-    /**
420
-     *
421
-     */
422
-    static getDeviceAvailability() {
423
-        return RTCUtils.getDeviceAvailability();
424
-    }
425
-
426
     /* eslint-disable max-params */
419
     /* eslint-disable max-params */
427
 
420
 
428
     /**
421
     /**

+ 11
- 44
modules/RTC/RTCUtils.js Voir le fichier

65
     }
65
     }
66
 };
66
 };
67
 
67
 
68
-
69
-// TODO (brian): Move this devices hash, maybe to a model, so RTCUtils remains
70
-// stateless.
71
-const devices = {
72
-    audio: false,
73
-    video: false
74
-};
75
-
76
 /**
68
 /**
77
  * The default frame rate for Screen Sharing.
69
  * The default frame rate for Screen Sharing.
78
  */
70
  */
496
 }
488
 }
497
 
489
 
498
 /**
490
 /**
499
- * Sets the availbale devices based on the options we requested and the
491
+ * Updates the granted permissions based on the options we requested and the
500
  * streams we received.
492
  * streams we received.
501
  * @param um the options we requested to getUserMedia.
493
  * @param um the options we requested to getUserMedia.
502
  * @param stream the stream we received from calling getUserMedia.
494
  * @param stream the stream we received from calling getUserMedia.
503
  */
495
  */
504
-function setAvailableDevices(um, stream) {
496
+function updateGrantedPermissions(um, stream) {
505
     const audioTracksReceived = stream && stream.getAudioTracks().length > 0;
497
     const audioTracksReceived = stream && stream.getAudioTracks().length > 0;
506
     const videoTracksReceived = stream && stream.getVideoTracks().length > 0;
498
     const videoTracksReceived = stream && stream.getVideoTracks().length > 0;
499
+    const grantedPermissions = {};
507
 
500
 
508
     if (um.indexOf('video') !== -1) {
501
     if (um.indexOf('video') !== -1) {
509
-        devices.video = videoTracksReceived;
502
+        grantedPermissions.video = videoTracksReceived;
510
     }
503
     }
511
     if (um.indexOf('audio') !== -1) {
504
     if (um.indexOf('audio') !== -1) {
512
-        devices.audio = audioTracksReceived;
505
+        grantedPermissions.audio = audioTracksReceived;
513
     }
506
     }
507
+
508
+    eventEmitter.emit(RTCEvents.GRANTED_PERMISSIONS, grantedPermissions);
514
 }
509
 }
515
 
510
 
516
 /**
511
 /**
594
 
589
 
595
     sendDeviceListToAnalytics(availableDevices);
590
     sendDeviceListToAnalytics(availableDevices);
596
 
591
 
597
-    const videoInputDevices
598
-        = availableDevices.filter(d => d.kind === 'videoinput');
599
-    const audioInputDevices
600
-        = availableDevices.filter(d => d.kind === 'audioinput');
601
-    const videoInputDevicesWithEmptyLabels
602
-        = videoInputDevices.filter(d => d.label === '');
603
-    const audioInputDevicesWithEmptyLabels
604
-        = audioInputDevices.filter(d => d.label === '');
605
-
606
-    if (videoInputDevices.length
607
-            && videoInputDevices.length
608
-                === videoInputDevicesWithEmptyLabels.length) {
609
-        devices.video = false;
610
-    }
611
-
612
-    if (audioInputDevices.length
613
-            && audioInputDevices.length
614
-                === audioInputDevicesWithEmptyLabels.length) {
615
-        devices.audio = false;
616
-    }
617
-
618
     eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devicesReceived);
592
     eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devicesReceived);
619
 }
593
 }
620
 
594
 
1009
             navigator.mediaDevices.getUserMedia(constraints)
983
             navigator.mediaDevices.getUserMedia(constraints)
1010
                 .then(stream => {
984
                 .then(stream => {
1011
                     logger.log('onUserMediaSuccess');
985
                     logger.log('onUserMediaSuccess');
1012
-                    setAvailableDevices(um, stream);
986
+                    updateGrantedPermissions(um, stream);
1013
                     resolve(stream);
987
                     resolve(stream);
1014
                 })
988
                 })
1015
                 .catch(error => {
989
                 .catch(error => {
1016
                     logger.warn('Failed to get access to local media. '
990
                     logger.warn('Failed to get access to local media. '
1017
                         + ` ${error} ${constraints} `);
991
                         + ` ${error} ${constraints} `);
1018
-                    setAvailableDevices(um, undefined);
992
+                    updateGrantedPermissions(um, undefined);
1019
                     reject(new JitsiTrackError(error, constraints, um));
993
                     reject(new JitsiTrackError(error, constraints, um));
1020
                 });
994
                 });
1021
         });
995
         });
1034
             navigator.mediaDevices.getUserMedia(constraints)
1008
             navigator.mediaDevices.getUserMedia(constraints)
1035
                 .then(stream => {
1009
                 .then(stream => {
1036
                     logger.log('onUserMediaSuccess');
1010
                     logger.log('onUserMediaSuccess');
1037
-                    setAvailableDevices(umDevices, stream);
1011
+                    updateGrantedPermissions(umDevices, stream);
1038
                     resolve(stream);
1012
                     resolve(stream);
1039
                 })
1013
                 })
1040
                 .catch(error => {
1014
                 .catch(error => {
1041
                     logger.warn('Failed to get access to local media. '
1015
                     logger.warn('Failed to get access to local media. '
1042
                         + ` ${error} ${constraints} `);
1016
                         + ` ${error} ${constraints} `);
1043
-                    setAvailableDevices(umDevices, undefined);
1017
+                    updateGrantedPermissions(umDevices, undefined);
1044
                     reject(new JitsiTrackError(error, constraints, umDevices));
1018
                     reject(new JitsiTrackError(error, constraints, umDevices));
1045
                 });
1019
                 });
1046
         });
1020
         });
1426
             .then(() => mediaStreamsMetaData);
1400
             .then(() => mediaStreamsMetaData);
1427
     }
1401
     }
1428
 
1402
 
1429
-    /**
1430
-     *
1431
-     */
1432
-    getDeviceAvailability() {
1433
-        return devices;
1434
-    }
1435
-
1436
     /**
1403
     /**
1437
      * Checks whether it is possible to enumerate available cameras/microphones.
1404
      * Checks whether it is possible to enumerate available cameras/microphones.
1438
      *
1405
      *

+ 7
- 0
service/RTC/RTCEvents.js Voir le fichier

13
     DOMINANT_SPEAKER_CHANGED: 'rtc.dominant_speaker_changed',
13
     DOMINANT_SPEAKER_CHANGED: 'rtc.dominant_speaker_changed',
14
     LASTN_ENDPOINT_CHANGED: 'rtc.lastn_endpoint_changed',
14
     LASTN_ENDPOINT_CHANGED: 'rtc.lastn_endpoint_changed',
15
 
15
 
16
+    /**
17
+     * Event emitted when the user granted a permission for the camera / mic.
18
+     * Used to keep track of the granted permissions on browsers which don't
19
+     * support the Permissions API.
20
+     */
21
+    GRANTED_PERMISSIONS: 'rtc.granted_permissions',
22
+
16
     IS_SELECTED_CHANGED: 'rtc.is_selected_change',
23
     IS_SELECTED_CHANGED: 'rtc.is_selected_change',
17
 
24
 
18
     /**
25
     /**

Chargement…
Annuler
Enregistrer