feat(conference) Implement audio/video mute disable when sender limit is reached.

* feat(conference) Impl audio/video mute disable when sender limit is reached.
Jicofo sends a presence when the audio/video sender limit is reached in the conference. The client can then proceed to disable the audio and video mute buttons when this occurs.

* squash: use a different action type and show notification.
This commit is contained in:
Jaya Allamsetty 2021-11-30 15:08:25 -05:00 committed by GitHub
parent d8487c25b2
commit c7765cc1b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 220 additions and 27 deletions

View File

@ -76,12 +76,16 @@ import {
import { import {
getStartWithAudioMuted, getStartWithAudioMuted,
getStartWithVideoMuted, getStartWithVideoMuted,
isAudioMuted,
isVideoMuted,
isVideoMutedByUser, isVideoMutedByUser,
MEDIA_TYPE, MEDIA_TYPE,
setAudioAvailable, setAudioAvailable,
setAudioMuted, setAudioMuted,
setAudioUnmutePermissions,
setVideoAvailable, setVideoAvailable,
setVideoMuted setVideoMuted,
setVideoUnmutePermissions
} from './react/features/base/media'; } from './react/features/base/media';
import { import {
dominantSpeakerChanged, dominantSpeakerChanged,
@ -2257,6 +2261,27 @@ export default {
APP.store.dispatch(suspendDetected()); APP.store.dispatch(suspendDetected());
}); });
room.on(
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
disableAudioMuteChange => {
const muted = isAudioMuted(APP.store.getState());
// Disable the mute button only if its muted.
if (!disableAudioMuteChange || (disableAudioMuteChange && muted)) {
APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
}
});
room.on(
JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
disableVideoMuteChange => {
const muted = isVideoMuted(APP.store.getState());
// Disable the mute button only if its muted.
if (!disableVideoMuteChange || (disableVideoMuteChange && muted)) {
APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
}
});
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => { APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
this.muteAudio(muted); this.muteAudio(muted);
}); });

View File

@ -572,6 +572,8 @@
"notify": { "notify": {
"allowAction": "Allow", "allowAction": "Allow",
"allowedUnmute": "You can unmute your microphone, start your camera or share your screen.", "allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
"audioUnmuteBlockedTitle": "Mic unmute blocked!",
"audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
"connectedOneMember": "{{name}} joined the meeting", "connectedOneMember": "{{name}} joined the meeting",
"connectedThreePlusMembers": "{{name}} and many others joined the meeting", "connectedThreePlusMembers": "{{name}} and many others joined the meeting",
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting", "connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
@ -622,7 +624,9 @@
"moderationToggleDescription": "by {{participantDisplayName}}", "moderationToggleDescription": "by {{participantDisplayName}}",
"raiseHandAction": "Raise hand", "raiseHandAction": "Raise hand",
"reactionSounds": "Disable sounds", "reactionSounds": "Disable sounds",
"groupTitle": "Notifications" "groupTitle": "Notifications",
"videoUnmuteBlockedTitle": "Camera unmute blocked!",
"videoUnmuteBlockedDescription": "Camera unmute operation has been temporarily blocked because of system limits."
}, },
"participantsPane": { "participantsPane": {
"close": "Close", "close": "Close",

View File

@ -10,7 +10,15 @@ import { endpointMessageReceived } from '../../subtitles';
import { getReplaceParticipant } from '../config/functions'; import { getReplaceParticipant } from '../config/functions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection'; import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
import { JitsiConferenceEvents } from '../lib-jitsi-meet'; import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MEDIA_TYPE, setAudioMuted, setVideoMuted } from '../media'; import {
MEDIA_TYPE,
isAudioMuted,
isVideoMuted,
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../media';
import { import {
dominantSpeakerChanged, dominantSpeakerChanged,
getNormalizedDisplayName, getNormalizedDisplayName,
@ -146,6 +154,27 @@ function _addConferenceListeners(conference, dispatch, state) {
} }
}); });
conference.on(
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
disableAudioMuteChange => {
const muted = isAudioMuted(state);
// Disable the mute button only if its muted.
if (!disableAudioMuteChange || (disableAudioMuteChange && muted)) {
APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
}
});
conference.on(
JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
disableVideoMuteChange => {
const muted = isVideoMuted(state);
// Disable the mute button only if its muted.
if (!disableVideoMuteChange || (disableVideoMuteChange && muted)) {
APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
}
});
// Dispatches into features/base/tracks follow: // Dispatches into features/base/tracks follow:
conference.on( conference.on(

View File

@ -1,3 +1,14 @@
/**
* The type of (redux) action to adjust the availability of the local audio.
*
* {
* type: SET_AUDIO_AVAILABLE,
* muted: boolean
* }
*/
export const SET_AUDIO_AVAILABLE = 'SET_AUDIO_AVAILABLE';
/** /**
* The type of (redux) action to set the muted state of the local audio. * The type of (redux) action to set the muted state of the local audio.
* *
@ -9,14 +20,14 @@
export const SET_AUDIO_MUTED = 'SET_AUDIO_MUTED'; export const SET_AUDIO_MUTED = 'SET_AUDIO_MUTED';
/** /**
* The type of (redux) action to adjust the availability of the local audio. * The type of (redux) action to enable/disable the audio mute icon.
* *
* { * {
* type: SET_AUDIO_AVAILABLE, * type: SET_AUDIO_UNMUTE_PERMISSIONS,
* muted: boolean * blocked: boolean
* } * }
*/ */
export const SET_AUDIO_AVAILABLE = 'SET_AUDIO_AVAILABLE'; export const SET_AUDIO_UNMUTE_PERMISSIONS = 'SET_AUDIO_UNMUTE_PERMISSIONS';
/** /**
* The type of (redux) action to set the facing mode of the local video camera * The type of (redux) action to set the facing mode of the local video camera
@ -61,6 +72,16 @@ export const SET_VIDEO_MUTED = 'SET_VIDEO_MUTED';
*/ */
export const STORE_VIDEO_TRANSFORM = 'STORE_VIDEO_TRANSFORM'; export const STORE_VIDEO_TRANSFORM = 'STORE_VIDEO_TRANSFORM';
/**
* The type of (redux) action to enable/disable the video mute icon.
*
* {
* type: SET_VIDEO_UNMUTE_PERMISSIONS,
* blocked: boolean
* }
*/
export const SET_VIDEO_UNMUTE_PERMISSIONS = 'SET_VIDEO_UNMUTE_PERMISSIONS';
/** /**
* The type of (redux) action to toggle the local video camera facing mode. In * The type of (redux) action to toggle the local video camera facing mode. In
* contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally * contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally

View File

@ -9,9 +9,11 @@ import { isModerationNotificationDisplayed } from '../../notifications';
import { import {
SET_AUDIO_MUTED, SET_AUDIO_MUTED,
SET_AUDIO_AVAILABLE, SET_AUDIO_AVAILABLE,
SET_AUDIO_UNMUTE_PERMISSIONS,
SET_CAMERA_FACING_MODE, SET_CAMERA_FACING_MODE,
SET_VIDEO_AVAILABLE, SET_VIDEO_AVAILABLE,
SET_VIDEO_MUTED, SET_VIDEO_MUTED,
SET_VIDEO_UNMUTE_PERMISSIONS,
STORE_VIDEO_TRANSFORM, STORE_VIDEO_TRANSFORM,
TOGGLE_CAMERA_FACING_MODE TOGGLE_CAMERA_FACING_MODE
} from './actionTypes'; } from './actionTypes';
@ -59,6 +61,19 @@ export function setAudioMuted(muted: boolean, ensureTrack: boolean = false) {
}; };
} }
/**
* Action to disable/enable the audio mute icon.
*
* @param {boolean} blocked - True if the audio mute icon needs to be disabled.
* @returns {Function}
*/
export function setAudioUnmutePermissions(blocked: boolean) {
return {
type: SET_AUDIO_UNMUTE_PERMISSIONS,
blocked
};
}
/** /**
* Action to set the facing mode of the local camera. * Action to set the facing mode of the local camera.
* *
@ -136,6 +151,19 @@ export function setVideoMuted(
}; };
} }
/**
* Action to disable/enable the video mute icon.
*
* @param {boolean} blocked - True if the video mute icon needs to be disabled.
* @returns {Function}
*/
export function setVideoUnmutePermissions(blocked: boolean) {
return {
type: SET_VIDEO_UNMUTE_PERMISSIONS,
blocked
};
}
/** /**
* Creates an action to store the last video {@link Transform} applied to a * Creates an action to store the last video {@link Transform} applied to a
* stream. * stream.

View File

@ -88,6 +88,16 @@ export function getStartWithVideoMuted(stateful: Object | Function) {
return Boolean(getPropertyValue(stateful, 'startWithVideoMuted', START_WITH_AUDIO_VIDEO_MUTED_SOURCES)); return Boolean(getPropertyValue(stateful, 'startWithVideoMuted', START_WITH_AUDIO_VIDEO_MUTED_SOURCES));
} }
/**
* Determines whether video is currently muted.
*
* @param {Function|Object} stateful - The redux store, state, or {@code getState} function.
* @returns {boolean}
*/
export function isVideoMuted(stateful: Function | Object) {
return Boolean(toState(stateful)['features/base/media'].video.muted);
}
/** /**
* Determines whether video is currently muted by the user authority. * Determines whether video is currently muted by the user authority.
* *

View File

@ -8,6 +8,10 @@ import {
sendAnalytics sendAnalytics
} from '../../analytics'; } from '../../analytics';
import { APP_STATE_CHANGED } from '../../mobile/background'; import { APP_STATE_CHANGED } from '../../mobile/background';
import {
NOTIFICATION_TIMEOUT_TYPE,
showWarningNotification
} from '../../notifications';
import { isForceMuted } from '../../participants-pane/functions'; import { isForceMuted } from '../../participants-pane/functions';
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only'; import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
import { isRoomValid, SET_ROOM } from '../conference'; import { isRoomValid, SET_ROOM } from '../conference';
@ -21,7 +25,12 @@ import {
TRACK_ADDED TRACK_ADDED
} from '../tracks'; } from '../tracks';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from './actionTypes'; import {
SET_AUDIO_MUTED,
SET_AUDIO_UNMUTE_PERMISSIONS,
SET_VIDEO_MUTED,
SET_VIDEO_UNMUTE_PERMISSIONS
} from './actionTypes';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions'; import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
import { import {
CAMERA_FACING_MODE, CAMERA_FACING_MODE,
@ -74,6 +83,18 @@ MiddlewareRegistry.register(store => next => action => {
break; break;
} }
case SET_AUDIO_UNMUTE_PERMISSIONS: {
const { blocked } = action;
if (blocked) {
store.dispatch(showWarningNotification({
descriptionKey: 'notify.audioUnmuteBlockedDescription',
titleKey: 'notify.audioUnmuteBlockedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
break;
}
case SET_VIDEO_MUTED: { case SET_VIDEO_MUTED: {
const state = store.getState(); const state = store.getState();
const participant = getLocalParticipant(state); const participant = getLocalParticipant(state);
@ -83,6 +104,18 @@ MiddlewareRegistry.register(store => next => action => {
} }
break; break;
} }
case SET_VIDEO_UNMUTE_PERMISSIONS: {
const { blocked } = action;
if (blocked) {
store.dispatch(showWarningNotification({
descriptionKey: 'notify.videoUnmuteBlockedDescription',
titleKey: 'notify.videoUnmuteBlockedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
break;
}
} }
return next(action); return next(action);

View File

@ -7,9 +7,11 @@ import { TRACK_REMOVED } from '../tracks/actionTypes';
import { import {
SET_AUDIO_AVAILABLE, SET_AUDIO_AVAILABLE,
SET_AUDIO_MUTED, SET_AUDIO_MUTED,
SET_AUDIO_UNMUTE_PERMISSIONS,
SET_CAMERA_FACING_MODE, SET_CAMERA_FACING_MODE,
SET_VIDEO_AVAILABLE, SET_VIDEO_AVAILABLE,
SET_VIDEO_MUTED, SET_VIDEO_MUTED,
SET_VIDEO_UNMUTE_PERMISSIONS,
STORE_VIDEO_TRANSFORM, STORE_VIDEO_TRANSFORM,
TOGGLE_CAMERA_FACING_MODE TOGGLE_CAMERA_FACING_MODE
} from './actionTypes'; } from './actionTypes';
@ -33,6 +35,7 @@ import { CAMERA_FACING_MODE } from './constants';
*/ */
export const _AUDIO_INITIAL_MEDIA_STATE = { export const _AUDIO_INITIAL_MEDIA_STATE = {
available: true, available: true,
blocked: false,
muted: false muted: false
}; };
@ -59,6 +62,12 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
muted: action.muted muted: action.muted
}; };
case SET_AUDIO_UNMUTE_PERMISSIONS:
return {
...state,
blocked: action.blocked
};
default: default:
return state; return state;
} }
@ -83,6 +92,7 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
*/ */
export const _VIDEO_INITIAL_MEDIA_STATE = { export const _VIDEO_INITIAL_MEDIA_STATE = {
available: true, available: true,
blocked: false,
facingMode: CAMERA_FACING_MODE.USER, facingMode: CAMERA_FACING_MODE.USER,
muted: 0, muted: 0,
@ -126,6 +136,12 @@ function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
muted: action.muted muted: action.muted
}; };
case SET_VIDEO_UNMUTE_PERMISSIONS:
return {
...state,
blocked: action.blocked
};
case STORE_VIDEO_TRANSFORM: case STORE_VIDEO_TRANSFORM:
return _storeVideoTransform(state, action); return _storeVideoTransform(state, action);

View File

@ -13,6 +13,7 @@ import {
showNotification showNotification
} from '../notifications'; } from '../notifications';
import { isForceMuted } from '../participants-pane/functions'; import { isForceMuted } from '../participants-pane/functions';
import { isAudioMuteButtonDisabled } from '../toolbox/functions.any';
import { setCurrentNotificationUid } from './actions'; import { setCurrentNotificationUid } from './actions';
import { TALK_WHILE_MUTED_SOUND_ID } from './constants'; import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
@ -46,24 +47,27 @@ MiddlewareRegistry.register(store => next => action => {
JitsiConferenceEvents.TALK_WHILE_MUTED, async () => { JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
const state = getState(); const state = getState();
const local = getLocalParticipant(state); const local = getLocalParticipant(state);
const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
const notification = await dispatch(showNotification({
titleKey: 'toolbar.talkWhileMutedPopup',
customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
const { soundsTalkWhileMuted } = getState()['features/base/settings']; // Display the talk while muted notification only when the audio button is not disabled.
if (!isAudioMuteButtonDisabled(state)) {
const forceMuted = isForceMuted(local, MEDIA_TYPE.AUDIO, state);
const notification = await dispatch(showNotification({
titleKey: 'toolbar.talkWhileMutedPopup',
customActionNameKey: forceMuted ? 'notify.raiseHandAction' : 'notify.unmute',
customActionHandler: () => dispatch(forceMuted ? raiseHand(true) : setAudioMuted(false))
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
if (soundsTalkWhileMuted) { const { soundsTalkWhileMuted } = getState()['features/base/settings'];
dispatch(playSound(TALK_WHILE_MUTED_SOUND_ID));
}
if (soundsTalkWhileMuted) {
dispatch(playSound(TALK_WHILE_MUTED_SOUND_ID));
}
if (notification) { if (notification) {
// we store the last start muted notification id that we showed, // we store the last start muted notification id that we showed,
// so we can hide it when unmuted mic is detected // so we can hide it when unmuted mic is detected
dispatch(setCurrentNotificationUid(notification.uid)); dispatch(setCurrentNotificationUid(notification.uid));
}
} }
}); });
break; break;

View File

@ -14,6 +14,7 @@ import { AbstractAudioMuteButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components'; import type { AbstractButtonProps } from '../../base/toolbox/components';
import { isLocalTrackMuted } from '../../base/tracks'; import { isLocalTrackMuted } from '../../base/tracks';
import { muteLocal } from '../../video-menu/actions'; import { muteLocal } from '../../video-menu/actions';
import { isAudioMuteButtonDisabled } from '../functions';
declare var APP: Object; declare var APP: Object;
@ -151,7 +152,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
*/ */
function _mapStateToProps(state): Object { function _mapStateToProps(state): Object {
const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO); const _audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const _disabled = state['features/base/config'].startSilent; const _disabled = state['features/base/config'].startSilent || isAudioMuteButtonDisabled(state);
const enabledFlag = getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true); const enabledFlag = getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true);
return { return {

View File

@ -0,0 +1,13 @@
// @flow
/**
* Indicates if the audio mute button is disabled or not.
*
* @param {Object} state - The state from the Redux store.
* @returns {boolean}
*/
export function isAudioMuteButtonDisabled(state: Object) {
const { audio } = state['features/base/media'];
return !(audio?.available && !audio?.blocked);
}

View File

@ -6,6 +6,8 @@ import { getParticipantCountWithFake } from '../base/participants';
import { toState } from '../base/redux'; import { toState } from '../base/redux';
import { isLocalVideoTrackDesktop } from '../base/tracks'; import { isLocalVideoTrackDesktop } from '../base/tracks';
export * from './functions.any';
const WIDTH = { const WIDTH = {
FIT_9_ICONS: 560, FIT_9_ICONS: 560,
FIT_8_ICONS: 500, FIT_8_ICONS: 500,
@ -78,5 +80,7 @@ export function isToolboxVisible(stateful: Object | Function) {
* @returns {boolean} * @returns {boolean}
*/ */
export function isVideoMuteButtonDisabled(state: Object) { export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput') || isLocalVideoTrackDesktop(state); const { video } = state['features/base/media'];
return !hasAvailableDevices(state, 'videoInput') || video?.blocked || isLocalVideoTrackDesktop(state);
} }

View File

@ -5,6 +5,8 @@ import { hasAvailableDevices } from '../base/devices';
import { TOOLBAR_TIMEOUT } from './constants'; import { TOOLBAR_TIMEOUT } from './constants';
export * from './functions.any';
/** /**
* Helper for getting the height of the toolbox. * Helper for getting the height of the toolbox.
* *
@ -58,8 +60,9 @@ export function isToolboxVisible(state: Object) {
* @returns {boolean} * @returns {boolean}
*/ */
export function isAudioSettingsButtonDisabled(state: Object) { export function isAudioSettingsButtonDisabled(state: Object) {
return (!hasAvailableDevices(state, 'audioInput')
&& !hasAvailableDevices(state, 'audioOutput')) return !(hasAvailableDevices(state, 'audioInput')
&& hasAvailableDevices(state, 'audioOutput'))
|| state['features/base/config'].startSilent; || state['features/base/config'].startSilent;
} }
@ -80,7 +83,9 @@ export function isVideoSettingsButtonDisabled(state: Object) {
* @returns {boolean} * @returns {boolean}
*/ */
export function isVideoMuteButtonDisabled(state: Object) { export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput'); const { video } = state['features/base/media'];
return !hasAvailableDevices(state, 'videoInput') || video?.blocked;
} }
/** /**