|
@@ -21,7 +21,7 @@ class JitsiMediaDevices {
|
21
|
21
|
*/
|
22
|
22
|
constructor() {
|
23
|
23
|
this._eventEmitter = new EventEmitter();
|
24
|
|
- this._grantedPermissions = {};
|
|
24
|
+ this._permissions = {};
|
25
|
25
|
|
26
|
26
|
RTC.addListener(
|
27
|
27
|
RTCEvents.DEVICE_LIST_CHANGED,
|
|
@@ -35,14 +35,18 @@ class JitsiMediaDevices {
|
35
|
35
|
this._logOutputDevice(
|
36
|
36
|
this.getAudioOutputDevice(),
|
37
|
37
|
devices));
|
|
38
|
+
|
|
39
|
+ // We would still want to update the permissions cache in case the permissions API is not supported.
|
38
|
40
|
RTC.addListener(
|
39
|
|
- RTCEvents.GRANTED_PERMISSIONS,
|
40
|
|
- grantedPermissions =>
|
41
|
|
- this._handleGrantedPermissions(grantedPermissions));
|
|
41
|
+ RTCEvents.PERMISSIONS_CHANGED,
|
|
42
|
+ permissions => this._handlePermissionsChange(permissions));
|
42
|
43
|
|
43
|
|
- // Test if the W3C Permissions API is implemented and the 'camera' and
|
44
|
|
- // 'microphone' permissions are implemented. (Testing for at least one
|
45
|
|
- // of them seems sufficient).
|
|
44
|
+ // Test if the W3C Permissions API is implemented and the 'camera' and 'microphone' permissions are
|
|
45
|
+ // implemented. If supported add onchange listeners.
|
|
46
|
+ //
|
|
47
|
+ // NOTE: We don't cache the result for the query because this can potentialy lead to outdated result returned
|
|
48
|
+ // from isDevicePermissionGranted method ( for the time period before the first GUM has been resolved) if the
|
|
49
|
+ // onchange handler is not working.
|
46
|
50
|
this._permissionsApiSupported = new Promise(resolve => {
|
47
|
51
|
if (!navigator.permissions) {
|
48
|
52
|
resolve(false);
|
|
@@ -50,24 +54,94 @@ class JitsiMediaDevices {
|
50
|
54
|
return;
|
51
|
55
|
}
|
52
|
56
|
|
53
|
|
- navigator.permissions.query({ name: VIDEO_PERMISSION_NAME })
|
54
|
|
- .then(() => resolve(true), () => resolve(false));
|
|
57
|
+ const self = this;
|
|
58
|
+
|
|
59
|
+ const promises = [];
|
|
60
|
+
|
|
61
|
+ promises.push(navigator.permissions.query({ name: VIDEO_PERMISSION_NAME })
|
|
62
|
+ .then(status => {
|
|
63
|
+ status.onchange = function() {
|
|
64
|
+ try {
|
|
65
|
+ self._handlePermissionsChange({
|
|
66
|
+ [MediaType.VIDEO]: self._parsePermissionState(this)
|
|
67
|
+ });
|
|
68
|
+ } catch (error) {
|
|
69
|
+ // Nothing to do.
|
|
70
|
+ }
|
|
71
|
+ };
|
|
72
|
+
|
|
73
|
+ return true;
|
|
74
|
+ })
|
|
75
|
+ .catch(() => false));
|
|
76
|
+
|
|
77
|
+ promises.push(navigator.permissions.query({ name: AUDIO_PERMISSION_NAME })
|
|
78
|
+ .then(status => {
|
|
79
|
+ status.onchange = function() {
|
|
80
|
+ try {
|
|
81
|
+ self._handlePermissionsChange({
|
|
82
|
+ [MediaType.AUDIO]: self._parsePermissionState(this)
|
|
83
|
+ });
|
|
84
|
+ } catch (error) {
|
|
85
|
+ // Nothing to do.
|
|
86
|
+ }
|
|
87
|
+ };
|
|
88
|
+
|
|
89
|
+ return true;
|
|
90
|
+ })
|
|
91
|
+ .catch(() => false));
|
|
92
|
+
|
|
93
|
+ Promise.all(promises).then(results => resolve(results.every(supported => supported)));
|
|
94
|
+
|
55
|
95
|
});
|
56
|
96
|
}
|
57
|
97
|
|
|
98
|
+
|
58
|
99
|
/**
|
59
|
|
- * Updated the local granted permissions cache. A permissions might be
|
|
100
|
+ * Parses a PermissionState object and returns true for granted and false otherwise.
|
|
101
|
+ *
|
|
102
|
+ * @param {PermissionState} permissionStatus - The PermissionState object retrieved from the Permissions API.
|
|
103
|
+ * @returns {boolean} - True for granted and false for denied.
|
|
104
|
+ * @throws {TypeError}
|
|
105
|
+ */
|
|
106
|
+ _parsePermissionState(permissionStatus = {}) {
|
|
107
|
+ // The status attribute is deprecated, and state
|
|
108
|
+ // should be used instead, but check both for now
|
|
109
|
+ // for backwards compatibility.
|
|
110
|
+ const status = permissionStatus.state || permissionStatus.status;
|
|
111
|
+
|
|
112
|
+ if (typeof status !== 'string') {
|
|
113
|
+ throw new TypeError();
|
|
114
|
+ }
|
|
115
|
+
|
|
116
|
+ return status === PERMISSION_GRANTED_STATUS;
|
|
117
|
+ }
|
|
118
|
+
|
|
119
|
+ /**
|
|
120
|
+ * Updates the local granted/denied permissions cache. A permissions might be
|
60
|
121
|
* granted, denied, or undefined. This is represented by having its media
|
61
|
122
|
* type key set to {@code true} or {@code false} respectively.
|
62
|
123
|
*
|
63
|
|
- * @param {Object} grantedPermissions - Array with the permissions
|
64
|
|
- * which were granted.
|
|
124
|
+ * @param {Object} permissions - Object with the permissions.
|
65
|
125
|
*/
|
66
|
|
- _handleGrantedPermissions(grantedPermissions) {
|
67
|
|
- this._grantedPermissions = {
|
68
|
|
- ...this._grantedPermissions,
|
69
|
|
- ...grantedPermissions
|
70
|
|
- };
|
|
126
|
+ _handlePermissionsChange(permissions) {
|
|
127
|
+ const hasPermissionsChanged
|
|
128
|
+ = [ MediaType.AUDIO, MediaType.VIDEO ]
|
|
129
|
+ .some(type => type in permissions && permissions[type] !== this._permissions[type]);
|
|
130
|
+
|
|
131
|
+ if (hasPermissionsChanged) {
|
|
132
|
+ this._permissions = {
|
|
133
|
+ ...this._permissions,
|
|
134
|
+ ...permissions
|
|
135
|
+ };
|
|
136
|
+ this._eventEmitter.emit(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, this._permissions);
|
|
137
|
+
|
|
138
|
+ if (this._permissions[MediaType.AUDIO] || this._permissions[MediaType.VIDEO]) {
|
|
139
|
+ // Triggering device list update when the permissiions are granted in order to update
|
|
140
|
+ // the labels the devices.
|
|
141
|
+ // eslint-disable-next-line no-empty-function
|
|
142
|
+ this.enumerateDevices(() => {});
|
|
143
|
+ }
|
|
144
|
+ }
|
71
|
145
|
}
|
72
|
146
|
|
73
|
147
|
/**
|
|
@@ -121,13 +195,17 @@ class JitsiMediaDevices {
|
121
|
195
|
* @param {'audio'|'video'} [type] - type of devices to check,
|
122
|
196
|
* undefined stands for both 'audio' and 'video' together
|
123
|
197
|
* @returns {Promise<boolean>}
|
|
198
|
+ *
|
|
199
|
+ * NOTE: We don't cache the result from the query because this can potentialy lead to outdated result returned
|
|
200
|
+ * from this method ( for the time period before the first GUM has been resolved) if the onchange handler is not
|
|
201
|
+ * working.
|
124
|
202
|
*/
|
125
|
203
|
isDevicePermissionGranted(type) {
|
126
|
204
|
return new Promise(resolve => {
|
127
|
205
|
// Shortcut: first check if we already know the permission was
|
128
|
206
|
// granted.
|
129
|
|
- if (type in this._grantedPermissions) {
|
130
|
|
- resolve(this._grantedPermissions[type]);
|
|
207
|
+ if (type in this._permissions) {
|
|
208
|
+ resolve(this._permissions[type]);
|
131
|
209
|
|
132
|
210
|
return;
|
133
|
211
|
}
|
|
@@ -135,14 +213,6 @@ class JitsiMediaDevices {
|
135
|
213
|
// Check using the Permissions API.
|
136
|
214
|
this._permissionsApiSupported.then(supported => {
|
137
|
215
|
if (!supported) {
|
138
|
|
- // Workaround on Safari for audio input device
|
139
|
|
- // selection to work. Safari doesn't support the
|
140
|
|
- // permissions query.
|
141
|
|
- if (browser.isSafari()) {
|
142
|
|
- resolve(true);
|
143
|
|
-
|
144
|
|
- return;
|
145
|
|
- }
|
146
|
216
|
resolve(false);
|
147
|
217
|
|
148
|
218
|
return;
|
|
@@ -176,13 +246,11 @@ class JitsiMediaDevices {
|
176
|
246
|
|
177
|
247
|
Promise.all(promises).then(
|
178
|
248
|
results => resolve(results.every(permissionStatus => {
|
179
|
|
- // The status attribute is deprecated, and state
|
180
|
|
- // should be used instead, but check both for now
|
181
|
|
- // for backwards compatibility.
|
182
|
|
- const grantStatus = permissionStatus.state
|
183
|
|
- || permissionStatus.status;
|
184
|
|
-
|
185
|
|
- return grantStatus === PERMISSION_GRANTED_STATUS;
|
|
249
|
+ try {
|
|
250
|
+ return this._parsePermissionState(permissionStatus);
|
|
251
|
+ } catch {
|
|
252
|
+ return false;
|
|
253
|
+ }
|
186
|
254
|
})),
|
187
|
255
|
() => resolve(false)
|
188
|
256
|
);
|