|
@@ -559,7 +559,7 @@ export default {
|
559
|
559
|
);
|
560
|
560
|
}
|
561
|
561
|
|
562
|
|
- let tryCreateLocalTracks;
|
|
562
|
+ let tryCreateLocalTracks = Promise.resolve([]);
|
563
|
563
|
|
564
|
564
|
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
|
565
|
565
|
// spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
|
|
@@ -600,76 +600,65 @@ export default {
|
600
|
600
|
|
601
|
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
|
604
|
tryCreateLocalTracks = createLocalTracksF({
|
608
|
605
|
devices: initialDevices,
|
609
|
606
|
timeout,
|
610
|
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
|
662
|
tryCreateLocalTracks.then(tracks => {
|
674
|
663
|
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
|
675
|
664
|
|
|
@@ -810,43 +799,51 @@ export default {
|
810
|
799
|
const initialOptions = {
|
811
|
800
|
startAudioOnly: config.startAudioOnly,
|
812
|
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
|
806
|
this.roomName = roomName;
|
820
|
807
|
|
821
|
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
|
813
|
await this._initDeviceList();
|
828
|
814
|
} catch (error) {
|
829
|
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
|
829
|
// Always add the track on Safari because of a known issue where audio playout doesn't happen
|
835
|
830
|
// if the user joins audio and video muted, i.e., if there is no local media capture.
|
836
|
831
|
if (browser.isWebKitBased()) {
|
837
|
832
|
this.muteAudio(true, true);
|
838
|
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
|
844
|
if (isPrejoinPageVisible(state)) {
|
847
|
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
|
847
|
APP.connection = c;
|
851
|
848
|
|
852
|
849
|
return c;
|
|
@@ -859,48 +856,28 @@ export default {
|
859
|
856
|
APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
|
860
|
857
|
|
861
|
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
|
863
|
this._initDeviceList(true);
|
868
|
864
|
|
869
|
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
|
869
|
logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
|
874
|
870
|
|
875
|
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
|
876
|
const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(roomName, initialOptions);
|
900
|
877
|
|
901
|
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
|
/**
|