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 {
getStartWithAudioMuted,
getStartWithVideoMuted,
isAudioMuted,
isVideoMuted,
isVideoMutedByUser,
MEDIA_TYPE,
setAudioAvailable,
setAudioMuted,
setAudioUnmutePermissions,
setVideoAvailable,
setVideoMuted
setVideoMuted,
setVideoUnmutePermissions
} from './react/features/base/media';
import {
dominantSpeakerChanged,
@ -2257,6 +2261,27 @@ export default {
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 => {
this.muteAudio(muted);
});

View File

@ -572,6 +572,8 @@
"notify": {
"allowAction": "Allow",
"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",
"connectedThreePlusMembers": "{{name}} and many others joined the meeting",
"connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
@ -622,7 +624,9 @@
"moderationToggleDescription": "by {{participantDisplayName}}",
"raiseHandAction": "Raise hand",
"reactionSounds": "Disable sounds",
"groupTitle": "Notifications"
"groupTitle": "Notifications",
"videoUnmuteBlockedTitle": "Camera unmute blocked!",
"videoUnmuteBlockedDescription": "Camera unmute operation has been temporarily blocked because of system limits."
},
"participantsPane": {
"close": "Close",

View File

@ -10,7 +10,15 @@ import { endpointMessageReceived } from '../../subtitles';
import { getReplaceParticipant } from '../config/functions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
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 {
dominantSpeakerChanged,
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:
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.
*
@ -9,14 +20,14 @@
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,
* muted: boolean
* type: SET_AUDIO_UNMUTE_PERMISSIONS,
* 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
@ -61,6 +72,16 @@ export const SET_VIDEO_MUTED = 'SET_VIDEO_MUTED';
*/
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
* contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally

View File

@ -9,9 +9,11 @@ import { isModerationNotificationDisplayed } from '../../notifications';
import {
SET_AUDIO_MUTED,
SET_AUDIO_AVAILABLE,
SET_AUDIO_UNMUTE_PERMISSIONS,
SET_CAMERA_FACING_MODE,
SET_VIDEO_AVAILABLE,
SET_VIDEO_MUTED,
SET_VIDEO_UNMUTE_PERMISSIONS,
STORE_VIDEO_TRANSFORM,
TOGGLE_CAMERA_FACING_MODE
} 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.
*
@ -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
* stream.

View File

@ -88,6 +88,16 @@ export function getStartWithVideoMuted(stateful: Object | Function) {
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.
*

View File

@ -8,6 +8,10 @@ import {
sendAnalytics
} from '../../analytics';
import { APP_STATE_CHANGED } from '../../mobile/background';
import {
NOTIFICATION_TIMEOUT_TYPE,
showWarningNotification
} from '../../notifications';
import { isForceMuted } from '../../participants-pane/functions';
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
import { isRoomValid, SET_ROOM } from '../conference';
@ -21,7 +25,12 @@ import {
TRACK_ADDED
} 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 {
CAMERA_FACING_MODE,
@ -74,6 +83,18 @@ MiddlewareRegistry.register(store => next => action => {
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: {
const state = store.getState();
const participant = getLocalParticipant(state);
@ -83,6 +104,18 @@ MiddlewareRegistry.register(store => next => action => {
}
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);

View File

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

View File

@ -13,6 +13,7 @@ import {
showNotification
} from '../notifications';
import { isForceMuted } from '../participants-pane/functions';
import { isAudioMuteButtonDisabled } from '../toolbox/functions.any';
import { setCurrentNotificationUid } from './actions';
import { TALK_WHILE_MUTED_SOUND_ID } from './constants';
@ -46,24 +47,27 @@ MiddlewareRegistry.register(store => next => action => {
JitsiConferenceEvents.TALK_WHILE_MUTED, async () => {
const state = getState();
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) {
dispatch(playSound(TALK_WHILE_MUTED_SOUND_ID));
}
const { soundsTalkWhileMuted } = getState()['features/base/settings'];
if (soundsTalkWhileMuted) {
dispatch(playSound(TALK_WHILE_MUTED_SOUND_ID));
}
if (notification) {
// we store the last start muted notification id that we showed,
// so we can hide it when unmuted mic is detected
dispatch(setCurrentNotificationUid(notification.uid));
if (notification) {
// we store the last start muted notification id that we showed,
// so we can hide it when unmuted mic is detected
dispatch(setCurrentNotificationUid(notification.uid));
}
}
});
break;

View File

@ -14,6 +14,7 @@ import { AbstractAudioMuteButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { isLocalTrackMuted } from '../../base/tracks';
import { muteLocal } from '../../video-menu/actions';
import { isAudioMuteButtonDisabled } from '../functions';
declare var APP: Object;
@ -151,7 +152,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
*/
function _mapStateToProps(state): Object {
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);
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 { isLocalVideoTrackDesktop } from '../base/tracks';
export * from './functions.any';
const WIDTH = {
FIT_9_ICONS: 560,
FIT_8_ICONS: 500,
@ -78,5 +80,7 @@ export function isToolboxVisible(stateful: Object | Function) {
* @returns {boolean}
*/
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';
export * from './functions.any';
/**
* Helper for getting the height of the toolbox.
*
@ -58,8 +60,9 @@ export function isToolboxVisible(state: Object) {
* @returns {boolean}
*/
export function isAudioSettingsButtonDisabled(state: Object) {
return (!hasAvailableDevices(state, 'audioInput')
&& !hasAvailableDevices(state, 'audioOutput'))
return !(hasAvailableDevices(state, 'audioInput')
&& hasAvailableDevices(state, 'audioOutput'))
|| state['features/base/config'].startSilent;
}
@ -80,7 +83,9 @@ export function isVideoSettingsButtonDisabled(state: Object) {
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput');
const { video } = state['features/base/media'];
return !hasAvailableDevices(state, 'videoInput') || video?.blocked;
}
/**