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

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

10
 export const TRACK_ADDED = Symbol('TRACK_ADDED');
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
  * Action for when a track has been removed from the conference,
54
  * Action for when a track has been removed from the conference,

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

10
 
10
 
11
 import {
11
 import {
12
     TRACK_ADDED,
12
     TRACK_ADDED,
13
-    TRACK_PERMISSION_ERROR,
13
+    TRACK_BEING_CREATED,
14
+    TRACK_CREATE_CANCELED,
15
+    TRACK_CREATE_ERROR,
14
     TRACK_REMOVED,
16
     TRACK_REMOVED,
15
     TRACK_UPDATED
17
     TRACK_UPDATED
16
 } from './actionTypes';
18
 } from './actionTypes';
79
         // to implement them) and the right thing to do is to ask for each
81
         // to implement them) and the right thing to do is to ask for each
80
         // device separately.
82
         // device separately.
81
         for (const device of devices) {
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
                     cameraDeviceId: options.cameraDeviceId,
90
                     cameraDeviceId: options.cameraDeviceId,
85
                     devices: [ device ],
91
                     devices: [ device ],
89
                 /* firePermissionPromptIsShownEvent */ false,
95
                 /* firePermissionPromptIsShownEvent */ false,
90
                 store)
96
                 store)
91
             .then(
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
                 reason =>
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
  * @returns {Function}
148
  * @returns {Function}
104
  */
149
  */
105
 export function destroyLocalTracks() {
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
 }
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
  * @private
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
                     trackPermissionError = error instanceof DOMException;
484
                     trackPermissionError = error instanceof DOMException;
431
                     break;
485
                     break;
432
                 }
486
                 }
433
-                trackPermissionError && dispatch({
434
-                    type: TRACK_PERMISSION_ERROR,
487
+
488
+                dispatch({
489
+                    type: TRACK_CREATE_ERROR,
490
+                    permissionDenied: trackPermissionError,
435
                     trackType: device
491
                     trackType: device
436
                 });
492
                 });
437
             }
493
             }
468
             // but that may not be the case tomorrow.
524
             // but that may not be the case tomorrow.
469
             && track.getCameraFacingMode() === CAMERA_FACING_MODE.USER);
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
  * @returns {(Track|undefined)}
106
  * @returns {(Track|undefined)}
107
  */
107
  */
108
 export function getLocalTrack(tracks, mediaType) {
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
 
3
 
4
 import {
4
 import {
5
     TRACK_ADDED,
5
     TRACK_ADDED,
6
+    TRACK_BEING_CREATED,
7
+    TRACK_CREATE_CANCELED,
8
+    TRACK_CREATE_ERROR,
6
     TRACK_REMOVED,
9
     TRACK_REMOVED,
7
     TRACK_UPDATED
10
     TRACK_UPDATED
8
 } from './actionTypes';
11
 } from './actionTypes';
9
 
12
 
10
 /**
13
 /**
11
  * @typedef {Object} Track
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
  * @property {boolean} local=false - If track is local.
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
  * @property {MEDIA_TYPE} mediaType=false - Media type of track.
22
  * @property {MEDIA_TYPE} mediaType=false - Media type of track.
16
  * @property {boolean} mirror=false - The indicator which determines whether the
23
  * @property {boolean} mirror=false - The indicator which determines whether the
17
  * display/rendering of the track should be mirrored. It only makes sense in the
24
  * display/rendering of the track should be mirrored. It only makes sense in the
81
     case TRACK_UPDATED:
88
     case TRACK_UPDATED:
82
         return state.map(t => track(t, action));
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
     case TRACK_REMOVED:
111
     case TRACK_REMOVED:
91
         return state.filter(t => t.jitsiTrack !== action.track.jitsiTrack);
112
         return state.filter(t => t.jitsiTrack !== action.track.jitsiTrack);

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

5
 import { isRoomValid } from '../../base/conference';
5
 import { isRoomValid } from '../../base/conference';
6
 import { Platform } from '../../base/react';
6
 import { Platform } from '../../base/react';
7
 import { MiddlewareRegistry } from '../../base/redux';
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
  * Middleware that captures track permission errors and alerts the user so they
11
  * Middleware that captures track permission errors and alerts the user so they
18
     const result = next(action);
18
     const result = next(action);
19
 
19
 
20
     switch (action.type) {
20
     switch (action.type) {
21
-    case TRACK_PERMISSION_ERROR:
21
+    case TRACK_CREATE_ERROR:
22
         // XXX We do not currently have user interface outside of a conference
22
         // XXX We do not currently have user interface outside of a conference
23
         // which the user may tap and cause a permission-related error. If we
23
         // which the user may tap and cause a permission-related error. If we
24
         // alert whenever we (intend to) ask for a permission, the scenario of
24
         // alert whenever we (intend to) ask for a permission, the scenario of
25
         // entering the WelcomePage, being asked for the camera permission, me
25
         // entering the WelcomePage, being asked for the camera permission, me
26
         // denying it, and being alerted that there is an error is overwhelming
26
         // denying it, and being alerted that there is an error is overwhelming
27
         // me.
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
             _alertPermissionErrorWithSettings(action.trackType);
31
             _alertPermissionErrorWithSettings(action.trackType);
30
         }
32
         }
31
         break;
33
         break;

Loading…
Cancel
Save