fix(visitors): Use single GUM for enabling media on promotion.

Before we were using setAudioMuted and setVideoMuted which was effectively using separate GUM calls for audio and video. This was problematic in the case where GUM permissions prompt was displayed because two separate prompts were displayed.
pull/14732/head
Hristo Terezov 1 year ago
parent e90b270b32
commit 53299a19c2
  1. 167
      conference.js
  2. 25
      react/features/base/conference/actions.any.ts
  3. 29
      react/features/base/conference/actions.native.ts
  4. 31
      react/features/base/conference/actions.web.ts
  5. 267
      react/features/base/tracks/actions.web.ts
  6. 13
      react/features/base/tracks/types.ts
  7. 12
      react/features/notifications/constants.ts

@ -4,7 +4,6 @@ import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from '@jitsi/logger';
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from './modules/UI/UIErrors';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import Recorder from './modules/recorder/Recorder';
import { createTaskQueue } from './modules/util/helpers';
@ -76,7 +75,6 @@ import {
JitsiConferenceEvents,
JitsiE2ePingEvents,
JitsiMediaDevicesEvents,
JitsiTrackErrors,
JitsiTrackEvents,
browser
} from './react/features/base/lib-jitsi-meet';
@ -117,8 +115,11 @@ import {
import { updateSettings } from './react/features/base/settings/actions';
import {
addLocalTrack,
createInitialAVTracks,
destroyLocalTracks,
displayErrorsForCreateInitialLocalTracks,
replaceLocalTrack,
setGUMPendingStateOnFailedTracks,
toggleScreensharing as toggleScreensharingA,
trackAdded,
trackRemoved
@ -383,27 +384,6 @@ function disconnect() {
return APP.connection.disconnect().then(onDisconnected, onDisconnected);
}
/**
* 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.
* @returns {void}
*/
function setGUMPendingStateOnFailedTracks(tracks) {
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));
APP.store.dispatch(gumPending(nonPendingTracks, IGUMPendingState.NONE));
}
export default {
/**
* Flag used to delay modification of the muted status of local media tracks
@ -502,57 +482,12 @@ export default {
return [];
});
} else if (requestedAudio || requestedVideo) {
APP.store.dispatch(gumPending(initialDevices, IGUMPendingState.PENDING_UNMUTE));
tryCreateLocalTracks = createLocalTracksF({
tryCreateLocalTracks = APP.store.dispatch(createInitialAVTracks({
devices: initialDevices,
timeout,
firePermissionPromptIsShownEvent: true
})
.catch(async error => {
if (error.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
errors.audioAndVideoError = error;
return [];
}
// Retry with separate gUM calls.
const gUMPromises = [];
const tracks = [];
if (requestedAudio) {
gUMPromises.push(createLocalTracksF(audioOptions));
}
if (requestedVideo) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.VIDEO ],
timeout,
firePermissionPromptIsShownEvent: true
}));
}
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;
}
})).then(({ tracks, errors: pErrors }) => {
Object.assign(errors, pErrors);
return tracks;
});
@ -573,42 +508,6 @@ export default {
};
},
/**
* Displays error notifications according to the state carried by {@code errors} object returned
* by {@link createInitialLocalTracks}.
* @param {Object} errors - the errors (if any) returned by {@link createInitialLocalTracks}.
*
* @returns {void}
* @private
*/
_displayErrorsForCreateInitialLocalTracks(errors) {
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) {
this._handleScreenSharingError(screenSharingError);
}
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.
APP.store.dispatch(notifyMicError(audioOnlyError));
APP.store.dispatch(notifyCameraError(videoOnlyError));
} else {
// 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.
APP.store.dispatch(notifyCameraError(audioAndVideoError));
}
}
},
startConference(tracks) {
tracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
@ -724,11 +623,11 @@ export default {
logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
this._displayErrorsForCreateInitialLocalTracks(errors);
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
const tracks = handleInitialTracks(initialOptions, localTracks);
setGUMPendingStateOnFailedTracks(tracks);
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
return this._setLocalAudioVideoStreams(tracks);
}
@ -737,7 +636,7 @@ export default {
return Promise.all([
tryCreateLocalTracks.then(tr => {
this._displayErrorsForCreateInitialLocalTracks(errors);
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
return tr;
}).then(tr => {
@ -745,7 +644,7 @@ export default {
const filteredTracks = handleInitialTracks(initialOptions, tr);
setGUMPendingStateOnFailedTracks(filteredTracks);
setGUMPendingStateOnFailedTracks(filteredTracks, APP.store.dispatch);
return filteredTracks;
}),
@ -1226,7 +1125,7 @@ export default {
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
const localTracks = await tryCreateLocalTracks;
this._displayErrorsForCreateInitialLocalTracks(errors);
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
localTracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
@ -1566,50 +1465,6 @@ export default {
});
},
/**
* Handles {@link JitsiTrackError} returned by the lib-jitsi-meet when
* trying to create screensharing track. It will either do nothing if
* the dialog was canceled on user's request or display an error if
* screensharing couldn't be started.
* @param {JitsiTrackError} error - The error returned by
* {@link _createDesktopTrack} Promise.
* @private
*/
_handleScreenSharingError(error) {
if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
return;
}
logger.error('failed to share local desktop', error);
// Handling:
// JitsiTrackErrors.CONSTRAINT_FAILED
// JitsiTrackErrors.PERMISSION_DENIED
// JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR
// and any other
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';
}
APP.store.dispatch(showErrorNotification({
descriptionKey,
titleKey
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
},
/**
* Setup interaction between conference and UI.
*/

@ -10,14 +10,12 @@ import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
import { hasAvailableDevices } from '../devices/functions.any';
import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
import {
gumPending,
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../media/actions';
import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import { MEDIA_TYPE, MediaType } from '../media/constants';
import {
dominantSpeakerChanged,
participantKicked,
@ -72,6 +70,7 @@ import {
SET_START_REACTIONS_MUTED,
UPDATE_CONFERENCE_METADATA
} from './actionTypes';
import { setupVisitorStartupMedia } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@ -1060,9 +1059,7 @@ export function redirect(vnode: string, focusJid: string, username: string) {
.then(() => dispatch(conferenceWillInit()))
.then(() => dispatch(connect()))
.then(() => {
// Clear the gum pending state in case we have set it to pending since we are starting the
// conference without tracks.
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
const media: Array<MediaType> = [];
if (!vnode) {
const state = getState();
@ -1076,10 +1073,7 @@ export function redirect(vnode: string, focusJid: string, username: string) {
// do not unmute the user if he was muted before (on the prejoin, the config
// or URL param, etc.)
if (!unmuteBlocked && !muted && !startSilent && available) {
dispatch(setAudioMuted(false, true));
// // FIXME: The old conference logic still relies on this event being emitted.
typeof APP === 'undefined' || APP.conference.muteAudio(false);
media.push(MEDIA_TYPE.AUDIO);
}
}
@ -1089,18 +1083,13 @@ export function redirect(vnode: string, focusJid: string, username: string) {
// do not unmute the user if he was muted before (on the prejoin, the config, URL param or
// audo only, etc)
if (!unmuteBlocked && !muted && hasAvailableDevices(state, 'videoInput')) {
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
// // FIXME: The old conference logic still relies on this event being emitted.
typeof APP === 'undefined' || APP.conference.muteVideo(false, false);
media.push(MEDIA_TYPE.VIDEO);
}
}
}
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
dispatch(setupVisitorStartupMedia(media));
});
};
}

@ -0,0 +1,29 @@
import { IStore } from '../../app/types';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_MUTISM_AUTHORITY } from '../media/constants';
export * from './actions.any';
/**
* Starts audio and/or video for the visitor.
*
* @param {Array<MediaType>} mediaTypes - The media types that need to be started.
* @returns {Function}
*/
export function setupVisitorStartupMedia(mediaTypes: Array<MediaType>) {
return (dispatch: IStore['dispatch']) => {
if (!mediaTypes || !Array.isArray(mediaTypes)) {
return;
}
mediaTypes.forEach(mediaType => {
switch (mediaType) {
case MEDIA_TYPE.AUDIO:
dispatch(setAudioMuted(false, true));
break;
case MEDIA_TYPE.VIDEO:
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
}
});
};
}

@ -0,0 +1,31 @@
import { IStore } from '../../app/types';
import { gumPending } from '../media/actions';
import { MEDIA_TYPE, MediaType } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import { createAndAddInitialAVTracks } from '../tracks/actions.web';
export * from './actions.any';
/**
* Starts audio and/or video for the visitor.
*
* @param {Array<MediaType>} media - The media types that need to be started.
* @returns {Function}
*/
export function setupVisitorStartupMedia(media: Array<MediaType>) {
return (dispatch: IStore['dispatch']) => {
// Clear the gum pending state in case we have set it to pending since we are starting the
// conference without tracks.
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
if (media && Array.isArray(media) && media.length > 0) {
dispatch(createAndAddInitialAVTracks(media));
}
// FIXME: The name of the function doesn't fit the startConference execution but another PR will removes
// this and calls startConference based on the connection status. This will stay here temporary.
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
};
}

@ -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, showNotification } 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) {
_handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
dispatch(handleScreenSharingError(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));
};
}

@ -72,3 +72,16 @@ export interface IShareOptions {
desktopSharingSources?: string[];
desktopStream?: any;
}
export interface ICreateInitialTracksOptions {
devices: Array<MediaType>;
firePermissionPromptIsShownEvent?: boolean;
timeout?: number;
}
export interface IInitialTracksErrors {
audioAndVideoError?: Error;
audioOnlyError: Error;
screenSharingError: Error;
videoOnlyError: Error;
}

@ -11,12 +11,12 @@ export const NOTIFICATION_TIMEOUT = {
/**
* Notification timeout type.
*/
export const NOTIFICATION_TIMEOUT_TYPE = {
SHORT: 'short',
MEDIUM: 'medium',
LONG: 'long',
STICKY: 'sticky'
};
export enum NOTIFICATION_TIMEOUT_TYPE {
LONG = 'long',
MEDIUM = 'medium',
SHORT = 'short',
STICKY = 'sticky'
}
/**
* The set of possible notification types.

Loading…
Cancel
Save