diff --git a/conference.js b/conference.js index 62a169fb0..9a523ab67 100644 --- a/conference.js +++ b/conference.js @@ -51,6 +51,8 @@ import { import { checkAndNotifyForNewDevice, getAvailableDevices, + notifyCameraError, + notifyMicError, setAudioOutputDeviceId, updateDeviceList } from './react/features/base/devices'; @@ -694,13 +696,14 @@ export default { // 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.UI.showMicErrorNotification(audioOnlyError); - APP.UI.showCameraErrorNotification(videoOnlyError); + 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.UI.showCameraErrorNotification(audioAndVideoError); + APP.store.dispatch( + notifyCameraError(audioAndVideoError)); } } @@ -839,7 +842,7 @@ export default { if (!this.localAudio && !mute) { const maybeShowErrorDialog = error => { - showUI && APP.UI.showMicErrorNotification(error); + showUI && APP.store.dispatch(notifyMicError(error)); }; createLocalTracksF({ devices: [ 'audio' ] }, false) @@ -902,7 +905,7 @@ export default { if (!this.localVideo && !mute) { const maybeShowErrorDialog = error => { - showUI && APP.UI.showCameraErrorNotification(error); + showUI && APP.store.dispatch(notifyCameraError(error)); }; // Try to create local video if there wasn't any. @@ -2109,7 +2112,7 @@ export default { this._updateVideoDeviceId(); }) .catch(err => { - APP.UI.showCameraErrorNotification(err); + APP.store.dispatch(notifyCameraError(err)); }); } ); @@ -2142,7 +2145,7 @@ export default { this._updateAudioDeviceId(); }) .catch(err => { - APP.UI.showMicErrorNotification(err); + APP.store.dispatch(notifyMicError(err)); }); } ); diff --git a/doc/api.md b/doc/api.md index 372545127..826bd3b23 100644 --- a/doc/api.md +++ b/doc/api.md @@ -264,6 +264,14 @@ The `event` parameter is a String object with the name of the event. The `listener` parameter is a Function object with one argument that will be notified when the event occurs with data related to the event. The following events are currently supported: +* **cameraError** - event notifications about Jitsi-Meet having failed to access the camera. The listener will receive an object with the following structure: +```javascript +{ + type: string, // A constant representing the overall type of the error. + message: string // Additional information about the error. +} +``` + * **avatarChanged** - event notifications about avatar changes. The listener will receive an object with the following structure: ```javascript @@ -287,6 +295,14 @@ changes. The listener will receive an object with the following structure: } ``` +* **micError** - event notifications about Jitsi-Meet having failed to access the mic. The listener will receive an object with the following structure: +```javascript +{ + type: string, // A constant representing the overall type of the error. + message: string // Additional information about the error. +} +``` + * **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure: ```javascript { diff --git a/modules/API/API.js b/modules/API/API.js index fe45efef6..fd29f71ba 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -559,6 +559,38 @@ class API { }); } + /** + * Notify external application of an unexpected camera-related error having + * occurred. + * + * @param {string} type - The type of the camera error. + * @param {string} message - Additional information about the error. + * @returns {void} + */ + notifyOnCameraError(type: string, message: string) { + this._sendEvent({ + name: 'camera-error', + type, + message + }); + } + + /** + * Notify external application of an unexpected mic-related error having + * occurred. + * + * @param {string} type - The type of the mic error. + * @param {string} message - Additional information about the error. + * @returns {void} + */ + notifyOnMicError(type: string, message: string) { + this._sendEvent({ + name: 'mic-error', + type, + message + }); + } + /** * Notify external application (if API is enabled) that conference feedback * has been submitted. Intended to be used in conjunction with the diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index d7ae31387..9bef21501 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -51,6 +51,7 @@ const events = { 'avatar-changed': 'avatarChanged', 'audio-availability-changed': 'audioAvailabilityChanged', 'audio-mute-status-changed': 'audioMuteStatusChanged', + 'camera-error': 'cameraError', 'device-list-changed': 'deviceListChanged', 'display-name-change': 'displayNameChange', 'email-change': 'emailChange', @@ -58,6 +59,7 @@ const events = { 'feedback-prompt-displayed': 'feedbackPromptDisplayed', 'filmstrip-display-changed': 'filmstripDisplayChanged', 'incoming-message': 'incomingMessage', + 'mic-error': 'micError', 'outgoing-message': 'outgoingMessage', 'participant-joined': 'participantJoined', 'participant-left': 'participantLeft', diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 373cf9c44..4a3144bf5 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -13,16 +13,12 @@ import SharedVideoManager from './shared_video/SharedVideo'; import VideoLayout from './videolayout/VideoLayout'; import Filmstrip from './videolayout/Filmstrip'; -import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet'; import { getLocalParticipant } from '../../react/features/base/participants'; import { toggleChat } from '../../react/features/chat'; import { openDisplayNamePrompt } from '../../react/features/display-name'; import { setEtherpadHasInitialzied } from '../../react/features/etherpad'; import { setFilmstripVisible } from '../../react/features/filmstrip'; -import { - setNotificationsEnabled, - showWarningNotification -} from '../../react/features/notifications'; +import { setNotificationsEnabled } from '../../react/features/notifications'; import { dockToolbox, setToolboxEnabled, @@ -40,39 +36,6 @@ UI.eventEmitter = eventEmitter; let etherpadManager; let sharedVideoManager; -const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = { - microphone: {}, - camera: {} -}; - -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .camera[JitsiTrackErrors.UNSUPPORTED_RESOLUTION] - = 'dialog.cameraUnsupportedResolutionError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.GENERAL] - = 'dialog.cameraUnknownError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.PERMISSION_DENIED] - = 'dialog.cameraPermissionDeniedError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.NOT_FOUND] - = 'dialog.cameraNotFoundError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.CONSTRAINT_FAILED] - = 'dialog.cameraConstraintFailedError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .camera[JitsiTrackErrors.NO_DATA_FROM_SOURCE] - = 'dialog.cameraNotSendingData'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.GENERAL] - = 'dialog.micUnknownError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .microphone[JitsiTrackErrors.PERMISSION_DENIED] - = 'dialog.micPermissionDeniedError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.NOT_FOUND] - = 'dialog.micNotFoundError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .microphone[JitsiTrackErrors.CONSTRAINT_FAILED] - = 'dialog.micConstraintFailedError'; -JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .microphone[JitsiTrackErrors.NO_DATA_FROM_SOURCE] - = 'dialog.micNotSendingData'; - const UIListeners = new Map([ [ UIEvents.ETHERPAD_CLICKED, @@ -774,65 +737,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) { }); }; -/** - * Shows a notifications about the passed in microphone error. - * - * @param {JitsiTrackError} micError - An error object related to using or - * acquiring an audio stream. - * @returns {void} - */ -UI.showMicErrorNotification = function(micError) { - if (!micError) { - return; - } - - const { message, name } = micError; - - const micJitsiTrackErrorMsg - = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name]; - const micErrorMsg = micJitsiTrackErrorMsg - || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .microphone[JitsiTrackErrors.GENERAL]; - const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message; - - APP.store.dispatch(showWarningNotification({ - description: additionalMicErrorMsg, - descriptionKey: micErrorMsg, - titleKey: name === JitsiTrackErrors.PERMISSION_DENIED - ? 'deviceError.microphonePermission' - : 'deviceError.microphoneError' - })); -}; - -/** - * Shows a notifications about the passed in camera error. - * - * @param {JitsiTrackError} cameraError - An error object related to using or - * acquiring a video stream. - * @returns {void} - */ -UI.showCameraErrorNotification = function(cameraError) { - if (!cameraError) { - return; - } - - const { message, name } = cameraError; - - const cameraJitsiTrackErrorMsg - = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name]; - const cameraErrorMsg = cameraJitsiTrackErrorMsg - || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP - .camera[JitsiTrackErrors.GENERAL]; - const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message; - - APP.store.dispatch(showWarningNotification({ - description: additionalCameraErrorMsg, - descriptionKey: cameraErrorMsg, - titleKey: name === JitsiTrackErrors.PERMISSION_DENIED - ? 'deviceError.cameraPermission' : 'deviceError.cameraError' - })); -}; - /** * Shows error dialog that informs the user that no data is received from the * device. diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index 03627cef8..ceb44b464 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -1,6 +1,10 @@ /* global APP, JitsiMeetJS */ -import { getAudioOutputDeviceId } from '../../react/features/base/devices'; +import { + getAudioOutputDeviceId, + notifyCameraError, + notifyMicError +} from '../../react/features/base/devices'; import { getUserSelectedCameraDeviceId, getUserSelectedMicDeviceId, @@ -176,11 +180,11 @@ export default { ])) .then(tracks => { if (audioTrackError) { - APP.UI.showMicErrorNotification(audioTrackError); + APP.store.dispatch(notifyMicError(audioTrackError)); } if (videoTrackError) { - APP.UI.showCameraErrorNotification(videoTrackError); + APP.store.dispatch(notifyCameraError(videoTrackError)); } return tracks.filter(t => typeof t !== 'undefined'); @@ -205,7 +209,7 @@ export default { }) .catch(err => { audioTrackError = err; - showError && APP.UI.showMicErrorNotification(err); + showError && APP.store.disptach(notifyMicError(err)); return []; })); @@ -223,7 +227,7 @@ export default { }) .catch(err => { videoTrackError = err; - showError && APP.UI.showCameraErrorNotification(err); + showError && APP.store.dispatch(notifyCameraError(err)); return []; })); diff --git a/react/features/app/components/App.web.js b/react/features/app/components/App.web.js index 91fdfee0e..dc3034829 100644 --- a/react/features/app/components/App.web.js +++ b/react/features/app/components/App.web.js @@ -6,6 +6,7 @@ import React from 'react'; import { DialogContainer } from '../../base/dialog'; import '../../base/responsive-ui'; import '../../chat'; +import '../../external-api'; import '../../room-lock'; import '../../video-layout'; diff --git a/react/features/base/devices/actionTypes.js b/react/features/base/devices/actionTypes.js index 942c1414f..5e24ae45e 100644 --- a/react/features/base/devices/actionTypes.js +++ b/react/features/base/devices/actionTypes.js @@ -1,3 +1,25 @@ +/** + * The type of Redux action which signals that an error occurred while obtaining + * a camera. + * + * { + * type: NOTIFY_CAMERA_ERROR, + * error: Object + * } + */ +export const NOTIFY_CAMERA_ERROR = 'NOTIFY_CAMERA_ERROR'; + +/** + * The type of Redux action which signals that an error occurred while obtaining + * a microphone. + * + * { + * type: NOTIFY_MIC_ERROR, + * error: Object + * } + */ +export const NOTIFY_MIC_ERROR = 'NOTIFY_MIC_ERROR'; + /** * The type of Redux action which signals that the currently used audio * input device should be changed. diff --git a/react/features/base/devices/actions.js b/react/features/base/devices/actions.js index 20b8dec0d..28203cb18 100644 --- a/react/features/base/devices/actions.js +++ b/react/features/base/devices/actions.js @@ -7,6 +7,8 @@ import { import { ADD_PENDING_DEVICE_REQUEST, CHECK_AND_NOTIFY_FOR_NEW_DEVICE, + NOTIFY_CAMERA_ERROR, + NOTIFY_MIC_ERROR, REMOVE_PENDING_DEVICE_REQUESTS, SET_AUDIO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE, @@ -148,6 +150,43 @@ export function getAvailableDevices() { }); } +/** + * Signals that an error occurred while trying to obtain a track from a camera. + * + * @param {Object} error - The device error, as provided by lib-jitsi-meet. + * @param {string} error.name - The constant for the type of the error. + * @param {string} error.message - Optional additional information about the + * error. + * @returns {{ + * type: NOTIFY_CAMERA_ERROR, + * error: Object + * }} + */ +export function notifyCameraError(error) { + return { + type: NOTIFY_CAMERA_ERROR, + error + }; +} + +/** + * Signals that an error occurred while trying to obtain a track from a mic. + * + * @param {Object} error - The device error, as provided by lib-jitsi-meet. + * @param {Object} error.name - The constant for the type of the error. + * @param {string} error.message - Optional additional information about the + * error. + * @returns {{ + * type: NOTIFY_MIC_ERROR, + * error: Object + * }} + */ +export function notifyMicError(error) { + return { + type: NOTIFY_MIC_ERROR, + error + }; +} /** * Remove all pending device requests. diff --git a/react/features/base/devices/middleware.js b/react/features/base/devices/middleware.js index 9a8962d2f..b6a0cca3a 100644 --- a/react/features/base/devices/middleware.js +++ b/react/features/base/devices/middleware.js @@ -4,6 +4,7 @@ import { CONFERENCE_JOINED } from '../conference'; import { processExternalDeviceRequest } from '../../device-selection'; import { MiddlewareRegistry } from '../redux'; import UIEvents from '../../../../service/UI/UIEvents'; +import { JitsiTrackErrors } from '../lib-jitsi-meet'; import { removePendingDeviceRequests, @@ -12,15 +13,35 @@ import { } from './actions'; import { CHECK_AND_NOTIFY_FOR_NEW_DEVICE, + NOTIFY_CAMERA_ERROR, + NOTIFY_MIC_ERROR, SET_AUDIO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE } from './actionTypes'; -import { showNotification } from '../../notifications'; +import { showNotification, showWarningNotification } from '../../notifications'; import { updateSettings } from '../settings'; import { setAudioOutputDeviceId } from './functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); +const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = { + microphone: { + [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError', + [JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError', + [JitsiTrackErrors.NO_DATA_FROM_SOURCE]: 'dialog.micNotSendingData', + [JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError', + [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError' + }, + camera: { + [JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError', + [JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError', + [JitsiTrackErrors.NO_DATA_FROM_SOURCE]: 'dialog.cameraNotSendingData', + [JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError', + [JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError', + [JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError' + } +}; + /** * Implements the middleware of the feature base/devices. * @@ -32,6 +53,53 @@ MiddlewareRegistry.register(store => next => action => { switch (action.type) { case CONFERENCE_JOINED: return _conferenceJoined(store, next, action); + case NOTIFY_CAMERA_ERROR: { + if (typeof APP !== 'object' || !action.error) { + break; + } + + const { message, name } = action.error; + + const cameraJitsiTrackErrorMsg + = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name]; + const cameraErrorMsg = cameraJitsiTrackErrorMsg + || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP + .camera[JitsiTrackErrors.GENERAL]; + const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message; + + store.dispatch(showWarningNotification({ + description: additionalCameraErrorMsg, + descriptionKey: cameraErrorMsg, + titleKey: name === JitsiTrackErrors.PERMISSION_DENIED + ? 'deviceError.cameraPermission' : 'deviceError.cameraError' + })); + + break; + } + case NOTIFY_MIC_ERROR: { + if (typeof APP !== 'object' || !action.error) { + break; + } + + const { message, name } = action.error; + + const micJitsiTrackErrorMsg + = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name]; + const micErrorMsg = micJitsiTrackErrorMsg + || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP + .microphone[JitsiTrackErrors.GENERAL]; + const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message; + + store.dispatch(showWarningNotification({ + description: additionalMicErrorMsg, + descriptionKey: micErrorMsg, + titleKey: name === JitsiTrackErrors.PERMISSION_DENIED + ? 'deviceError.microphonePermission' + : 'deviceError.microphoneError' + })); + + break; + } case SET_AUDIO_INPUT_DEVICE: APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId); break; diff --git a/react/features/external-api/index.js b/react/features/external-api/index.js new file mode 100644 index 000000000..d43689289 --- /dev/null +++ b/react/features/external-api/index.js @@ -0,0 +1 @@ +import './middleware'; diff --git a/react/features/external-api/middleware.js b/react/features/external-api/middleware.js new file mode 100644 index 000000000..0f3ad0fb2 --- /dev/null +++ b/react/features/external-api/middleware.js @@ -0,0 +1,30 @@ +// @flow + +import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices'; +import { MiddlewareRegistry } from '../base/redux'; + +declare var APP: Object; + +/** + * The middleware of the feature {@code external-api}. + * + * @returns {Function} + */ +MiddlewareRegistry.register((/* store */) => next => action => { + switch (action.type) { + case NOTIFY_CAMERA_ERROR: + if (action.error) { + APP.API.notifyOnCameraError( + action.error.name, action.error.message); + } + break; + + case NOTIFY_MIC_ERROR: + if (action.error) { + APP.API.notifyOnMicError(action.error.name, action.error.message); + } + break; + } + + return next(action); +});