|
@@ -46,6 +46,36 @@ const eventEmitter = new EventEmitter();
|
46
|
46
|
|
47
|
47
|
const AVAILABLE_DEVICES_POLL_INTERVAL_TIME = 3000; // ms
|
48
|
48
|
|
|
49
|
+/**
|
|
50
|
+ * Default resolution to obtain for video tracks if no resolution is specified.
|
|
51
|
+ * This default is used for old gum flow only, as new gum flow uses
|
|
52
|
+ * {@link DEFAULT_CONSTRAINTS}.
|
|
53
|
+ */
|
|
54
|
+const OLD_GUM_DEFAULT_RESOLUTION = 720;
|
|
55
|
+
|
|
56
|
+/**
|
|
57
|
+ * Default devices to obtain when no specific devices are specified. This
|
|
58
|
+ * default is used for old gum flow only.
|
|
59
|
+ */
|
|
60
|
+const OLD_GUM_DEFAULT_DEVICES = [ 'audio', 'video' ];
|
|
61
|
+
|
|
62
|
+/**
|
|
63
|
+ * Default MediaStreamConstraints to use for calls to getUserMedia.
|
|
64
|
+ *
|
|
65
|
+ * @private
|
|
66
|
+ */
|
|
67
|
+const DEFAULT_CONSTRAINTS = {
|
|
68
|
+ video: {
|
|
69
|
+ aspectRatio: 16 / 9,
|
|
70
|
+ height: {
|
|
71
|
+ ideal: 1080,
|
|
72
|
+ max: 1080,
|
|
73
|
+ min: 240
|
|
74
|
+ }
|
|
75
|
+ }
|
|
76
|
+};
|
|
77
|
+
|
|
78
|
+
|
49
|
79
|
// TODO (brian): Move this devices hash, maybe to a model, so RTCUtils remains
|
50
|
80
|
// stateless.
|
51
|
81
|
const devices = {
|
|
@@ -353,22 +383,6 @@ function getConstraints(um, options) {
|
353
|
383
|
return constraints;
|
354
|
384
|
}
|
355
|
385
|
|
356
|
|
-/**
|
357
|
|
- * Default MediaStreamConstraints to use for calls to getUserMedia.
|
358
|
|
- *
|
359
|
|
- * @private
|
360
|
|
- */
|
361
|
|
-const DEFAULT_CONSTRAINTS = {
|
362
|
|
- video: {
|
363
|
|
- aspectRatio: 16 / 9,
|
364
|
|
- height: {
|
365
|
|
- ideal: 1080,
|
366
|
|
- max: 1080,
|
367
|
|
- min: 240
|
368
|
|
- }
|
369
|
|
- }
|
370
|
|
-};
|
371
|
|
-
|
372
|
386
|
/**
|
373
|
387
|
* Creates a constraints object to be passed into a call to getUserMedia.
|
374
|
388
|
*
|
|
@@ -1158,6 +1172,8 @@ class RTCUtils extends Listenable {
|
1158
|
1172
|
* @param {string} options.desktopStream
|
1159
|
1173
|
* @param {string} options.cameraDeviceId
|
1160
|
1174
|
* @param {string} options.micDeviceId
|
|
1175
|
+ * @returns {Promise} Returns a media stream on success or a JitsiTrackError
|
|
1176
|
+ * on failure.
|
1161
|
1177
|
**/
|
1162
|
1178
|
getUserMediaWithConstraints(
|
1163
|
1179
|
um,
|
|
@@ -1168,31 +1184,46 @@ class RTCUtils extends Listenable {
|
1168
|
1184
|
|
1169
|
1185
|
logger.info('Get media constraints', constraints);
|
1170
|
1186
|
|
1171
|
|
- try {
|
1172
|
|
- this.getUserMedia(
|
1173
|
|
- constraints,
|
1174
|
|
- stream => {
|
1175
|
|
- logger.log('onUserMediaSuccess');
|
1176
|
|
- setAvailableDevices(um, stream);
|
1177
|
|
- successCallback(stream);
|
1178
|
|
- },
|
1179
|
|
- error => {
|
1180
|
|
- setAvailableDevices(um, undefined);
|
1181
|
|
- logger.warn('Failed to get access to local media. Error ',
|
1182
|
|
- error, constraints);
|
|
1187
|
+ return new Promise((resolve, reject) => {
|
|
1188
|
+ try {
|
|
1189
|
+ this.getUserMedia(
|
|
1190
|
+ constraints,
|
|
1191
|
+ stream => {
|
|
1192
|
+ logger.log('onUserMediaSuccess');
|
|
1193
|
+ setAvailableDevices(um, stream);
|
1183
|
1194
|
|
1184
|
|
- if (failureCallback) {
|
1185
|
|
- failureCallback(
|
1186
|
|
- new JitsiTrackError(error, constraints, um));
|
1187
|
|
- }
|
1188
|
|
- });
|
1189
|
|
- } catch (e) {
|
1190
|
|
- logger.error('GUM failed: ', e);
|
|
1195
|
+ if (successCallback) {
|
|
1196
|
+ successCallback(stream);
|
|
1197
|
+ }
|
|
1198
|
+
|
|
1199
|
+ resolve(stream);
|
|
1200
|
+ },
|
|
1201
|
+ error => {
|
|
1202
|
+ setAvailableDevices(um, undefined);
|
|
1203
|
+ logger.warn(
|
|
1204
|
+ 'Failed to get access to local media. Error ',
|
|
1205
|
+ error, constraints);
|
|
1206
|
+ const jitsiTrackError
|
|
1207
|
+ = new JitsiTrackError(error, constraints, um);
|
|
1208
|
+
|
|
1209
|
+ if (failureCallback) {
|
|
1210
|
+ failureCallback(jitsiTrackError);
|
|
1211
|
+ }
|
|
1212
|
+
|
|
1213
|
+ reject(jitsiTrackError);
|
|
1214
|
+ });
|
|
1215
|
+ } catch (e) {
|
|
1216
|
+ logger.error('GUM failed: ', e);
|
|
1217
|
+ const jitsiTrackError
|
|
1218
|
+ = new JitsiTrackError(e, constraints, um);
|
1191
|
1219
|
|
1192
|
|
- if (failureCallback) {
|
1193
|
|
- failureCallback(new JitsiTrackError(e, constraints, um));
|
|
1220
|
+ if (failureCallback) {
|
|
1221
|
+ failureCallback(jitsiTrackError);
|
|
1222
|
+ }
|
|
1223
|
+
|
|
1224
|
+ reject(jitsiTrackError);
|
1194
|
1225
|
}
|
1195
|
|
- }
|
|
1226
|
+ });
|
1196
|
1227
|
}
|
1197
|
1228
|
|
1198
|
1229
|
/**
|
|
@@ -1287,169 +1318,175 @@ class RTCUtils extends Listenable {
|
1287
|
1318
|
* @returns {*} Promise object that will receive the new JitsiTracks
|
1288
|
1319
|
*/
|
1289
|
1320
|
obtainAudioAndVideoPermissions(options = {}) {
|
1290
|
|
- const self = this;
|
|
1321
|
+ options.devices = options.devices || [ ...OLD_GUM_DEFAULT_DEVICES ];
|
|
1322
|
+ options.resolution = options.resolution || OLD_GUM_DEFAULT_RESOLUTION;
|
1291
|
1323
|
|
1292
|
|
- const dsOptions = {
|
1293
|
|
- ...options.desktopSharingExtensionExternalInstallation,
|
1294
|
|
- desktopSharingSources: options.desktopSharingSources
|
1295
|
|
- };
|
|
1324
|
+ const requestingDesktop = options.devices.includes('desktop');
|
1296
|
1325
|
|
1297
|
|
- return new Promise((resolve, reject) => {
|
1298
|
|
- const successCallback = function(stream) {
|
1299
|
|
- resolve(handleLocalStream(stream, options.resolution));
|
1300
|
|
- };
|
|
1326
|
+ if (requestingDesktop && !screenObtainer.isSupported()) {
|
|
1327
|
+ return Promise.reject(
|
|
1328
|
+ new Error('Desktop sharing is not supported!'));
|
|
1329
|
+ }
|
1301
|
1330
|
|
1302
|
|
- options.devices = options.devices || [ 'audio', 'video' ];
|
1303
|
|
- options.resolution = options.resolution || '720';
|
|
1331
|
+ let gumPromise;
|
1304
|
1332
|
|
1305
|
|
- if (!screenObtainer.isSupported()
|
1306
|
|
- && options.devices.indexOf('desktop') !== -1) {
|
1307
|
|
- reject(new Error('Desktop sharing is not supported!'));
|
1308
|
|
- }
|
1309
|
|
- if (browser.isFirefox()
|
1310
|
|
-
|
1311
|
|
- // XXX The react-native-webrtc implementation that we
|
1312
|
|
- // utilize on React Native at the time of this writing does
|
1313
|
|
- // not support the MediaStream constructors defined by
|
1314
|
|
- // https://www.w3.org/TR/mediacapture-streams/#constructors
|
1315
|
|
- // and instead has a single constructor which expects (an
|
1316
|
|
- // NSNumber as) a MediaStream ID.
|
1317
|
|
- || browser.isReactNative()
|
1318
|
|
- || browser.isTemasysPluginUsed()) {
|
1319
|
|
- const GUM = function(device, s, e) {
|
1320
|
|
- this.getUserMediaWithConstraints(device, s, e, options);
|
1321
|
|
- };
|
|
1333
|
+ if (browser.supportsMediaStreamConstructor()) {
|
|
1334
|
+ gumPromise = this._getAudioAndVideoStreams(options);
|
|
1335
|
+ } else {
|
|
1336
|
+ // If the MediaStream constructor is not supported, then get tracks
|
|
1337
|
+ // in separate GUM calls in order to keep different tracks separate.
|
|
1338
|
+ gumPromise = this._getAudioAndVideoStreamsSeparately(options);
|
|
1339
|
+ }
|
1322
|
1340
|
|
1323
|
|
- const deviceGUM = {
|
1324
|
|
- 'audio': GUM.bind(self, [ 'audio' ]),
|
1325
|
|
- 'video': GUM.bind(self, [ 'video' ])
|
1326
|
|
- };
|
|
1341
|
+ return gumPromise.then(streams =>
|
|
1342
|
+ handleLocalStream(streams, options.resolution));
|
|
1343
|
+ }
|
1327
|
1344
|
|
1328
|
|
- if (screenObtainer.isSupported()) {
|
1329
|
|
- deviceGUM.desktop = screenObtainer.obtainStream.bind(
|
1330
|
|
- screenObtainer,
|
1331
|
|
- dsOptions);
|
|
1345
|
+ /**
|
|
1346
|
+ * Performs one call to getUserMedia for audio and/or video and another call
|
|
1347
|
+ * for desktop.
|
|
1348
|
+ *
|
|
1349
|
+ * @param {Object} options - An object describing how the gUM request should
|
|
1350
|
+ * be executed. See {@link obtainAudioAndVideoPermissions} for full options.
|
|
1351
|
+ * @returns {*} Promise object that will receive the new JitsiTracks on
|
|
1352
|
+ * success or a JitsiTrackError on failure.
|
|
1353
|
+ */
|
|
1354
|
+ _getAudioAndVideoStreams(options) {
|
|
1355
|
+ const requestingDesktop = options.devices.includes('desktop');
|
|
1356
|
+
|
|
1357
|
+ options.devices = options.devices.filter(device =>
|
|
1358
|
+ device !== 'desktop');
|
|
1359
|
+
|
|
1360
|
+ const gumPromise = options.devices.length
|
|
1361
|
+ ? this.getUserMediaWithConstraints(
|
|
1362
|
+ options.devices, null, null, options)
|
|
1363
|
+ : Promise.resolve(null);
|
|
1364
|
+
|
|
1365
|
+ return gumPromise
|
|
1366
|
+ .then(avStream => {
|
|
1367
|
+ // If any requested devices are missing, call gum again in
|
|
1368
|
+ // an attempt to obtain the actual error. For example, the
|
|
1369
|
+ // requested video device is missing or permission was
|
|
1370
|
+ // denied.
|
|
1371
|
+ const missingTracks
|
|
1372
|
+ = this._getMissingTracks(options.devices, avStream);
|
|
1373
|
+
|
|
1374
|
+ if (missingTracks.length) {
|
|
1375
|
+ this.stopMediaStream(avStream);
|
|
1376
|
+
|
|
1377
|
+ return this.getUserMediaWithConstraints(
|
|
1378
|
+ missingTracks, options)
|
|
1379
|
+
|
|
1380
|
+ // GUM has already failed earlier and this success
|
|
1381
|
+ // handling should not be reached.
|
|
1382
|
+ .then(() => Promise.reject(new JitsiTrackError(
|
|
1383
|
+ { name: 'UnknownError' },
|
|
1384
|
+ getConstraints(options.devices, options),
|
|
1385
|
+ missingTracks)));
|
1332
|
1386
|
}
|
1333
|
1387
|
|
1334
|
|
- // With FF/IE we can't split the stream into audio and video
|
1335
|
|
- // because FF doesn't support media stream constructors. So, we
|
1336
|
|
- // need to get the audio stream separately from the video stream
|
1337
|
|
- // using two distinct GUM calls. Not very user friendly :-( but
|
1338
|
|
- // we don't have many other options neither.
|
1339
|
|
- //
|
1340
|
|
- // Note that we pack those 2 streams in a single object and pass
|
1341
|
|
- // it to the successCallback method.
|
1342
|
|
- obtainDevices({
|
1343
|
|
- devices: options.devices,
|
1344
|
|
- streams: [],
|
1345
|
|
- successCallback,
|
1346
|
|
- errorCallback: reject,
|
1347
|
|
- deviceGUM
|
|
1388
|
+ return avStream;
|
|
1389
|
+ })
|
|
1390
|
+ .then(audioVideo => {
|
|
1391
|
+ if (!requestingDesktop) {
|
|
1392
|
+ return { audioVideo };
|
|
1393
|
+ }
|
|
1394
|
+
|
|
1395
|
+ return new Promise((resolve, reject) => {
|
|
1396
|
+ screenObtainer.obtainStream(
|
|
1397
|
+ this._parseDesktopSharingOptions(options),
|
|
1398
|
+ desktop => resolve({
|
|
1399
|
+ audioVideo,
|
|
1400
|
+ desktop
|
|
1401
|
+ }),
|
|
1402
|
+ error => {
|
|
1403
|
+ if (audioVideo) {
|
|
1404
|
+ this.stopMediaStream(audioVideo);
|
|
1405
|
+ }
|
|
1406
|
+ reject(error);
|
|
1407
|
+ });
|
1348
|
1408
|
});
|
1349
|
|
- } else {
|
1350
|
|
- const hasDesktop = options.devices.indexOf('desktop') > -1;
|
|
1409
|
+ });
|
|
1410
|
+ }
|
1351
|
1411
|
|
1352
|
|
- if (hasDesktop) {
|
1353
|
|
- options.devices.splice(
|
1354
|
|
- options.devices.indexOf('desktop'),
|
1355
|
|
- 1);
|
1356
|
|
- }
|
|
1412
|
+ /**
|
|
1413
|
+ * Private utility for determining if the passed in MediaStream contains
|
|
1414
|
+ * tracks of the type(s) specified in the requested devices.
|
|
1415
|
+ *
|
|
1416
|
+ * @param {string[]} requestedDevices - The track types that are expected to
|
|
1417
|
+ * be includes in the stream.
|
|
1418
|
+ * @param {MediaStream} stream - The MediaStream to check if it has the
|
|
1419
|
+ * expected track types.
|
|
1420
|
+ * @returns {string[]} An array of string with the missing track types. The
|
|
1421
|
+ * array will be empty if all requestedDevices are found in the stream.
|
|
1422
|
+ */
|
|
1423
|
+ _getMissingTracks(requestedDevices = [], stream) {
|
|
1424
|
+ const missingDevices = [];
|
1357
|
1425
|
|
1358
|
|
- if (options.devices.length) {
|
1359
|
|
- this.getUserMediaWithConstraints(
|
1360
|
|
- options.devices,
|
1361
|
|
- stream => {
|
1362
|
|
- const audioDeviceRequested
|
1363
|
|
- = options.devices.indexOf('audio') !== -1;
|
1364
|
|
- const videoDeviceRequested
|
1365
|
|
- = options.devices.indexOf('video') !== -1;
|
1366
|
|
- const audioTracksReceived
|
1367
|
|
- = stream.getAudioTracks().length > 0;
|
1368
|
|
- const videoTracksReceived
|
1369
|
|
- = stream.getVideoTracks().length > 0;
|
1370
|
|
-
|
1371
|
|
- if ((audioDeviceRequested && !audioTracksReceived)
|
1372
|
|
- || (videoDeviceRequested
|
1373
|
|
- && !videoTracksReceived)) {
|
1374
|
|
- self.stopMediaStream(stream);
|
1375
|
|
-
|
1376
|
|
- // We are getting here in case if we requested
|
1377
|
|
- // 'audio' or 'video' devices or both, but
|
1378
|
|
- // didn't get corresponding MediaStreamTrack in
|
1379
|
|
- // response stream. We don't know the reason why
|
1380
|
|
- // this happened, so reject with general error.
|
1381
|
|
- // eslint-disable-next-line no-shadow
|
1382
|
|
- const devices = [];
|
1383
|
|
-
|
1384
|
|
- if (audioDeviceRequested
|
1385
|
|
- && !audioTracksReceived) {
|
1386
|
|
- devices.push('audio');
|
1387
|
|
- }
|
|
1426
|
+ const audioDeviceRequested = requestedDevices.includes('audio');
|
|
1427
|
+ const audioTracksReceived
|
|
1428
|
+ = stream && stream.getAudioTracks().length > 0;
|
1388
|
1429
|
|
1389
|
|
- if (videoDeviceRequested
|
1390
|
|
- && !videoTracksReceived) {
|
1391
|
|
- devices.push('video');
|
1392
|
|
- }
|
|
1430
|
+ if (audioDeviceRequested && !audioTracksReceived) {
|
|
1431
|
+ missingDevices.push('audio');
|
|
1432
|
+ }
|
1393
|
1433
|
|
1394
|
|
- // we are missing one of the media we requested
|
1395
|
|
- // in order to get the actual error that caused
|
1396
|
|
- // this missing media we will call one more time
|
1397
|
|
- // getUserMedia so we can obtain the actual
|
1398
|
|
- // error (Example usecases are requesting
|
1399
|
|
- // audio and video and video device is missing
|
1400
|
|
- // or device is denied to be used and chrome is
|
1401
|
|
- // set to not ask for permissions)
|
1402
|
|
- self.getUserMediaWithConstraints(
|
1403
|
|
- devices,
|
1404
|
|
- () => {
|
1405
|
|
- // we already failed to obtain this
|
1406
|
|
- // media, so we are not supposed in any
|
1407
|
|
- // way to receive success for this call
|
1408
|
|
- // any way we will throw an error to be
|
1409
|
|
- // sure the promise will finish
|
1410
|
|
- reject(new JitsiTrackError(
|
1411
|
|
- { name: 'UnknownError' },
|
1412
|
|
- getConstraints(
|
1413
|
|
- options.devices,
|
1414
|
|
- options),
|
1415
|
|
- devices)
|
1416
|
|
- );
|
1417
|
|
- },
|
1418
|
|
- error => {
|
1419
|
|
- // rejects with real error for not
|
1420
|
|
- // obtaining the media
|
1421
|
|
- reject(error);
|
1422
|
|
- }, options);
|
1423
|
|
-
|
1424
|
|
- return;
|
1425
|
|
- }
|
1426
|
|
- if (hasDesktop) {
|
1427
|
|
- screenObtainer.obtainStream(
|
1428
|
|
- dsOptions,
|
1429
|
|
- desktop => {
|
1430
|
|
- successCallback({ audioVideo: stream,
|
1431
|
|
- desktop });
|
1432
|
|
- }, error => {
|
1433
|
|
- self.stopMediaStream(stream);
|
1434
|
|
-
|
1435
|
|
- reject(error);
|
1436
|
|
- });
|
1437
|
|
- } else {
|
1438
|
|
- successCallback({ audioVideo: stream });
|
1439
|
|
- }
|
1440
|
|
- },
|
1441
|
|
- error => reject(error),
|
1442
|
|
- options);
|
1443
|
|
- } else if (hasDesktop) {
|
|
1434
|
+ const videoDeviceRequested = requestedDevices.includes('video');
|
|
1435
|
+ const videoTracksReceived
|
|
1436
|
+ = stream && stream.getVideoTracks().length > 0;
|
|
1437
|
+
|
|
1438
|
+ if (videoDeviceRequested && !videoTracksReceived) {
|
|
1439
|
+ missingDevices.push('video');
|
|
1440
|
+ }
|
|
1441
|
+
|
|
1442
|
+ return missingDevices;
|
|
1443
|
+ }
|
|
1444
|
+
|
|
1445
|
+ /**
|
|
1446
|
+ * Performs separate getUserMedia calls for audio and video instead of in
|
|
1447
|
+ * one call. Will also request desktop if specified.
|
|
1448
|
+ *
|
|
1449
|
+ * @param {Object} options - An object describing how the gUM request should
|
|
1450
|
+ * be executed. See {@link obtainAudioAndVideoPermissions} for full options.
|
|
1451
|
+ * @returns {*} Promise object that will receive the new JitsiTracks on
|
|
1452
|
+ * success or a JitsiTrackError on failure.
|
|
1453
|
+ */
|
|
1454
|
+ _getAudioAndVideoStreamsSeparately(options) {
|
|
1455
|
+ return new Promise((resolve, reject) => {
|
|
1456
|
+ const deviceGUM = {
|
|
1457
|
+ audio: (...args) =>
|
|
1458
|
+ this.getUserMediaWithConstraints([ 'audio' ], ...args),
|
|
1459
|
+ video: (...args) =>
|
|
1460
|
+ this.getUserMediaWithConstraints([ 'video' ], ...args),
|
|
1461
|
+ desktop: (...args) =>
|
1444
|
1462
|
screenObtainer.obtainStream(
|
1445
|
|
- dsOptions,
|
1446
|
|
- desktop => successCallback({ desktop }),
|
1447
|
|
- error => reject(error));
|
1448
|
|
- }
|
1449
|
|
- }
|
|
1463
|
+ this._parseDesktopSharingOptions(options), ...args)
|
|
1464
|
+ };
|
|
1465
|
+
|
|
1466
|
+ obtainDevices({
|
|
1467
|
+ devices: options.devices,
|
|
1468
|
+ streams: [],
|
|
1469
|
+ successCallback: resolve,
|
|
1470
|
+ errorCallback: reject,
|
|
1471
|
+ deviceGUM
|
|
1472
|
+ });
|
1450
|
1473
|
});
|
1451
|
1474
|
}
|
1452
|
1475
|
|
|
1476
|
+ /**
|
|
1477
|
+ * Returns an object formatted for specifying desktop sharing parameters.
|
|
1478
|
+ *
|
|
1479
|
+ * @param {Object} options - Takes in the same options object as
|
|
1480
|
+ * {@link obtainAudioAndVideoPermissions}.
|
|
1481
|
+ * @returns {Object}
|
|
1482
|
+ */
|
|
1483
|
+ _parseDesktopSharingOptions(options) {
|
|
1484
|
+ return {
|
|
1485
|
+ ...options.desktopSharingExtensionExternalInstallation,
|
|
1486
|
+ desktopSharingSources: options.desktopSharingSources
|
|
1487
|
+ };
|
|
1488
|
+ }
|
|
1489
|
+
|
1453
|
1490
|
/**
|
1454
|
1491
|
* Gets streams from specified device types. This function intentionally
|
1455
|
1492
|
* ignores errors for upstream to catch and handle instead.
|