Переглянути джерело

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é 7 роки тому
джерело
коміт
91f15f86ab
5 змінених файлів з 152 додано та 111 видалено
  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 Переглянути файл

@@ -8,32 +8,88 @@ import Statistics from './modules/statistics/statistics';
8 8
 
9 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 87
      * Executes callback with list of media devices connected.
32 88
      * @param {function} callback
33 89
      */
34 90
     enumerateDevices(callback) {
35 91
         RTC.enumerateDevices(callback);
36
-    },
92
+    }
37 93
 
38 94
     /**
39 95
      * Checks if its possible to enumerate available cameras/micropones.
@@ -43,7 +99,7 @@ const JitsiMediaDevices = {
43 99
      */
44 100
     isDeviceListAvailable() {
45 101
         return RTC.isDeviceListAvailable();
46
-    },
102
+    }
47 103
 
48 104
     /**
49 105
      * Returns true if changing the input (camera / microphone) or output
@@ -54,26 +110,58 @@ const JitsiMediaDevices = {
54 110
      */
55 111
     isDeviceChangeAvailable(deviceType) {
56 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 118
      * @param {'audio'|'video'} [type] - type of devices to check,
62 119
      *      undefined stands for both 'audio' and 'video' together
63
-     * @returns {boolean}
120
+     * @returns {Promise<boolean>}
64 121
      */
65 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 167
      * Returns true if it is possible to be simultaneously capturing audio
@@ -83,7 +171,7 @@ const JitsiMediaDevices = {
83 171
      */
84 172
     isMultipleAudioInputSupported() {
85 173
         return !browser.isFirefox();
86
-    },
174
+    }
87 175
 
88 176
     /**
89 177
      * Returns currently used audio output device id, 'default' stands
@@ -92,7 +180,7 @@ const JitsiMediaDevices = {
92 180
      */
93 181
     getAudioOutputDevice() {
94 182
         return RTC.getAudioOutputDevice();
95
-    },
183
+    }
96 184
 
97 185
     /**
98 186
      * Sets current audio output device.
@@ -103,18 +191,18 @@ const JitsiMediaDevices = {
103 191
      *      otherwise
104 192
      */
105 193
     setAudioOutputDevice(deviceId) {
106
-
107 194
         const availableDevices = RTC.getCurrentlyAvailableMediaDevices();
108 195
 
109 196
         if (availableDevices && availableDevices.length > 0) {
110 197
             // if we have devices info report device to stats
111 198
             // normally this will not happen on startup as this method is called
112 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 204
         return RTC.setAudioOutputDevice(deviceId);
117
-    },
205
+    }
118 206
 
119 207
     /**
120 208
      * Adds an event handler.
@@ -122,8 +210,8 @@ const JitsiMediaDevices = {
122 210
      * @param {function} handler - event handler
123 211
      */
124 212
     addEventListener(event, handler) {
125
-        eventEmitter.addListener(event, handler);
126
-    },
213
+        this._eventEmitter.addListener(event, handler);
214
+    }
127 215
 
128 216
     /**
129 217
      * Removes event handler.
@@ -131,16 +219,16 @@ const JitsiMediaDevices = {
131 219
      * @param {function} handler - event handler
132 220
      */
133 221
     removeEventListener(event, handler) {
134
-        eventEmitter.removeListener(event, handler);
135
-    },
222
+        this._eventEmitter.removeListener(event, handler);
223
+    }
136 224
 
137 225
     /**
138 226
      * Emits an event.
139 227
      * @param {string} event - event name
140 228
      */
141 229
     emitEvent(event, ...args) {
142
-        eventEmitter.emit(event, ...args);
143
-    },
230
+        this._eventEmitter.emit(event, ...args);
231
+    }
144 232
 
145 233
     /**
146 234
      * Returns whether or not the current browser can support capturing video,
@@ -154,20 +242,6 @@ const JitsiMediaDevices = {
154 242
         // JitsiMediaDevices.
155 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 Переглянути файл

@@ -88,7 +88,7 @@ JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
88 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 89
     - ```setAudioOutputDevice(deviceId)``` - sets current audio output device. ```deviceId``` - id of 'audiooutput' device from ```JitsiMeetJS.enumerateDevices()```, '' is for default device.
90 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 92
     - ```addEventListener(event, handler)``` - attaches an event handler.
93 93
     - ```removeEventListener(event, handler)``` - removes an event handler.
94 94
 

+ 0
- 7
modules/RTC/RTC.js Переглянути файл

@@ -416,13 +416,6 @@ export default class RTC extends Listenable {
416 416
         return RTCUtils.init(this.options);
417 417
     }
418 418
 
419
-    /**
420
-     *
421
-     */
422
-    static getDeviceAvailability() {
423
-        return RTCUtils.getDeviceAvailability();
424
-    }
425
-
426 419
     /* eslint-disable max-params */
427 420
 
428 421
     /**

+ 11
- 44
modules/RTC/RTCUtils.js Переглянути файл

@@ -65,14 +65,6 @@ const DEFAULT_CONSTRAINTS = {
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 69
  * The default frame rate for Screen Sharing.
78 70
  */
@@ -496,21 +488,24 @@ function getTrackSSConstraints(options = {}) {
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 492
  * streams we received.
501 493
  * @param um the options we requested to getUserMedia.
502 494
  * @param stream the stream we received from calling getUserMedia.
503 495
  */
504
-function setAvailableDevices(um, stream) {
496
+function updateGrantedPermissions(um, stream) {
505 497
     const audioTracksReceived = stream && stream.getAudioTracks().length > 0;
506 498
     const videoTracksReceived = stream && stream.getVideoTracks().length > 0;
499
+    const grantedPermissions = {};
507 500
 
508 501
     if (um.indexOf('video') !== -1) {
509
-        devices.video = videoTracksReceived;
502
+        grantedPermissions.video = videoTracksReceived;
510 503
     }
511 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,27 +589,6 @@ function onMediaDevicesListChanged(devicesReceived) {
594 589
 
595 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 592
     eventEmitter.emit(RTCEvents.DEVICE_LIST_CHANGED, devicesReceived);
619 593
 }
620 594
 
@@ -1009,13 +983,13 @@ class RTCUtils extends Listenable {
1009 983
             navigator.mediaDevices.getUserMedia(constraints)
1010 984
                 .then(stream => {
1011 985
                     logger.log('onUserMediaSuccess');
1012
-                    setAvailableDevices(um, stream);
986
+                    updateGrantedPermissions(um, stream);
1013 987
                     resolve(stream);
1014 988
                 })
1015 989
                 .catch(error => {
1016 990
                     logger.warn('Failed to get access to local media. '
1017 991
                         + ` ${error} ${constraints} `);
1018
-                    setAvailableDevices(um, undefined);
992
+                    updateGrantedPermissions(um, undefined);
1019 993
                     reject(new JitsiTrackError(error, constraints, um));
1020 994
                 });
1021 995
         });
@@ -1034,13 +1008,13 @@ class RTCUtils extends Listenable {
1034 1008
             navigator.mediaDevices.getUserMedia(constraints)
1035 1009
                 .then(stream => {
1036 1010
                     logger.log('onUserMediaSuccess');
1037
-                    setAvailableDevices(umDevices, stream);
1011
+                    updateGrantedPermissions(umDevices, stream);
1038 1012
                     resolve(stream);
1039 1013
                 })
1040 1014
                 .catch(error => {
1041 1015
                     logger.warn('Failed to get access to local media. '
1042 1016
                         + ` ${error} ${constraints} `);
1043
-                    setAvailableDevices(umDevices, undefined);
1017
+                    updateGrantedPermissions(umDevices, undefined);
1044 1018
                     reject(new JitsiTrackError(error, constraints, umDevices));
1045 1019
                 });
1046 1020
         });
@@ -1426,13 +1400,6 @@ class RTCUtils extends Listenable {
1426 1400
             .then(() => mediaStreamsMetaData);
1427 1401
     }
1428 1402
 
1429
-    /**
1430
-     *
1431
-     */
1432
-    getDeviceAvailability() {
1433
-        return devices;
1434
-    }
1435
-
1436 1403
     /**
1437 1404
      * Checks whether it is possible to enumerate available cameras/microphones.
1438 1405
      *

+ 7
- 0
service/RTC/RTCEvents.js Переглянути файл

@@ -13,6 +13,13 @@ const RTCEvents = {
13 13
     DOMINANT_SPEAKER_CHANGED: 'rtc.dominant_speaker_changed',
14 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 23
     IS_SELECTED_CHANGED: 'rtc.is_selected_change',
17 24
 
18 25
     /**

Завантаження…
Відмінити
Зберегти