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

ref(gum): try to reduce complexity of obtainAudioAndVideoPermissions (#707)

* ref(gum): try to reduce complexity of obtainAudioAndVideoPermissions

Even though I walk through the valley of the shadow of death
I will fear no evil... Reduce indenting and repeated calls to
getting desktop streams by creating a promise chain.

* squash: use arrow func

* squash: move constructor support to helper

* squash: put ff into mediastream constructor check

* squash: ff support media stream constructor

MDN states its been supported since 44. ESR is currently
52.

* squash: rename dsoptions, constant defaults

* squash: move comment about missing tracks error handling

* squash: wrap getUserMediaWithConstraints in promise

* squash: split up av funcs

* squash: hey, some tests...are better than no tests?
master
virtuacoplenny 7 роки тому
джерело
коміт
e293246aff
3 змінених файлів з 520 додано та 184 видалено
  1. 221
    184
      modules/RTC/RTCUtils.js
  2. 286
    0
      modules/RTC/RTCUtils.spec.js
  3. 13
    0
      modules/browser/BrowserCapabilities.js

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

@@ -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.

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

@@ -0,0 +1,286 @@
1
+import RTCUtils from './RTCUtils';
2
+import browser from '../browser';
3
+import screenObtainer from './ScreenObtainer';
4
+
5
+// TODO move webrtc mocks/polyfills into a easily accessible file when needed
6
+/**
7
+ * A constructor to create a mock for the native MediaStreamTrack.
8
+ */
9
+function MediaStreamTrackMock(kind, options = {}) {
10
+    this.kind = kind;
11
+    this._settings = {};
12
+
13
+    if (options.resolution) {
14
+        this._settings.height = options.resolution;
15
+    }
16
+}
17
+
18
+MediaStreamTrackMock.prototype.getSettings = function() {
19
+    return this._settings;
20
+};
21
+
22
+MediaStreamTrackMock.prototype.stop
23
+    = function() { /** intentionally blank **/ };
24
+
25
+/**
26
+ * A constructor to create a mock for the native MediaStream.
27
+ */
28
+function MediaStreamMock() {
29
+    this.id = Date.now();
30
+    this._audioTracks = [];
31
+    this._videoTracks = [];
32
+}
33
+
34
+MediaStreamMock.prototype.addTrack = function(track) {
35
+    if (track.kind === 'audio') {
36
+        this._audioTracks.push(track);
37
+    } else if (track.kind === 'video') {
38
+        this._videoTracks.push(track);
39
+    }
40
+};
41
+
42
+MediaStreamMock.prototype.getAudioTracks = function() {
43
+    return this._audioTracks;
44
+};
45
+
46
+MediaStreamMock.prototype.getTracks = function() {
47
+    return [
48
+        ...this._audioTracks,
49
+        ...this._videoTracks
50
+    ];
51
+};
52
+
53
+MediaStreamMock.prototype.getVideoTracks = function() {
54
+    return this._videoTracks;
55
+};
56
+
57
+/* eslint-disable max-params */
58
+/**
59
+ * A mock function to be used for stubbing out the wrapper around getUserMedia.
60
+ *
61
+ * @param {String[]} devices - The media devices to obtain. Valid devices are
62
+ * 'audio', 'video', and 'desktop'.
63
+ * @param {Function} onSuccess - An optional success callback to trigger.
64
+ * @param {Function} onError - An optional error callback to trigger. This is
65
+ * not used in this function.
66
+ * @param {Object} options - An object describing the constraints to pass to
67
+ * gum.
68
+ * @private
69
+ * @returns {Promise} A resolved promise with a MediaStreamMock.
70
+ */
71
+function successfulGum(devices, onSuccess, onError, options) {
72
+    /* eslint-enable max-params */
73
+
74
+    const mediaStreamMock = new MediaStreamMock();
75
+
76
+    if (devices.includes('audio')) {
77
+        mediaStreamMock.addTrack(new MediaStreamTrackMock('audio', options));
78
+    }
79
+
80
+    if (devices.includes('video')) {
81
+        mediaStreamMock.addTrack(new MediaStreamTrackMock('video', options));
82
+    }
83
+
84
+    if (devices.includes('desktop')) {
85
+        mediaStreamMock.addTrack(new MediaStreamTrackMock('video', options));
86
+    }
87
+
88
+    if (onSuccess) {
89
+        onSuccess(mediaStreamMock);
90
+    }
91
+
92
+    return Promise.resolve(mediaStreamMock);
93
+}
94
+
95
+/**
96
+ * General error handling for a promise chain that threw an unexpected error.
97
+ *
98
+ * @param {Error} error - The error object describing what error occurred.
99
+ * @param {function} done - Jasmine's done function to trigger a failed test.
100
+ * @private
101
+ * @returns {void}
102
+ */
103
+function unexpectedErrorHandler(error = {}, done) {
104
+    done.fail(`unexpected error occurred: ${error.message}`);
105
+}
106
+
107
+describe('RTCUtils', () => {
108
+    describe('obtainAudioAndVideoPermissions', () => {
109
+        let getUserMediaSpy, isScreenSupportedSpy, oldMediaStream,
110
+            oldMediaStreamTrack, oldWebkitMediaStream,
111
+            supportsMediaConstructorSpy;
112
+
113
+        beforeEach(() => {
114
+            // FIXME: To get some kind of initial testing working assume a
115
+            // chrome environment so RTCUtils can actually initialize properly.
116
+            spyOn(browser, 'isChrome').and.returnValue(true);
117
+            supportsMediaConstructorSpy
118
+                = spyOn(browser, 'supportsMediaStreamConstructor')
119
+                    .and.returnValue(true);
120
+            spyOn(screenObtainer, '_createObtainStreamMethod')
121
+                .and.returnValue(() => { /** intentional no op */ });
122
+            isScreenSupportedSpy = spyOn(screenObtainer, 'isSupported')
123
+                .and.returnValue(true);
124
+
125
+            oldMediaStreamTrack = window.MediaStreamTrack;
126
+            window.MediaStreamTrack = MediaStreamTrackMock;
127
+
128
+            oldMediaStream = window.MediaStream;
129
+            window.MediaStream = MediaStreamMock;
130
+
131
+            oldWebkitMediaStream = window.webkitMediaStream;
132
+            window.webkitMediaStream = MediaStreamMock;
133
+            RTCUtils.init();
134
+
135
+            getUserMediaSpy = spyOn(RTCUtils, 'getUserMediaWithConstraints');
136
+        });
137
+
138
+        afterEach(() => {
139
+            window.MediaStreamTrack = oldMediaStreamTrack;
140
+            window.MediaStream = oldMediaStream;
141
+            window.webkitMediaStream = oldWebkitMediaStream;
142
+        });
143
+
144
+        it('gets audio and video by default', done => {
145
+            getUserMediaSpy.and.callFake(successfulGum);
146
+
147
+            RTCUtils.obtainAudioAndVideoPermissions()
148
+                .then(streams => {
149
+                    expect(streams.length).toBe(2);
150
+
151
+                    const audioStream = streams.find(stream =>
152
+                        stream.mediaType === 'audio');
153
+
154
+                    expect(audioStream).toBeTruthy();
155
+                    expect(audioStream.stream instanceof MediaStreamMock)
156
+                        .toBe(true);
157
+                    expect(audioStream.stream.getAudioTracks().length).toBe(1);
158
+
159
+                    const videoStream = streams.find(stream =>
160
+                        stream.mediaType === 'video');
161
+
162
+                    expect(videoStream).toBeTruthy();
163
+                    expect(videoStream.stream instanceof MediaStreamMock)
164
+                        .toBe(true);
165
+                    expect(videoStream.stream.getVideoTracks().length).toBe(1);
166
+
167
+                    done();
168
+                })
169
+                .catch(error => unexpectedErrorHandler(error, done));
170
+        });
171
+
172
+        it('can get an audio track', done => {
173
+            getUserMediaSpy.and.callFake(successfulGum);
174
+
175
+            RTCUtils.obtainAudioAndVideoPermissions({ devices: [ 'audio' ] })
176
+                .then(streams => {
177
+                    expect(streams.length).toBe(1);
178
+
179
+                    expect(streams[0].stream instanceof MediaStreamMock)
180
+                        .toBe(true);
181
+                    expect(streams[0].stream.getAudioTracks().length).toBe(1);
182
+
183
+                    done();
184
+                })
185
+                .catch(error => unexpectedErrorHandler(error, done));
186
+
187
+        });
188
+
189
+        it('can get a video track', done => {
190
+            getUserMediaSpy.and.callFake(successfulGum);
191
+
192
+            RTCUtils.obtainAudioAndVideoPermissions({ devices: [ 'video' ] })
193
+                .then(streams => {
194
+                    expect(streams.length).toBe(1);
195
+
196
+                    expect(streams[0].stream instanceof MediaStreamMock)
197
+                        .toBe(true);
198
+                    expect(streams[0].stream.getVideoTracks().length).toBe(1);
199
+
200
+                    done();
201
+                })
202
+                .catch(error => unexpectedErrorHandler(error, done));
203
+        });
204
+
205
+        it('gets 720 videor resolution by default', done => {
206
+            getUserMediaSpy.and.callFake(successfulGum);
207
+
208
+            RTCUtils.obtainAudioAndVideoPermissions({ devices: [ 'video' ] })
209
+                .then(streams => {
210
+                    const videoTrack = streams[0].stream.getVideoTracks()[0];
211
+                    const { height } = videoTrack.getSettings();
212
+
213
+                    expect(height).toBe(720);
214
+
215
+                    done();
216
+                })
217
+                .catch(error => unexpectedErrorHandler(error, done));
218
+        });
219
+
220
+        describe('requesting desktop', () => {
221
+            it('errors if desktop is not supported', done => {
222
+                isScreenSupportedSpy.and.returnValue(false);
223
+
224
+                RTCUtils.obtainAudioAndVideoPermissions({
225
+                    devices: [ 'desktop' ] })
226
+                    .then(() => done.fail(
227
+                        'obtainAudioAndVideoPermissions should not succeed'))
228
+                    .catch(error => {
229
+                        expect(error.message)
230
+                            .toBe('Desktop sharing is not supported!');
231
+
232
+                        done();
233
+                    });
234
+            });
235
+
236
+            it('can obtain a desktop stream', done => {
237
+                spyOn(screenObtainer, 'obtainStream')
238
+                    .and.callFake((options, success) => {
239
+                        const mediaStreamMock = new MediaStreamMock();
240
+
241
+                        mediaStreamMock.addTrack(
242
+                            new MediaStreamTrackMock('video', options));
243
+
244
+                        success({ stream: mediaStreamMock });
245
+                    });
246
+
247
+                RTCUtils.obtainAudioAndVideoPermissions({
248
+                    devices: [ 'desktop' ] })
249
+                    .then(streams => {
250
+                        expect(streams.length).toBe(1);
251
+                        expect(streams[0].videoType).toBe('desktop');
252
+
253
+                        done();
254
+                    })
255
+                    .catch(error => unexpectedErrorHandler(error, done));
256
+            });
257
+        });
258
+
259
+        describe('without MediaStream constructor support', () => {
260
+            it('makes separate getUserMedia calls', done => {
261
+                supportsMediaConstructorSpy.and.returnValue(false);
262
+                getUserMediaSpy.and.callFake(successfulGum);
263
+
264
+                RTCUtils.obtainAudioAndVideoPermissions({
265
+                    devices: [ 'audio', 'video' ] })
266
+                    .then(streams => {
267
+                        expect(getUserMediaSpy.calls.count()).toBe(2);
268
+                        expect(streams.length).toBe(2);
269
+
270
+                        const audioStream = streams.find(stream =>
271
+                            stream.mediaType === 'audio');
272
+
273
+                        expect(audioStream).toBeTruthy();
274
+
275
+                        const videoStream = streams.find(stream =>
276
+                            stream.mediaType === 'video');
277
+
278
+                        expect(videoStream).toBeTruthy();
279
+
280
+                        done();
281
+                    })
282
+                    .catch(error => unexpectedErrorHandler(error, done));
283
+            });
284
+        });
285
+    });
286
+});

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

@@ -103,6 +103,19 @@ export default class BrowserCapabilities extends BrowserDetection {
103 103
         return !this.isEdge();
104 104
     }
105 105
 
106
+
107
+    /**
108
+     * Checks if the current browser supports the MediaStream constructor as
109
+     * defined by https://www.w3.org/TR/mediacapture-streams/#constructors. In
110
+     * cases where there is no support, it maybe be necessary to get audio
111
+     * and video in two distinct GUM calls.
112
+     * @return {boolean}
113
+     */
114
+    supportsMediaStreamConstructor() {
115
+        return !this.isReactNative()
116
+            && !this.isTemasysPluginUsed();
117
+    }
118
+
106 119
     /**
107 120
      * Checks if the current browser reports round trip time statistics for
108 121
      * the ICE candidate pair.

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