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:
parent
d8487c25b2
commit
c7765cc1b7
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue