浏览代码

ref(conference) Simplify track creation. (#13209)

* ref(conference) Simplify track creation.
If gUM fails, we do not have to retry gUM with mic only and camera only constraints. gUM has come a long way and this is not needed anymore.

* ref(conference) Filter tracks that are added to conference.

* squash: Address review comments

* fix(prejoin): Display the exact gUM error in prejoin.

* squash: Address review comments
factor2
Jaya Allamsetty 2 年前
父节点
当前提交
4c37ef7a2c
没有帐户链接到提交者的电子邮件
共有 3 个文件被更改,包括 154 次插入165 次删除
  1. 75
    98
      conference.js
  2. 63
    51
      react/features/base/tracks/functions.web.ts
  3. 16
    16
      react/features/prejoin/reducer.ts

+ 75
- 98
conference.js 查看文件

559
             );
559
             );
560
         }
560
         }
561
 
561
 
562
-        let tryCreateLocalTracks;
562
+        let tryCreateLocalTracks = Promise.resolve([]);
563
 
563
 
564
         // On Electron there is no permission prompt for granting permissions. That's why we don't need to
564
         // On Electron there is no permission prompt for granting permissions. That's why we don't need to
565
         // spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
565
         // spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
600
 
600
 
601
                     return [];
601
                     return [];
602
                 });
602
                 });
603
-        } else if (!requestedAudio && !requestedVideo) {
604
-            // Resolve with no tracks
605
-            tryCreateLocalTracks = Promise.resolve([]);
606
-        } else {
603
+        } else if (requestedAudio || requestedVideo) {
607
             tryCreateLocalTracks = createLocalTracksF({
604
             tryCreateLocalTracks = createLocalTracksF({
608
                 devices: initialDevices,
605
                 devices: initialDevices,
609
                 timeout,
606
                 timeout,
610
                 firePermissionPromptIsShownEvent: true
607
                 firePermissionPromptIsShownEvent: true
611
             })
608
             })
612
-                .catch(err => {
613
-                    if (requestedAudio && requestedVideo) {
614
-
615
-                        // Try audio only...
616
-                        errors.audioAndVideoError = err;
609
+            .catch(async error => {
610
+                if (error.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
611
+                    errors.audioAndVideoError = error;
617
 
612
 
618
-                        if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
619
-                            // In this case we expect that the permission prompt is still visible. There is no point of
620
-                            // executing GUM with different source. Also at the time of writing the following
621
-                            // inconsistency have been noticed in some browsers - if the permissions prompt is visible
622
-                            // and another GUM is executed the prompt does not change its content but if the user
623
-                            // clicks allow the user action isassociated with the latest GUM call.
624
-                            errors.audioOnlyError = err;
625
-                            errors.videoOnlyError = err;
613
+                    return [];
614
+                }
626
 
615
 
627
-                            return [];
628
-                        }
616
+                // Retry with separate gUM calls.
617
+                const gUMPromises = [];
618
+                const tracks = [];
629
 
619
 
630
-                        return createLocalTracksF(audioOptions);
631
-                    } else if (requestedAudio && !requestedVideo) {
632
-                        errors.audioOnlyError = err;
620
+                if (requestedAudio) {
621
+                    gUMPromises.push(createLocalTracksF(audioOptions));
622
+                }
633
 
623
 
634
-                        return [];
635
-                    } else if (requestedVideo && !requestedAudio) {
636
-                        errors.videoOnlyError = err;
624
+                if (requestedVideo) {
625
+                    gUMPromises.push(createLocalTracksF({
626
+                        devices: [ MEDIA_TYPE.VIDEO ],
627
+                        timeout,
628
+                        firePermissionPromptIsShownEvent: true
629
+                    }));
630
+                }
637
 
631
 
638
-                        return [];
639
-                    }
640
-                    logger.error('Should never happen');
641
-                })
642
-                .catch(err => {
643
-                    // Log this just in case...
644
-                    if (!requestedAudio) {
645
-                        logger.error('The impossible just happened', err);
646
-                    }
647
-                    errors.audioOnlyError = err;
648
-
649
-                    // Try video only...
650
-                    return requestedVideo
651
-                        ? createLocalTracksF({
652
-                            devices: [ MEDIA_TYPE.VIDEO ],
653
-                            firePermissionPromptIsShownEvent: true
654
-                        })
655
-                        : [];
656
-                })
657
-                .catch(err => {
658
-                    // Log this just in case...
659
-                    if (!requestedVideo) {
660
-                        logger.error('The impossible just happened', err);
632
+                const results = await Promise.allSettled(gUMPromises);
633
+                let errorMsg;
634
+
635
+                results.forEach((result, idx) => {
636
+                    if (result.status === 'fulfilled') {
637
+                        tracks.push(result.value[0]);
638
+                    } else {
639
+                        errorMsg = result.reason;
640
+                        const isAudio = idx === 0;
641
+
642
+                        logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
643
+                        if (isAudio) {
644
+                            errors.audioOnlyError = errorMsg;
645
+                        } else {
646
+                            errors.videoOnlyError = errorMsg;
647
+                        }
661
                     }
648
                     }
662
-                    errors.videoOnlyError = err;
663
-
664
-                    return [];
665
                 });
649
                 });
650
+
651
+                if (errors.audioOnlyError && errors.videoOnlyError) {
652
+                    errors.audioAndVideoError = errorMsg;
653
+                }
654
+
655
+                return tracks;
656
+            });
666
         }
657
         }
667
 
658
 
668
-        // Hide the permissions prompt/overlay as soon as the tracks are
669
-        // created. Don't wait for the connection to be made, since in some
670
-        // cases, when auth is required, for instance, that won't happen until
671
-        // the user inputs their credentials, but the dialog would be
672
-        // overshadowed by the overlay.
659
+        // Hide the permissions prompt/overlay as soon as the tracks are created. Don't wait for the connection to
660
+        // be established, as in some cases like when auth is required, connection won't be established until the user
661
+        // inputs their credentials, but the dialog would be overshadowed by the overlay.
673
         tryCreateLocalTracks.then(tracks => {
662
         tryCreateLocalTracks.then(tracks => {
674
             APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
663
             APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
675
 
664
 
810
         const initialOptions = {
799
         const initialOptions = {
811
             startAudioOnly: config.startAudioOnly,
800
             startAudioOnly: config.startAudioOnly,
812
             startScreenSharing: config.startScreenSharing,
801
             startScreenSharing: config.startScreenSharing,
813
-            startWithAudioMuted: getStartWithAudioMuted(state)
814
-                || isUserInteractionRequiredForUnmute(state),
815
-            startWithVideoMuted: getStartWithVideoMuted(state)
816
-                || isUserInteractionRequiredForUnmute(state)
802
+            startWithAudioMuted: getStartWithAudioMuted(state) || isUserInteractionRequiredForUnmute(state),
803
+            startWithVideoMuted: getStartWithVideoMuted(state) || isUserInteractionRequiredForUnmute(state)
817
         };
804
         };
818
 
805
 
819
         this.roomName = roomName;
806
         this.roomName = roomName;
820
 
807
 
821
         try {
808
         try {
822
-            // Initialize the device list first. This way, when creating tracks
823
-            // based on preferred devices, loose label matching can be done in
824
-            // cases where the exact ID match is no longer available, such as
825
-            // when the camera device has switched USB ports.
826
-            // when in startSilent mode we want to start with audio muted
809
+            // Initialize the device list first. This way, when creating tracks based on preferred devices, loose label
810
+            // matching can be done in cases where the exact ID match is no longer available, such as -
811
+            // 1. When the camera device has switched USB ports.
812
+            // 2. When in startSilent mode we want to start with audio muted
827
             await this._initDeviceList();
813
             await this._initDeviceList();
828
         } catch (error) {
814
         } catch (error) {
829
             logger.warn('initial device list initialization failed', error);
815
             logger.warn('initial device list initialization failed', error);
830
         }
816
         }
831
 
817
 
832
-        const handleStartAudioMuted = (options, tracks) => {
833
-            if (options.startWithAudioMuted) {
818
+        // Filter out the local tracks based on various config options, i.e., when user joins muted or is muted by
819
+        // focus. However, audio track will always be created even though it is not added to the conference since we
820
+        // want audio related features (noisy mic, talk while muted, etc.) to work even if the mic is muted.
821
+        const handleInitialTracks = (options, tracks) => {
822
+            let localTracks = tracks;
823
+
824
+            // No local tracks are added when user joins as a visitor.
825
+            if (iAmVisitor(state)) {
826
+                return [];
827
+            }
828
+            if (options.startWithAudioMuted || room?.isStartAudioMuted()) {
834
                 // Always add the track on Safari because of a known issue where audio playout doesn't happen
829
                 // Always add the track on Safari because of a known issue where audio playout doesn't happen
835
                 // if the user joins audio and video muted, i.e., if there is no local media capture.
830
                 // if the user joins audio and video muted, i.e., if there is no local media capture.
836
                 if (browser.isWebKitBased()) {
831
                 if (browser.isWebKitBased()) {
837
                     this.muteAudio(true, true);
832
                     this.muteAudio(true, true);
838
                 } else {
833
                 } else {
839
-                    return tracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
834
+                    localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
840
                 }
835
                 }
841
             }
836
             }
837
+            if (room?.isStartVideoMuted()) {
838
+                localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.VIDEO);
839
+            }
842
 
840
 
843
-            return tracks;
841
+            return localTracks;
844
         };
842
         };
845
 
843
 
846
         if (isPrejoinPageVisible(state)) {
844
         if (isPrejoinPageVisible(state)) {
847
             _connectionPromise = connect(roomName).then(c => {
845
             _connectionPromise = connect(roomName).then(c => {
848
-                // we want to initialize it early, in case of errors to be able
849
-                // to gather logs
846
+                // We want to initialize it early, in case of errors to be able to gather logs.
850
                 APP.connection = c;
847
                 APP.connection = c;
851
 
848
 
852
                 return c;
849
                 return c;
859
             APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
856
             APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
860
 
857
 
861
             const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
858
             const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
862
-            const tracks = await tryCreateLocalTracks;
859
+            const localTracks = await tryCreateLocalTracks;
863
 
860
 
864
-            // Initialize device list a second time to ensure device labels
865
-            // get populated in case of an initial gUM acceptance; otherwise
866
-            // they may remain as empty strings.
861
+            // Initialize device list a second time to ensure device labels get populated in case of an initial gUM
862
+            // acceptance; otherwise they may remain as empty strings.
867
             this._initDeviceList(true);
863
             this._initDeviceList(true);
868
 
864
 
869
             if (isPrejoinPageVisible(state)) {
865
             if (isPrejoinPageVisible(state)) {
870
-                return APP.store.dispatch(initPrejoin(tracks, errors));
866
+                return APP.store.dispatch(initPrejoin(localTracks, errors));
871
             }
867
             }
872
 
868
 
873
             logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
869
             logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
874
 
870
 
875
             this._displayErrorsForCreateInitialLocalTracks(errors);
871
             this._displayErrorsForCreateInitialLocalTracks(errors);
876
 
872
 
877
-            let localTracks = handleStartAudioMuted(initialOptions, tracks);
878
-
879
-            // In case where gUM is slow and resolves after the startAudio/VideoMuted coming from jicofo, we can be
880
-            // join unmuted even though jicofo had instruct us to mute, so let's respect that before passing the tracks
881
-            if (!browser.isWebKitBased()) {
882
-                if (room?.isStartAudioMuted()) {
883
-                    localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
884
-                }
885
-            }
886
-
887
-            if (room?.isStartVideoMuted()) {
888
-                localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.VIDEO);
889
-            }
890
-
891
-            // Do not add the tracks if the user has joined the call as a visitor.
892
-            if (iAmVisitor(state)) {
893
-                return Promise.resolve();
894
-            }
895
-
896
-            return this._setLocalAudioVideoStreams(localTracks);
873
+            return this._setLocalAudioVideoStreams(handleInitialTracks(initialOptions, localTracks));
897
         }
874
         }
898
 
875
 
899
         const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(roomName, initialOptions);
876
         const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(roomName, initialOptions);
900
 
877
 
901
         this._initDeviceList(true);
878
         this._initDeviceList(true);
902
 
879
 
903
-        return this.startConference(con, handleStartAudioMuted(initialOptions, tracks));
880
+        return this.startConference(con, handleInitialTracks(initialOptions, tracks));
904
     },
881
     },
905
 
882
 
906
     /**
883
     /**

+ 63
- 51
react/features/base/tracks/functions.web.ts 查看文件

1
 import { IStore } from '../../app/types';
1
 import { IStore } from '../../app/types';
2
 import { IStateful } from '../app/types';
2
 import { IStateful } from '../app/types';
3
 import { isMobileBrowser } from '../environment/utils';
3
 import { isMobileBrowser } from '../environment/utils';
4
-import JitsiMeetJS from '../lib-jitsi-meet';
4
+import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
5
 import { setAudioMuted } from '../media/actions';
5
 import { setAudioMuted } from '../media/actions';
6
+import { MEDIA_TYPE } from '../media/constants';
7
+import { getStartWithAudioMuted } from '../media/functions';
6
 import { toState } from '../redux/functions';
8
 import { toState } from '../redux/functions';
7
 import {
9
 import {
8
     getUserSelectedCameraDeviceId,
10
     getUserSelectedCameraDeviceId,
94
 }
96
 }
95
 
97
 
96
 /**
98
 /**
97
- * Returns an object containing a promise which resolves with the created tracks &
98
- * the errors resulting from that process.
99
+ * Returns an object containing a promise which resolves with the created tracks and the errors resulting from that
100
+ * process.
99
  *
101
  *
100
- * @returns {Promise<JitsiLocalTrack>}
102
+ * @returns {Promise<JitsiLocalTrack[]>}
101
  *
103
  *
102
  * @todo Refactor to not use APP.
104
  * @todo Refactor to not use APP.
103
  */
105
  */
106
     const initialDevices = [ 'audio' ];
108
     const initialDevices = [ 'audio' ];
107
     const requestedAudio = true;
109
     const requestedAudio = true;
108
     let requestedVideo = false;
110
     let requestedVideo = false;
109
-    const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
111
+    const { startAudioOnly, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
112
+    const startWithAudioMuted = getStartWithAudioMuted(APP.store.getState());
113
+
114
+    // On Electron there is no permission prompt for granting permissions. That's why we don't need to
115
+    // spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
116
+    // probably never resolve.
117
+    const timeout = browser.isElectron() ? 15000 : 60000;
110
 
118
 
111
     // Always get a handle on the audio input device so that we have statistics even if the user joins the
119
     // Always get a handle on the audio input device so that we have statistics even if the user joins the
112
     // conference muted. Previous implementation would only acquire the handle when the user first unmuted,
120
     // conference muted. Previous implementation would only acquire the handle when the user first unmuted,
121
         requestedVideo = true;
129
         requestedVideo = true;
122
     }
130
     }
123
 
131
 
124
-    let tryCreateLocalTracks;
132
+    let tryCreateLocalTracks: any = Promise.resolve([]);
125
 
133
 
126
-    if (!requestedAudio && !requestedVideo) {
127
-        // Resolve with no tracks
128
-        tryCreateLocalTracks = Promise.resolve([]);
129
-    } else {
134
+    if (requestedAudio || requestedVideo) {
130
         tryCreateLocalTracks = createLocalTracksF({
135
         tryCreateLocalTracks = createLocalTracksF({
131
             devices: initialDevices,
136
             devices: initialDevices,
132
-            firePermissionPromptIsShownEvent: true
137
+            firePermissionPromptIsShownEvent: true,
138
+            timeout
133
         }, APP.store)
139
         }, APP.store)
134
-                .catch((err: Error) => {
135
-                    if (requestedAudio && requestedVideo) {
140
+        .catch(async (err: Error) => {
141
+            if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
142
+                errors.audioAndVideoError = err;
136
 
143
 
137
-                        // Try audio only...
138
-                        errors.audioAndVideoError = err;
144
+                return [];
145
+            }
139
 
146
 
140
-                        return (
141
-                            createLocalTracksF({
142
-                                devices: [ 'audio' ],
143
-                                firePermissionPromptIsShownEvent: true
144
-                            }));
145
-                    } else if (requestedAudio && !requestedVideo) {
146
-                        errors.audioOnlyError = err;
147
+            // Retry with separate gUM calls.
148
+            const gUMPromises: any = [];
149
+            const tracks: any = [];
147
 
150
 
148
-                        return [];
149
-                    } else if (requestedVideo && !requestedAudio) {
150
-                        errors.videoOnlyError = err;
151
+            if (requestedAudio) {
152
+                gUMPromises.push(createLocalTracksF({
153
+                    devices: [ MEDIA_TYPE.AUDIO ],
154
+                    firePermissionPromptIsShownEvent: true,
155
+                    timeout
156
+                }));
157
+            }
151
 
158
 
152
-                        return [];
153
-                    }
154
-                    logger.error('Should never happen');
155
-                })
156
-                .catch((err: Error) => {
157
-                    // Log this just in case...
158
-                    if (!requestedAudio) {
159
-                        logger.error('The impossible just happened', err);
160
-                    }
161
-                    errors.audioOnlyError = err;
162
-
163
-                    // Try video only...
164
-                    return requestedVideo
165
-                        ? createLocalTracksF({
166
-                            devices: [ 'video' ],
167
-                            firePermissionPromptIsShownEvent: true
168
-                        })
169
-                        : [];
170
-                })
171
-                .catch((err: Error) => {
172
-                    // Log this just in case...
173
-                    if (!requestedVideo) {
174
-                        logger.error('The impossible just happened', err);
159
+            if (requestedVideo) {
160
+                gUMPromises.push(createLocalTracksF({
161
+                    devices: [ MEDIA_TYPE.VIDEO ],
162
+                    firePermissionPromptIsShownEvent: true,
163
+                    timeout
164
+                }));
165
+            }
166
+
167
+            const results = await Promise.allSettled(gUMPromises);
168
+            let errorMsg;
169
+
170
+            results.forEach((result, idx) => {
171
+                if (result.status === 'fulfilled') {
172
+                    tracks.push(result.value[0]);
173
+                } else {
174
+                    errorMsg = result.reason;
175
+                    const isAudio = idx === 0;
176
+
177
+                    logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
178
+                    if (isAudio) {
179
+                        errors.audioOnlyError = errorMsg;
180
+                    } else {
181
+                        errors.videoOnlyError = errorMsg;
175
                     }
182
                     }
176
-                    errors.videoOnlyError = err;
183
+                }
184
+            });
185
+
186
+            if (errors.audioOnlyError && errors.videoOnlyError) {
187
+                errors.audioAndVideoError = errorMsg;
188
+            }
177
 
189
 
178
-                    return [];
179
-                });
190
+            return tracks;
191
+        });
180
     }
192
     }
181
 
193
 
182
     return {
194
     return {

+ 16
- 16
react/features/prejoin/reducer.ts 查看文件

165
 function getStatusFromErrors(errors: {
165
 function getStatusFromErrors(errors: {
166
     audioAndVideoError?: { message: string; };
166
     audioAndVideoError?: { message: string; };
167
     audioOnlyError?: { message: string; };
167
     audioOnlyError?: { message: string; };
168
-    videoOnlyError?: Object; }
168
+    videoOnlyError?: { message: string; }; }
169
 ) {
169
 ) {
170
     const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
170
     const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
171
 
171
 
172
     if (audioAndVideoError) {
172
     if (audioAndVideoError) {
173
-        if (audioOnlyError) {
174
-            if (videoOnlyError) {
175
-                return {
176
-                    deviceStatusType: 'warning',
177
-                    deviceStatusText: 'prejoin.audioAndVideoError',
178
-                    rawError: audioAndVideoError.message
179
-                };
180
-            }
173
+        return {
174
+            deviceStatusType: 'warning',
175
+            deviceStatusText: 'prejoin.audioAndVideoError',
176
+            rawError: audioAndVideoError.message
177
+        };
178
+    }
181
 
179
 
182
-            return {
183
-                deviceStatusType: 'warning',
184
-                deviceStatusText: 'prejoin.audioOnlyError',
185
-                rawError: audioOnlyError.message
186
-            };
187
-        }
180
+    if (audioOnlyError) {
181
+        return {
182
+            deviceStatusType: 'warning',
183
+            deviceStatusText: 'prejoin.audioOnlyError',
184
+            rawError: audioOnlyError.message
185
+        };
186
+    }
188
 
187
 
188
+    if (videoOnlyError) {
189
         return {
189
         return {
190
             deviceStatusType: 'warning',
190
             deviceStatusType: 'warning',
191
             deviceStatusText: 'prejoin.videoOnlyError',
191
             deviceStatusText: 'prejoin.videoOnlyError',
192
-            rawError: audioAndVideoError.message
192
+            rawError: videoOnlyError.message
193
         };
193
         };
194
     }
194
     }
195
 
195
 

正在加载...
取消
保存