diff --git a/conference.js b/conference.js index 52aaea994..ac382127a 100644 --- a/conference.js +++ b/conference.js @@ -33,7 +33,10 @@ import { updateDeviceList } from './react/features/base/devices'; import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-meet'; -import { setVideoAvailable } from './react/features/base/media'; +import { + setAudioAvailable, + setVideoAvailable +} from './react/features/base/media'; import { localParticipantConnectionStatusChanged, localParticipantRoleChanged, @@ -671,24 +674,17 @@ export default { } // if user didn't give access to mic or camera or doesn't have - // them at all, we disable corresponding toolbar buttons + // them at all, we mark corresponding toolbar buttons as muted, + // so that the user can try unmute later on and add audio/video + // to the conference if (!tracks.find((t) => t.isAudioTrack())) { - APP.UI.setMicrophoneButtonEnabled(false); + this.audioMuted = true; + APP.UI.setAudioMuted(this.getMyUserId(), this.audioMuted); } if (!tracks.find((t) => t.isVideoTrack())) { - // Instead of disabling the button we want to show button - // muted, so that the user can have the opportunity to add - // the video later on, even if joined without it. this.videoMuted = true; APP.UI.setVideoMuted(this.getMyUserId(), this.videoMuted); - // FIXME this is a workaround for the situation where - // both audio and video permissions are rejected initially - // and the callback from _initDeviceList will never be - // executed (GUM not initialized - check lib-jitsi-meet). - // The goal here is to disable the video icon in case no - // video permissions were granted. - this.updateVideoIconEnabled(); } this._initDeviceList(); @@ -1156,7 +1152,6 @@ export default { this.isSharingScreen = false; } APP.UI.setVideoMuted(this.getMyUserId(), this.videoMuted); - this.updateVideoIconEnabled(); APP.UI.updateDesktopSharingButtons(); }); }, @@ -1178,7 +1173,6 @@ export default { } else { this.audioMuted = false; } - APP.UI.setMicrophoneButtonEnabled(true); APP.UI.setAudioMuted(this.getMyUserId(), this.audioMuted); }); }, @@ -1799,7 +1793,19 @@ export default { APP.UI.updateDTMFSupport(isDTMFSupported); }); - APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio); + APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => { + if (!localAudio && this.audioMuted && !muted) { + createLocalTracks({ devices: ['audio'] }, false) + .then(([audioTrack]) => { + this.useAudioStream(audioTrack); + }) + .catch(error => { + APP.UI.showDeviceErrorDialog(error, null); + }); + } else { + muteLocalAudio(muted); + } + }); APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => { if (this.isAudioOnly() && !muted) { this._displayAudioOnlyTooltip('videoMute'); @@ -2139,7 +2145,6 @@ export default { mediaDeviceHelper.setCurrentMediaDevices(devices); APP.UI.onAvailableDevicesChanged(devices); APP.store.dispatch(updateDeviceList(devices)); - this.updateVideoIconEnabled(); }); this.deviceChangeListener = (devices) => @@ -2221,9 +2226,31 @@ export default { .then(() => { mediaDeviceHelper.setCurrentMediaDevices(devices); APP.UI.onAvailableDevicesChanged(devices); - this.updateVideoIconEnabled(); }); }, + + /** + * Determines whether or not the audio button should be enabled. + */ + updateAudioIconEnabled() { + const audioMediaDevices + = mediaDeviceHelper.getCurrentMediaDevices().audioinput; + const audioDeviceCount + = audioMediaDevices ? audioMediaDevices.length : 0; + + // The audio functionality is considered available if there are any + // audio devices detected or if the local audio stream already exists. + const available = audioDeviceCount > 0 || Boolean(localAudio); + + logger.debug( + 'Microphone button enabled: ' + available, + 'local audio: ' + localAudio, + 'audio devices: ' + audioMediaDevices, + 'device count: ' + audioDeviceCount); + + APP.store.dispatch(setAudioAvailable(available)); + }, + /** * Determines whether or not the video button should be enabled. */ diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 36c8801fd..4ee560482 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -32,7 +32,6 @@ import { import { checkAutoEnableDesktopSharing, dockToolbox, - setAudioIconEnabled, setToolbarButton, showDialPadButton, showEtherpadButton, @@ -710,9 +709,7 @@ UI.setAudioMuted = function (id, muted) { VideoLayout.onAudioMute(id, muted); if (APP.conference.isLocalId(id)) { APP.store.dispatch(setAudioMuted(muted)); - APP.store.dispatch(setToolbarButton('microphone', { - toggled: muted - })); + APP.conference.updateAudioIconEnabled(); } }; @@ -723,6 +720,7 @@ UI.setVideoMuted = function (id, muted) { VideoLayout.onVideoMute(id, muted); if (APP.conference.isLocalId(id)) { APP.store.dispatch(setVideoMuted(muted)); + APP.conference.updateVideoIconEnabled(); } }; @@ -1096,6 +1094,8 @@ UI.onLocalRaiseHandChanged = function (isRaisedHand) { */ UI.onAvailableDevicesChanged = function (devices) { APP.store.dispatch(updateDeviceList(devices)); + APP.conference.updateAudioIconEnabled(); + APP.conference.updateVideoIconEnabled(); }; /** @@ -1367,15 +1367,6 @@ UI.onSharedVideoStop = function (id, attributes) { sharedVideoManager.onSharedVideoStop(id, attributes); }; -/** - * Enables / disables microphone toolbar button. - * - * @param {boolean} enabled indicates if the microphone button should be - * enabled or disabled - */ -UI.setMicrophoneButtonEnabled - = enabled => APP.store.dispatch(setAudioIconEnabled(enabled)); - /** * Indicates if any the "top" overlays are currently visible. The check includes * the call/ring overlay, the suspended overlay, the GUM permissions overlay, diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index 2a1d7c7ea..cf03c107b 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -55,11 +55,6 @@ function getNewAudioInputDevice(newDevices, localAudio) { availableAudioInputDevices[0].label !== '') { return availableAudioInputDevices[0].deviceId; } - // Otherwise we assume that we don't have any audio input devices - // to use and that's why disable microphone button on UI. - else { - APP.UI.setMicrophoneButtonEnabled(false); - } } else { // And here we handle case when we already have some device working, // but we plug-in a "preferred" (previously selected in settings, stored diff --git a/react/features/base/media/actionTypes.js b/react/features/base/media/actionTypes.js index 4c233082a..592644e7e 100644 --- a/react/features/base/media/actionTypes.js +++ b/react/features/base/media/actionTypes.js @@ -8,6 +8,16 @@ */ export const SET_AUDIO_MUTED = Symbol('SET_AUDIO_MUTED'); +/** + * The type of (redux) action to adjust the availability of the local audio. + * + * { + * type: SET_AUDIO_AVAILABLE, + * muted: boolean + * } + */ +export const SET_AUDIO_AVAILABLE = Symbol('SET_AUDIO_AVAILABLE'); + /** * The type of (redux) action to set the facing mode of the local video camera * to a specific value. diff --git a/react/features/base/media/actions.js b/react/features/base/media/actions.js index c15d23f21..d26602941 100644 --- a/react/features/base/media/actions.js +++ b/react/features/base/media/actions.js @@ -4,6 +4,7 @@ import type { Dispatch } from 'redux'; import { SET_AUDIO_MUTED, + SET_AUDIO_AVAILABLE, SET_CAMERA_FACING_MODE, SET_VIDEO_AVAILABLE, SET_VIDEO_MUTED, @@ -11,6 +12,23 @@ import { } from './actionTypes'; import { CAMERA_FACING_MODE } from './constants'; +/** + * Action to adjust the availability of the local audio. + * + * @param {boolean} available - True if the local audio is to be marked as + * available or false if the local audio is not available. + * @returns {{ + * type: SET_AUDIO_AVAILABLE, + * available: boolean + * }} + */ +export function setAudioAvailable(available: boolean) { + return { + type: SET_AUDIO_AVAILABLE, + available + }; +} + /** * Action to set the muted state of the local audio. * diff --git a/react/features/base/media/reducer.js b/react/features/base/media/reducer.js index d8ce8fc5b..9bbd77eab 100644 --- a/react/features/base/media/reducer.js +++ b/react/features/base/media/reducer.js @@ -3,6 +3,7 @@ import { combineReducers } from 'redux'; import { ReducerRegistry } from '../redux'; import { + SET_AUDIO_AVAILABLE, SET_AUDIO_MUTED, SET_CAMERA_FACING_MODE, SET_VIDEO_AVAILABLE, @@ -24,6 +25,7 @@ import { CAMERA_FACING_MODE } from './constants'; * @type {AudioMediaState} */ const AUDIO_INITIAL_MEDIA_STATE = { + available: true, muted: false }; @@ -38,6 +40,12 @@ const AUDIO_INITIAL_MEDIA_STATE = { */ function _audio(state = AUDIO_INITIAL_MEDIA_STATE, action) { switch (action.type) { + case SET_AUDIO_AVAILABLE: + return { + ...state, + available: action.available + }; + case SET_AUDIO_MUTED: return { ...state, diff --git a/react/features/toolbox/actions.native.js b/react/features/toolbox/actions.native.js index 6511c4190..28a06a1fa 100644 --- a/react/features/toolbox/actions.native.js +++ b/react/features/toolbox/actions.native.js @@ -51,27 +51,6 @@ export function clearToolboxTimeout(): Object { }; } -/** - * Enables/disables audio toolbar button. - * - * @param {boolean} enabled - True if the button should be enabled; otherwise, - * false. - * @returns {Function} - */ -export function setAudioIconEnabled(enabled: boolean = false): Function { - return (dispatch: Dispatch<*>) => { - const i18nKey = enabled ? 'mute' : 'micDisabled'; - const i18n = `[content]toolbar.${i18nKey}`; - const button = { - enabled, - i18n, - toggled: !enabled - }; - - dispatch(setToolbarButton('microphone', button)); - }; -} - /** * Signals that value of conference subject should be changed. * diff --git a/react/features/toolbox/middleware.js b/react/features/toolbox/middleware.js index c01cb7a55..533d21082 100644 --- a/react/features/toolbox/middleware.js +++ b/react/features/toolbox/middleware.js @@ -1,6 +1,10 @@ /* @flow */ -import { SET_VIDEO_AVAILABLE, SET_VIDEO_MUTED } from '../base/media'; +import { + SET_AUDIO_AVAILABLE, + SET_AUDIO_MUTED, + SET_VIDEO_AVAILABLE, + SET_VIDEO_MUTED } from '../base/media'; import { MiddlewareRegistry } from '../base/redux'; import { setToolbarButton } from './actions'; @@ -33,6 +37,11 @@ MiddlewareRegistry.register(store => next => action => { break; } + case SET_AUDIO_AVAILABLE: + case SET_AUDIO_MUTED: { + return _setAudioAvailableOrMuted(store, next, action); + } + case SET_VIDEO_AVAILABLE: case SET_VIDEO_MUTED: return _setVideoAvailableOrMuted(store, next, action); @@ -41,6 +50,31 @@ MiddlewareRegistry.register(store => next => action => { return next(action); }); +/** + * Adjusts the state of toolbar's microphone button. + * + * @param {Store} store - The Redux store instance. + * @param {Function} next - The redux function to continue dispatching the + * specified {@code action} in the specified {@code store}. + * @param {Object} action - Either SET_AUDIO_AVAILABLE or SET_AUDIO_MUTED. + * + * @returns {*} + */ +function _setAudioAvailableOrMuted({ dispatch, getState }, next, action) { + const result = next(action); + + const { available, muted } = getState()['features/base/media'].audio; + const i18nKey = available ? 'mute' : 'micDisabled'; + + dispatch(setToolbarButton('microphone', { + enabled: available, + i18n: `[content]toolbar.${i18nKey}`, + toggled: available ? muted : true + })); + + return result; +} + /** * Adjusts the state of toolbar's camera button. *