snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
313
react/features/av-moderation/middleware.ts
Normal file
313
react/features/av-moderation/middleware.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
import { getConferenceState } from '../base/conference/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE as TRACK_MEDIA_TYPE } from '../base/media/constants';
|
||||
import {
|
||||
isAudioMuted,
|
||||
isScreenshareMuted,
|
||||
isVideoMuted
|
||||
} from '../base/media/functions';
|
||||
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getRemoteParticipants,
|
||||
hasRaisedHand,
|
||||
isLocalParticipantModerator,
|
||||
isParticipantModerator
|
||||
} from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { hideNotification, showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { muteLocal } from '../video-menu/actions.any';
|
||||
|
||||
import {
|
||||
DISABLE_MODERATION,
|
||||
ENABLE_MODERATION,
|
||||
LOCAL_PARTICIPANT_APPROVED,
|
||||
LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
|
||||
LOCAL_PARTICIPANT_REJECTED,
|
||||
PARTICIPANT_APPROVED,
|
||||
PARTICIPANT_REJECTED,
|
||||
REQUEST_DISABLE_AUDIO_MODERATION,
|
||||
REQUEST_DISABLE_DESKTOP_MODERATION,
|
||||
REQUEST_DISABLE_VIDEO_MODERATION,
|
||||
REQUEST_ENABLE_AUDIO_MODERATION,
|
||||
REQUEST_ENABLE_DESKTOP_MODERATION,
|
||||
REQUEST_ENABLE_VIDEO_MODERATION
|
||||
} from './actionTypes';
|
||||
import {
|
||||
disableModeration,
|
||||
dismissPendingAudioParticipant,
|
||||
dismissPendingParticipant,
|
||||
enableModeration,
|
||||
localParticipantApproved,
|
||||
localParticipantRejected,
|
||||
participantApproved,
|
||||
participantPendingAudio,
|
||||
participantRejected
|
||||
} from './actions';
|
||||
import {
|
||||
ASKED_TO_UNMUTE_NOTIFICATION_ID,
|
||||
ASKED_TO_UNMUTE_SOUND_ID,
|
||||
AUDIO_MODERATION_NOTIFICATION_ID,
|
||||
DESKTOP_MODERATION_NOTIFICATION_ID,
|
||||
MEDIA_TYPE,
|
||||
MediaType,
|
||||
VIDEO_MODERATION_NOTIFICATION_ID,
|
||||
} from './constants';
|
||||
import {
|
||||
isEnabledFromState,
|
||||
isParticipantApproved,
|
||||
isParticipantPending
|
||||
} from './functions';
|
||||
import { ASKED_TO_UNMUTE_FILE } from './sounds';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { type } = action;
|
||||
const { conference } = getConferenceState(getState());
|
||||
|
||||
switch (type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
dispatch(registerSound(ASKED_TO_UNMUTE_SOUND_ID, ASKED_TO_UNMUTE_FILE));
|
||||
break;
|
||||
}
|
||||
case APP_WILL_UNMOUNT: {
|
||||
dispatch(unregisterSound(ASKED_TO_UNMUTE_SOUND_ID));
|
||||
break;
|
||||
}
|
||||
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
|
||||
let titleKey;
|
||||
let uid = '';
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
const raisedHand = hasRaisedHand(localParticipant);
|
||||
|
||||
switch (action.mediaType) {
|
||||
case MEDIA_TYPE.AUDIO: {
|
||||
titleKey = 'notify.moderationInEffectTitle';
|
||||
uid = AUDIO_MODERATION_NOTIFICATION_ID;
|
||||
break;
|
||||
}
|
||||
case MEDIA_TYPE.VIDEO: {
|
||||
titleKey = 'notify.moderationInEffectVideoTitle';
|
||||
uid = VIDEO_MODERATION_NOTIFICATION_ID;
|
||||
break;
|
||||
}
|
||||
case MEDIA_TYPE.DESKTOP: {
|
||||
titleKey = 'notify.moderationInEffectCSTitle';
|
||||
uid = DESKTOP_MODERATION_NOTIFICATION_ID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
customActionNameKey: [ 'notify.raiseHandAction' ],
|
||||
customActionHandler: [ () => batch(() => {
|
||||
!raisedHand && dispatch(raiseHand(true));
|
||||
dispatch(hideNotification(uid));
|
||||
}) ],
|
||||
titleKey,
|
||||
uid
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
break;
|
||||
}
|
||||
case REQUEST_DISABLE_AUDIO_MODERATION: {
|
||||
conference?.disableAVModeration(MEDIA_TYPE.AUDIO);
|
||||
break;
|
||||
}
|
||||
case REQUEST_DISABLE_DESKTOP_MODERATION: {
|
||||
conference?.disableAVModeration(MEDIA_TYPE.DESKTOP);
|
||||
break;
|
||||
}
|
||||
case REQUEST_DISABLE_VIDEO_MODERATION: {
|
||||
conference?.disableAVModeration(MEDIA_TYPE.VIDEO);
|
||||
break;
|
||||
}
|
||||
case REQUEST_ENABLE_AUDIO_MODERATION: {
|
||||
conference?.enableAVModeration(MEDIA_TYPE.AUDIO);
|
||||
break;
|
||||
}
|
||||
case REQUEST_ENABLE_DESKTOP_MODERATION: {
|
||||
conference?.enableAVModeration(MEDIA_TYPE.DESKTOP);
|
||||
break;
|
||||
}
|
||||
case REQUEST_ENABLE_VIDEO_MODERATION: {
|
||||
conference?.enableAVModeration(MEDIA_TYPE.VIDEO);
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const state = getState();
|
||||
const audioModerationEnabled = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
|
||||
const participant = action.participant;
|
||||
|
||||
if (participant && audioModerationEnabled) {
|
||||
if (isLocalParticipantModerator(state)) {
|
||||
|
||||
// this is handled only by moderators
|
||||
if (hasRaisedHand(participant)) {
|
||||
// if participant raises hand show notification
|
||||
!isParticipantApproved(participant.id, MEDIA_TYPE.AUDIO)(state)
|
||||
&& dispatch(participantPendingAudio(participant));
|
||||
} else {
|
||||
// if participant lowers hand hide notification
|
||||
isParticipantPending(participant, MEDIA_TYPE.AUDIO)(state)
|
||||
&& dispatch(dismissPendingAudioParticipant(participant));
|
||||
}
|
||||
} else if (participant.id === getLocalParticipant(state)?.id
|
||||
&& /* the new role */ isParticipantModerator(participant)) {
|
||||
|
||||
// this is the granted moderator case
|
||||
getRemoteParticipants(state).forEach(p => {
|
||||
hasRaisedHand(p) && !isParticipantApproved(p.id, MEDIA_TYPE.AUDIO)(state)
|
||||
&& dispatch(participantPendingAudio(p));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ENABLE_MODERATION: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyModerationChanged(action.mediaType, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DISABLE_MODERATION: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyModerationChanged(action.mediaType, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LOCAL_PARTICIPANT_APPROVED: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const local = getLocalParticipant(getState());
|
||||
|
||||
APP.API.notifyParticipantApproved(local?.id, action.mediaType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_APPROVED: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyParticipantApproved(action.id, action.mediaType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LOCAL_PARTICIPANT_REJECTED: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const local = getLocalParticipant(getState());
|
||||
|
||||
APP.API.notifyParticipantRejected(local?.id, action.mediaType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_REJECTED: {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyParticipantRejected(action.id, action.mediaType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers a change handler for state['features/base/conference'].conference to
|
||||
* set the event listeners needed for the A/V moderation feature to operate.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => state['features/base/conference'].conference,
|
||||
(conference, { dispatch, getState }, previousConference) => {
|
||||
if (conference && !previousConference) {
|
||||
// local participant is allowed to unmute
|
||||
conference.on(JitsiConferenceEvents.AV_MODERATION_APPROVED, ({ mediaType }: { mediaType: MediaType; }) => {
|
||||
dispatch(localParticipantApproved(mediaType));
|
||||
|
||||
const customActionNameKey = [];
|
||||
const customActionHandler = [];
|
||||
|
||||
if ((mediaType === MEDIA_TYPE.AUDIO || getState()['features/av-moderation'].audioUnmuteApproved)
|
||||
&& isAudioMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmute');
|
||||
customActionHandler.push(() => {
|
||||
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.AUDIO));
|
||||
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
|
||||
});
|
||||
}
|
||||
|
||||
if ((mediaType === MEDIA_TYPE.DESKTOP || getState()['features/av-moderation'].desktopUnmuteApproved)
|
||||
&& isScreenshareMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmuteScreen');
|
||||
customActionHandler.push(() => {
|
||||
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.SCREENSHARE));
|
||||
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
|
||||
|
||||
// Since permission is requested by raising the hand, lower it not to rely on dominant speaker detection
|
||||
// to clear the hand.
|
||||
dispatch(raiseHand(false));
|
||||
});
|
||||
}
|
||||
|
||||
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
|
||||
&& isVideoMuted(getState())) {
|
||||
customActionNameKey.push('notify.unmuteVideo');
|
||||
customActionHandler.push(() => {
|
||||
dispatch(muteLocal(false, TRACK_MEDIA_TYPE.VIDEO));
|
||||
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
|
||||
|
||||
// Since permission is requested by raising the hand, lower it not to rely on dominant speaker detection
|
||||
// to clear the hand.
|
||||
dispatch(raiseHand(false));
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.hostAskedUnmute',
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
uid: ASKED_TO_UNMUTE_NOTIFICATION_ID
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.AV_MODERATION_REJECTED, ({ mediaType }: { mediaType: MediaType; }) => {
|
||||
dispatch(localParticipantRejected(mediaType));
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.AV_MODERATION_CHANGED, ({ enabled, mediaType, actor }: {
|
||||
actor: Object; enabled: boolean; mediaType: MediaType;
|
||||
}) => {
|
||||
enabled ? dispatch(enableModeration(mediaType, actor)) : dispatch(disableModeration(mediaType, actor));
|
||||
});
|
||||
|
||||
// this is received by moderators
|
||||
conference.on(
|
||||
JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_APPROVED,
|
||||
({ participant, mediaType }: { mediaType: MediaType; participant: { _id: string; }; }) => {
|
||||
const { _id: id } = participant;
|
||||
|
||||
batch(() => {
|
||||
// store in the whitelist
|
||||
dispatch(participantApproved(id, mediaType));
|
||||
|
||||
// remove from pending list
|
||||
dispatch(dismissPendingParticipant(id, mediaType));
|
||||
});
|
||||
});
|
||||
|
||||
// this is received by moderators
|
||||
conference.on(
|
||||
JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_REJECTED,
|
||||
({ participant, mediaType }: { mediaType: MediaType; participant: { _id: string; }; }) => {
|
||||
const { _id: id } = participant;
|
||||
|
||||
dispatch(participantRejected(id, mediaType));
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user