@ -4,7 +4,7 @@ import { IReduxState, IStore } from '../../app/types';
import { showModeratedNotification } from '../../av-moderation/actions' ;
import { shouldShowModeratedNotification } from '../../av-moderation/functions' ;
import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions' ;
import { showNotification } from '../../notifications/actions' ;
import { showErrorNotification , show Notification } from '../../notifications/actions' ;
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants' ;
import { stopReceiver } from '../../remote-control/actions' ;
import { setScreenAudioShareState , setScreenshareAudioTrack } from '../../screen-share/actions' ;
@ -13,10 +13,12 @@ import { toggleScreenshotCaptureSummary } from '../../screenshot-capture/actions
import { isScreenshotCaptureEnabled } from '../../screenshot-capture/functions' ;
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect' ;
import { getCurrentConference } from '../conference/functions' ;
import { notifyCameraError , notifyMicError } from '../devices/actions.web' ;
import { openDialog } from '../dialog/actions' ;
import { JitsiTrackErrors , JitsiTrackEvents } from '../lib-jitsi-meet' ;
import { setScreenshareMuted } from '../media/actions' ;
import { MEDIA_TYPE , VIDEO_TYPE } from '../media/constants' ;
import { JitsiTrackErrors , JitsiTrackEvents , browser } from '../lib-jitsi-meet' ;
import { gumPending , setScreenshareMuted } from '../media/actions' ;
import { MEDIA_TYPE , MediaType , VIDEO_TYPE } from '../media/constants' ;
import { IGUMPendingState } from '../media/types' ;
import {
addLocalTrack ,
@ -31,7 +33,8 @@ import {
getLocalVideoTrack ,
isToggleCameraEnabled
} from './functions' ;
import { IShareOptions , IToggleScreenSharingOptions } from './types' ;
import logger from './logger' ;
import { ICreateInitialTracksOptions , IInitialTracksErrors , IShareOptions , IToggleScreenSharingOptions } from './types' ;
export * from './actions.any' ;
@ -74,33 +77,6 @@ export function toggleScreensharing(
* @param { Object } store - The redux store .
* @returns { void }
* /
function _handleScreensharingError (
error : Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK ,
{ dispatch } : IStore ) : void {
if ( error . name === JitsiTrackErrors . SCREENSHARING_USER_CANCELED ) {
return ;
}
let descriptionKey , titleKey ;
if ( error . name === JitsiTrackErrors . PERMISSION_DENIED ) {
descriptionKey = 'dialog.screenSharingPermissionDeniedError' ;
titleKey = 'dialog.screenSharingFailedTitle' ;
} else if ( error . name === JitsiTrackErrors . CONSTRAINT_FAILED ) {
descriptionKey = 'dialog.cameraConstraintFailedError' ;
titleKey = 'deviceError.cameraError' ;
} else if ( error . name === JitsiTrackErrors . SCREENSHARING_GENERIC_ERROR ) {
descriptionKey = 'dialog.screenSharingFailed' ;
titleKey = 'dialog.screenSharingFailedTitle' ;
} else if ( error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK ) {
descriptionKey = 'notify.screenShareNoAudio' ;
titleKey = 'notify.screenShareNoAudioTitle' ;
}
dispatch ( showNotification ( {
titleKey ,
descriptionKey
} , NOTIFICATION_TIMEOUT_TYPE . MEDIUM ) ) ;
}
/ * *
@ -128,7 +104,6 @@ async function _maybeApplyAudioMixerEffect(desktopAudioTrack: any, state: IRedux
}
}
/ * *
* Toggles screen sharing .
*
@ -182,7 +157,7 @@ async function _toggleScreenSharing(
try {
tracks = await createLocalTracksF ( options ) as any [ ] ;
} catch ( error ) {
_handleScreensharingError ( error as any , store ) ;
dispatch ( handleScreenSharingError ( error , NOTIFICATION_TIMEOUT_TYPE . MEDIUM ) ) ;
throw error ;
}
@ -196,7 +171,7 @@ async function _toggleScreenSharing(
desktopVideoTrack . dispose ( ) ;
if ( ! desktopAudioTrack ) {
_handleScreens haringError( AUDIO_ONLY_SCREEN_SHARE_NO_TRACK , store ) ;
dispatch ( handleScreenS haringError( AUDIO_ONLY_SCREEN_SHARE_NO_TRACK , NOTIFICATION_TIMEOUT_TYPE . MEDIUM ) ) ;
throw new Error ( AUDIO_ONLY_SCREEN_SHARE_NO_TRACK ) ;
}
@ -317,3 +292,225 @@ export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: stri
initiatorId
} ) ;
}
/ * *
* Sets the GUM pending state for the tracks that have failed .
*
* NOTE : Some of the track that we will be setting to GUM pending state NONE may not have failed but they may have
* been requested . This won ' t be a problem because their current GUM pending state will be NONE anyway .
*
* @param { JitsiLocalTrack } tracks - The tracks that have been created .
* @param { Function } dispatch - The redux dispatch function .
* @returns { void }
* /
export function setGUMPendingStateOnFailedTracks ( tracks : Array < any > , dispatch : IStore [ 'dispatch' ] ) {
const tracksTypes = tracks . map ( track = > {
if ( track . getVideoType ( ) === VIDEO_TYPE . DESKTOP ) {
return MEDIA_TYPE . SCREENSHARE ;
}
return track . getType ( ) ;
} ) ;
const nonPendingTracks = [ MEDIA_TYPE . AUDIO , MEDIA_TYPE . VIDEO ] . filter ( type = > ! tracksTypes . includes ( type ) ) ;
dispatch ( gumPending ( nonPendingTracks , IGUMPendingState . NONE ) ) ;
}
/ * *
* Creates and adds to the conference the initial audio / video tracks .
*
* @param { Array < MediaType > } devices - Array with devices ( audio / video ) that will be used .
* @returns { Function }
* /
export function createAndAddInitialAVTracks ( devices : Array < MediaType > ) {
return ( dispatch : IStore [ 'dispatch' ] ) = > {
dispatch ( gumPending ( devices , IGUMPendingState . PENDING_UNMUTE ) ) ;
return dispatch ( createInitialAVTracks ( { devices } ) )
. then ( ( { tracks , errors } ) = > {
setGUMPendingStateOnFailedTracks ( tracks , dispatch ) ;
dispatch ( displayErrorsForCreateInitialLocalTracks ( errors ) ) ;
return Promise . allSettled ( tracks . map ( ( track : any ) = > {
const legacyConferenceObject = APP . conference ;
if ( track . isAudioTrack ( ) ) {
return legacyConferenceObject . useAudioStream ( track ) ;
}
if ( track . isVideoTrack ( ) ) {
return legacyConferenceObject . useVideoStream ( track ) ;
}
return Promise . resolve ( ) ;
} ) ) ;
} )
. finally ( ( ) = > {
dispatch ( gumPending ( devices , IGUMPendingState . NONE ) ) ;
} ) ;
} ;
}
/ * *
* Creates the initial audio / video tracks .
*
* @param { ICreateInitialTracksOptions } options - Options for creating the audio / video tracks .
* @returns { Function }
* /
export function createInitialAVTracks ( options : ICreateInitialTracksOptions ) {
return ( dispatch : IStore [ 'dispatch' ] , _getState : IStore [ 'getState' ] ) = > {
const {
devices ,
timeout ,
firePermissionPromptIsShownEvent
} = options ;
const audioOptions = {
devices : [ MEDIA_TYPE . AUDIO ] ,
timeout ,
firePermissionPromptIsShownEvent
} ;
dispatch ( gumPending ( devices , IGUMPendingState . PENDING_UNMUTE ) ) ;
return createLocalTracksF ( options ) . then ( tracks = > {
return {
errors : { } as IInitialTracksErrors ,
tracks
} ;
} )
. catch ( async error = > {
const errors = { } as IInitialTracksErrors ;
if ( error . name === JitsiTrackErrors . TIMEOUT && ! browser . isElectron ( ) ) {
errors . audioAndVideoError = error ;
return {
errors ,
tracks : [ ]
} ;
}
// Retry with separate gUM calls.
const gUMPromises = [ ] ;
const tracks : any [ ] | PromiseLike < any [ ] > = [ ] ;
if ( devices . includes ( MEDIA_TYPE . AUDIO ) ) {
gUMPromises . push ( createLocalTracksF ( audioOptions ) ) ;
}
if ( devices . includes ( MEDIA_TYPE . VIDEO ) ) {
gUMPromises . push ( createLocalTracksF ( {
devices : [ MEDIA_TYPE . VIDEO ] ,
timeout ,
firePermissionPromptIsShownEvent
} ) ) ;
}
const results = await Promise . allSettled ( gUMPromises ) ;
let errorMsg ;
results . forEach ( ( result , idx ) = > {
if ( result . status === 'fulfilled' ) {
tracks . push ( result . value [ 0 ] ) ;
} else {
errorMsg = result . reason ;
const isAudio = idx === 0 ;
logger . error ( ` ${ isAudio ? 'Audio' : 'Video' } track creation failed with error ${ errorMsg } ` ) ;
if ( isAudio ) {
errors . audioOnlyError = errorMsg ;
} else {
errors . videoOnlyError = errorMsg ;
}
}
} ) ;
if ( errors . audioOnlyError && errors . videoOnlyError ) {
errors . audioAndVideoError = errorMsg ;
}
return {
tracks ,
errors
} ;
} ) ;
} ;
}
/ * *
* Displays error notifications according to the state carried by the passed { @code errors } object .
*
* @param { InitialTracksErrors } errors - The errors ( if any ) .
* @returns { Function }
* @private
* /
export function displayErrorsForCreateInitialLocalTracks ( errors : IInitialTracksErrors ) {
return ( dispatch : IStore [ 'dispatch' ] ) = > {
const {
audioAndVideoError ,
audioOnlyError ,
screenSharingError ,
videoOnlyError
} = errors ;
// FIXME If there will be microphone error it will cover any screensharing dialog, but it's still better than in
// the reverse order where the screensharing dialog will sometimes be closing the microphone alert
// ($.prompt.close(); is called). Need to figure out dialogs chaining to fix that.
if ( screenSharingError ) {
dispatch ( handleScreenSharingError ( screenSharingError , NOTIFICATION_TIMEOUT_TYPE . LONG ) ) ;
}
if ( audioAndVideoError || audioOnlyError ) {
if ( audioOnlyError || videoOnlyError ) {
// If both requests for 'audio' + 'video' and 'audio' only failed, we assume that there are some
// problems with user's microphone and show corresponding dialog.
dispatch ( notifyMicError ( audioOnlyError ) ) ;
dispatch ( notifyCameraError ( videoOnlyError ) ) ;
} else if ( audioAndVideoError ) {
// If request for 'audio' + 'video' failed, but request for 'audio' only was OK, we assume that we had
// problems with camera and show corresponding dialog.
dispatch ( notifyCameraError ( audioAndVideoError ) ) ;
}
}
} ;
}
/ * *
* Displays a UI notification for screensharing failure based on the error passed .
*
* @private
* @param { Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } error - The error .
* @param { NOTIFICATION_TIMEOUT_TYPE } timeout - The time for showing the notification .
* @returns { Function }
* /
export function handleScreenSharingError (
error : Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK ,
timeout : NOTIFICATION_TIMEOUT_TYPE ) {
return ( dispatch : IStore [ 'dispatch' ] ) = > {
if ( error . name === JitsiTrackErrors . SCREENSHARING_USER_CANCELED ) {
return ;
}
logger . error ( 'failed to share local desktop' , error ) ;
let descriptionKey ;
let titleKey ;
if ( error . name === JitsiTrackErrors . PERMISSION_DENIED ) {
descriptionKey = 'dialog.screenSharingPermissionDeniedError' ;
titleKey = 'dialog.screenSharingFailedTitle' ;
} else if ( error . name === JitsiTrackErrors . CONSTRAINT_FAILED ) {
descriptionKey = 'dialog.cameraConstraintFailedError' ;
titleKey = 'deviceError.cameraError' ;
} else if ( error . name === JitsiTrackErrors . SCREENSHARING_GENERIC_ERROR ) {
descriptionKey = 'dialog.screenSharingFailed' ;
titleKey = 'dialog.screenSharingFailedTitle' ;
} else if ( error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK ) {
descriptionKey = 'notify.screenShareNoAudio' ;
titleKey = 'notify.screenShareNoAudioTitle' ;
}
dispatch ( showErrorNotification ( {
descriptionKey ,
titleKey
} , timeout ) ) ;
} ;
}