Browse Source

fix(base/tracks): handle GUM in progress

This commit adds extra actions/Redux state to be able to deal with
the GUM operation being in progress. There will be early local track
stub in the Redux state for any a local track for which GUM has been
called, but not completed yet.

Local track is considered valid only after TRACK_ADDED event when it
will have JitsiLocalTrack instance set.
master
paweldomas 8 years ago
parent
commit
f37a12c332

+ 2
- 3
react/features/base/conference/actions.js View File

@@ -12,7 +12,7 @@ import {
12 12
     participantRoleChanged,
13 13
     participantUpdated
14 14
 } from '../participants';
15
-import { trackAdded, trackRemoved } from '../tracks';
15
+import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
16 16
 
17 17
 import {
18 18
     CONFERENCE_FAILED,
@@ -222,8 +222,7 @@ export function conferenceLeft(conference: Object) {
222 222
 function _conferenceWillJoin(conference: Object) {
223 223
     return (dispatch: Dispatch<*>, getState: Function) => {
224 224
         const localTracks
225
-            = getState()['features/base/tracks']
226
-                .filter(t => t.local)
225
+            = getLocalTracks(getState()['features/base/tracks'])
227 226
                 .map(t => t.jitsiTrack);
228 227
 
229 228
         if (localTracks.length) {

+ 35
- 5
react/features/base/tracks/actionTypes.js View File

@@ -10,15 +10,45 @@
10 10
 export const TRACK_ADDED = Symbol('TRACK_ADDED');
11 11
 
12 12
 /**
13
- * Action for when a track cannot be created because permissions have not been
14
- * granted.
13
+ * Action triggered when a local track starts being created through the WebRTC
14
+ * getUserMedia call. It will include extra 'gumProcess' field which is
15
+ * a Promise with extra 'cancel' method which can be used to cancel the process.
16
+ * Canceling will result in disposing any JitsiLocalTrack returned by the GUM
17
+ * callback. There will be TRACK_CREATE_CANCELED event instead of track
18
+ * added/gum failed events.
15 19
  *
16 20
  * {
17
- *     type: TRACK_PERMISSION_ERROR,
18
- *     trackType: string
21
+ *     type: TRACK_BEING_CREATED
22
+ *     track: {
23
+ *         local: true,
24
+ *         gumProcess: Promise with cancel() method to abort,
25
+ *         mediaType: MEDIA_TYPE
26
+ *     }
19 27
  * }
20 28
  */
21
-export const TRACK_PERMISSION_ERROR = Symbol('TRACK_PERMISSION_ERROR');
29
+export const TRACK_BEING_CREATED = Symbol('TRACK_BEING_CREATED');
30
+
31
+/**
32
+ * Action sent when canceled GUM process completes either successfully or with
33
+ * an error (error is ignored and track is immediately disposed if created).
34
+ *
35
+ * {
36
+ *     type: TRACK_CREATE_CANCELED,
37
+ *     trackType: MEDIA_TYPE
38
+ * }
39
+ */
40
+export const TRACK_CREATE_CANCELED = Symbol('TRACK_CREATE_CANCELED');
41
+
42
+/**
43
+ * Action sent when GUM fails with an error other than permission denied.
44
+ *
45
+ * {
46
+ *     type: TRACK_CREATE_ERROR,
47
+ *     permissionDenied: Boolean,
48
+ *     trackType: MEDIA_TYPE
49
+ * }
50
+ */
51
+export const TRACK_CREATE_ERROR = Symbol('TRACK_CREATE_ERROR');
22 52
 
23 53
 /**
24 54
  * Action for when a track has been removed from the conference,

+ 129
- 94
react/features/base/tracks/actions.js View File

@@ -10,7 +10,9 @@ import { getLocalParticipant } from '../participants';
10 10
 
11 11
 import {
12 12
     TRACK_ADDED,
13
-    TRACK_PERMISSION_ERROR,
13
+    TRACK_BEING_CREATED,
14
+    TRACK_CREATE_CANCELED,
15
+    TRACK_CREATE_ERROR,
14 16
     TRACK_REMOVED,
15 17
     TRACK_UPDATED
16 18
 } from './actionTypes';
@@ -79,7 +81,11 @@ export function createLocalTracksA(options = {}) {
79 81
         // to implement them) and the right thing to do is to ask for each
80 82
         // device separately.
81 83
         for (const device of devices) {
82
-            createLocalTracksF(
84
+            if (getState()['features/base/tracks']
85
+                    .find(t => t.local && t.mediaType === device)) {
86
+                throw new Error(`Local track for ${device} already exists`);
87
+            }
88
+            const gumProcess = createLocalTracksF(
83 89
                 {
84 90
                     cameraDeviceId: options.cameraDeviceId,
85 91
                     devices: [ device ],
@@ -89,9 +95,48 @@ export function createLocalTracksA(options = {}) {
89 95
                 /* firePermissionPromptIsShownEvent */ false,
90 96
                 store)
91 97
             .then(
92
-                localTracks => dispatch(_updateLocalTracks(localTracks)),
98
+                localTracks => {
99
+                    // Because GUM is called for 1 device (which is actually
100
+                    // a media type 'audio','video', 'screen' etc.) we should
101
+                    // not get more than one JitsiTrack.
102
+                    if (localTracks.length !== 1) {
103
+                        throw new Error(
104
+                            'Expected exactly 1 track, but was '
105
+                                + `given ${localTracks.length} tracks`
106
+                                + `for device: ${device}.`);
107
+                    }
108
+
109
+                    if (gumProcess.canceled) {
110
+                        return _disposeTracks(localTracks)
111
+                                    .then(
112
+                                        () =>
113
+                                            dispatch(
114
+                                                _trackCreateCanceled(device)));
115
+                    }
116
+
117
+                    return dispatch(trackAdded(localTracks[0]));
118
+                },
119
+                // eslint-disable-next-line no-confusing-arrow
93 120
                 reason =>
94
-                    dispatch(_onCreateLocalTracksRejected(reason, device)));
121
+                    dispatch(
122
+                        gumProcess.canceled
123
+                            ? _trackCreateCanceled(device)
124
+                            : _onCreateLocalTracksRejected(reason, device)));
125
+
126
+            gumProcess.cancel = () => {
127
+                gumProcess.canceled = true;
128
+
129
+                return gumProcess;
130
+            };
131
+
132
+            dispatch({
133
+                type: TRACK_BEING_CREATED,
134
+                track: {
135
+                    local: true,
136
+                    gumProcess,
137
+                    mediaType: device
138
+                }
139
+            });
95 140
         }
96 141
     };
97 142
 }
@@ -103,12 +148,17 @@ export function createLocalTracksA(options = {}) {
103 148
  * @returns {Function}
104 149
  */
105 150
 export function destroyLocalTracks() {
106
-    return (dispatch, getState) =>
107
-        dispatch(
108
-            _disposeAndRemoveTracks(
109
-                getState()['features/base/tracks']
110
-                    .filter(t => t.local)
111
-                    .map(t => t.jitsiTrack)));
151
+    return (dispatch, getState) => {
152
+        // First wait until any getUserMedia in progress is settled and then get
153
+        // rid of all local tracks.
154
+        _cancelAllGumInProgress(getState)
155
+            .then(
156
+                () => dispatch(
157
+                    _disposeAndRemoveTracks(
158
+                        getState()['features/base/tracks']
159
+                            .filter(t => t.local)
160
+                            .map(t => t.jitsiTrack))));
161
+    };
112 162
 }
113 163
 
114 164
 /**
@@ -316,81 +366,85 @@ function _addTracks(tracks) {
316 366
 }
317 367
 
318 368
 /**
319
- * Disposes passed tracks and signals them to be removed.
369
+ * Signals that track create operation for given media track has been canceled.
370
+ * Will clean up local track stub from the Redux state which holds the
371
+ * 'gumProcess' reference.
320 372
  *
321
- * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
322
- * @protected
323
- * @returns {Function}
373
+ * @param {MEDIA_TYPE} mediaType - The type of the media for which the track was
374
+ * being created.
375
+ * @returns {{
376
+ *      type,
377
+ *      trackType: MEDIA_TYPE
378
+ * }}
379
+ * @private
324 380
  */
325
-export function _disposeAndRemoveTracks(tracks) {
326
-    return dispatch =>
327
-        Promise.all(
328
-            tracks.map(t =>
329
-                t.dispose()
330
-                    .catch(err => {
331
-                        // Track might be already disposed so ignore such an
332
-                        // error. Of course, re-throw any other error(s).
333
-                        if (err.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
334
-                            throw err;
335
-                        }
336
-                    })
337
-            ))
338
-            .then(Promise.all(tracks.map(t => dispatch(trackRemoved(t)))));
381
+function _trackCreateCanceled(mediaType) {
382
+    return {
383
+        type: TRACK_CREATE_CANCELED,
384
+        trackType: mediaType
385
+    };
339 386
 }
340 387
 
341 388
 /**
342
- * Finds the first {@code JitsiLocalTrack} in a specific array/list of
343
- * {@code JitsiTrack}s which is of a specific {@code MEDIA_TYPE}.
389
+ * Cancels and waits for any get user media operations currently in progress to
390
+ * complete.
344 391
  *
345
- * @param {JitsiTrack[]} tracks - The array/list of {@code JitsiTrack}s to look
346
- * through.
347
- * @param {MEDIA_TYPE} mediaType - The {@code MEDIA_TYPE} of the first
348
- * {@code JitsiLocalTrack} to be returned.
392
+ * @param {Function} getState - The Redux store {@code getState} method used to
393
+ * obtain the state.
394
+ * @returns {Promise} - A Promise resolved once all {@code gumProcess.cancel}
395
+ * Promises are settled. That is when they are either resolved or rejected,
396
+ * because all we care about here is to be sure that get user media callbacks
397
+ * have completed (returned from the native side).
349 398
  * @private
350
- * @returns {JitsiLocalTrack} The first {@code JitsiLocalTrack}, if any, in the
351
- * specified {@code tracks} of the specified {@code mediaType}.
352 399
  */
353
-function _getLocalTrack(tracks, mediaType) {
354
-    return tracks.find(track =>
355
-        track.isLocal()
356
-
357
-            // XXX JitsiTrack#getType() returns a MEDIA_TYPE value in the terms
358
-            // of lib-jitsi-meet while mediaType is in the terms of jitsi-meet.
359
-            && track.getType() === mediaType);
400
+function _cancelAllGumInProgress(getState) {
401
+    // FIXME use logger
402
+    const logError
403
+        = error =>
404
+            console.error('gumProcess.cancel failed', JSON.stringify(error));
405
+
406
+    return Promise.all(
407
+        getState()['features/base/tracks']
408
+            .filter(t => t.local)
409
+            .map(
410
+                t => t.gumProcess
411
+                    && t.gumProcess.cancel().catch(logError)));
360 412
 }
361 413
 
362 414
 /**
363
- * Determines which local media tracks should be added and which removed.
415
+ * Disposes passed tracks and signals them to be removed.
364 416
  *
365
- * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} currentTracks - List of
366
- * current/existing media tracks.
367
- * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} newTracks - List of new media
368
- * tracks.
369
- * @private
370
- * @returns {{
371
- *     tracksToAdd: JitsiLocalTrack[],
372
- *     tracksToRemove: JitsiLocalTrack[]
373
- * }}
417
+ * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
418
+ * @protected
419
+ * @returns {Function}
374 420
  */
375
-function _getLocalTracksToChange(currentTracks, newTracks) {
376
-    const tracksToAdd = [];
377
-    const tracksToRemove = [];
378
-
379
-    for (const mediaType of [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ]) {
380
-        const newTrack = _getLocalTrack(newTracks, mediaType);
381
-
382
-        if (newTrack) {
383
-            const currentTrack = _getLocalTrack(currentTracks, mediaType);
384
-
385
-            tracksToAdd.push(newTrack);
386
-            currentTrack && tracksToRemove.push(currentTrack);
387
-        }
388
-    }
421
+export function _disposeAndRemoveTracks(tracks) {
422
+    return dispatch =>
423
+        _disposeTracks(tracks)
424
+            .then(
425
+                () => Promise.all(tracks.map(t => dispatch(trackRemoved(t)))));
426
+}
389 427
 
390
-    return {
391
-        tracksToAdd,
392
-        tracksToRemove
393
-    };
428
+/**
429
+ * Disposes passed tracks.
430
+ *
431
+ * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
432
+ * @protected
433
+ * @returns {Promise} - A Promise resolved once {@link JitsiTrack.dispose()} is
434
+ * done for every track from the list.
435
+ */
436
+function _disposeTracks(tracks) {
437
+    return Promise.all(
438
+        tracks.map(t =>
439
+            t.dispose()
440
+                .catch(err => {
441
+                    // Track might be already disposed so ignore such an
442
+                    // error. Of course, re-throw any other error(s).
443
+                    if (err.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
444
+                        throw err;
445
+                    }
446
+                })
447
+        ));
394 448
 }
395 449
 
396 450
 /**
@@ -430,8 +484,10 @@ function _onCreateLocalTracksRejected({ gum }, device) {
430 484
                     trackPermissionError = error instanceof DOMException;
431 485
                     break;
432 486
                 }
433
-                trackPermissionError && dispatch({
434
-                    type: TRACK_PERMISSION_ERROR,
487
+
488
+                dispatch({
489
+                    type: TRACK_CREATE_ERROR,
490
+                    permissionDenied: trackPermissionError,
435 491
                     trackType: device
436 492
                 });
437 493
             }
@@ -468,24 +524,3 @@ function _shouldMirror(track) {
468 524
             // but that may not be the case tomorrow.
469 525
             && track.getCameraFacingMode() === CAMERA_FACING_MODE.USER);
470 526
 }
471
-
472
-/**
473
- * Set new local tracks replacing any existing tracks that were previously
474
- * available. Currently only one audio and one video local tracks are allowed.
475
- *
476
- * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} [newTracks=[]] - List of new
477
- * media tracks.
478
- * @private
479
- * @returns {Function}
480
- */
481
-function _updateLocalTracks(newTracks = []) {
482
-    return (dispatch, getState) => {
483
-        const tracks
484
-            = getState()['features/base/tracks'].map(t => t.jitsiTrack);
485
-        const { tracksToAdd, tracksToRemove }
486
-            = _getLocalTracksToChange(tracks, newTracks);
487
-
488
-        return dispatch(_disposeAndRemoveTracks(tracksToRemove))
489
-            .then(() => dispatch(_addTracks(tracksToAdd)));
490
-    };
491
-}

+ 20
- 1
react/features/base/tracks/functions.js View File

@@ -106,7 +106,26 @@ export function getLocalAudioTrack(tracks) {
106 106
  * @returns {(Track|undefined)}
107 107
  */
108 108
 export function getLocalTrack(tracks, mediaType) {
109
-    return tracks.find(t => t.local && t.mediaType === mediaType);
109
+    return getLocalTracks(tracks).find(t => t.mediaType === mediaType);
110
+}
111
+
112
+/**
113
+ * Returns an array containing local tracks. Local tracks without valid
114
+ * JitsiTrack will not be included in the list.
115
+ *
116
+ * @param {Track[]} tracks - An array of all local tracks.
117
+ * @returns {Track[]}
118
+ */
119
+export function getLocalTracks(tracks) {
120
+
121
+    // XXX A local track is considered ready only once it has 'jitsiTrack' field
122
+    // set by the TRACK_ADDED action. Until then there is a stub added just
123
+    // before get user media call with a cancellable 'gumInProgress' field which
124
+    // then can be used to destroy the track that has not yet been added to
125
+    // the Redux store. Once GUM is cancelled it will never make it to the store
126
+    // nor there will be any TRACK_ADDED/TRACK_REMOVED related events fired for
127
+    // it.
128
+    return tracks.filter(t => t.local && t.jitsiTrack);
110 129
 }
111 130
 
112 131
 /**

+ 28
- 7
react/features/base/tracks/reducer.js View File

@@ -3,15 +3,22 @@ import { ReducerRegistry } from '../redux';
3 3
 
4 4
 import {
5 5
     TRACK_ADDED,
6
+    TRACK_BEING_CREATED,
7
+    TRACK_CREATE_CANCELED,
8
+    TRACK_CREATE_ERROR,
6 9
     TRACK_REMOVED,
7 10
     TRACK_UPDATED
8 11
 } from './actionTypes';
9 12
 
10 13
 /**
11 14
  * @typedef {Object} Track
12
- * @property {(JitsiLocalTrack|JitsiRemoteTrack)} jitsiTrack - JitsiTrack
13
- * instance.
15
+ * @property {(JitsiLocalTrack|JitsiRemoteTrack)} [jitsiTrack] - JitsiTrack
16
+ * instance. Optional for local tracks if those are being created (GUM in
17
+ * progress).
14 18
  * @property {boolean} local=false - If track is local.
19
+ * @property {Promise} [gumProcess] - if local track is being created it
20
+ * will have no JitsiTrack, but a 'gumProcess' set to a Promise with and extra
21
+ * cancel().
15 22
  * @property {MEDIA_TYPE} mediaType=false - Media type of track.
16 23
  * @property {boolean} mirror=false - The indicator which determines whether the
17 24
  * display/rendering of the track should be mirrored. It only makes sense in the
@@ -81,11 +88,25 @@ ReducerRegistry.register('features/base/tracks', (state = [], action) => {
81 88
     case TRACK_UPDATED:
82 89
         return state.map(t => track(t, action));
83 90
 
84
-    case TRACK_ADDED:
85
-        return [
86
-            ...state,
87
-            action.track
88
-        ];
91
+    case TRACK_ADDED: {
92
+        let withoutTrackStub = state;
93
+
94
+        if (action.track.local) {
95
+            withoutTrackStub
96
+                = state.filter(
97
+                    t => !t.local || t.mediaType !== action.track.mediaType);
98
+        }
99
+
100
+        return [ ...withoutTrackStub, action.track ];
101
+    }
102
+
103
+    case TRACK_BEING_CREATED:
104
+        return [ ...state, action.track ];
105
+
106
+    case TRACK_CREATE_CANCELED:
107
+    case TRACK_CREATE_ERROR: {
108
+        return state.filter(t => !t.local || t.mediaType !== action.trackType);
109
+    }
89 110
 
90 111
     case TRACK_REMOVED:
91 112
         return state.filter(t => t.jitsiTrack !== action.track.jitsiTrack);

+ 5
- 3
react/features/mobile/permissions/middleware.js View File

@@ -5,7 +5,7 @@ import { Alert, Linking, NativeModules } from 'react-native';
5 5
 import { isRoomValid } from '../../base/conference';
6 6
 import { Platform } from '../../base/react';
7 7
 import { MiddlewareRegistry } from '../../base/redux';
8
-import { TRACK_PERMISSION_ERROR } from '../../base/tracks';
8
+import { TRACK_CREATE_ERROR } from '../../base/tracks';
9 9
 
10 10
 /**
11 11
  * Middleware that captures track permission errors and alerts the user so they
@@ -18,14 +18,16 @@ MiddlewareRegistry.register(store => next => action => {
18 18
     const result = next(action);
19 19
 
20 20
     switch (action.type) {
21
-    case TRACK_PERMISSION_ERROR:
21
+    case TRACK_CREATE_ERROR:
22 22
         // XXX We do not currently have user interface outside of a conference
23 23
         // which the user may tap and cause a permission-related error. If we
24 24
         // alert whenever we (intend to) ask for a permission, the scenario of
25 25
         // entering the WelcomePage, being asked for the camera permission, me
26 26
         // denying it, and being alerted that there is an error is overwhelming
27 27
         // me.
28
-        if (isRoomValid(store.getState()['features/base/conference'].room)) {
28
+        if (action.permissionDenied
29
+                && isRoomValid(
30
+                        store.getState()['features/base/conference'].room)) {
29 31
             _alertPermissionErrorWithSettings(action.trackType);
30 32
         }
31 33
         break;

Loading…
Cancel
Save