feat(external_api) Exposed AV Moderation to the iFrame API

Renamed event property
This commit is contained in:
robertpin 2021-09-29 16:41:23 +03:00 committed by Horatiu Muresan
parent e4448e0a68
commit 09835a672b
4 changed files with 230 additions and 5 deletions

View File

@ -6,6 +6,17 @@ import {
createApiEvent, createApiEvent,
sendAnalytics sendAnalytics
} from '../../react/features/analytics'; } from '../../react/features/analytics';
import {
approveParticipantAudio,
approveParticipantVideo,
rejectParticipantAudio,
rejectParticipantVideo,
requestDisableAudioModeration,
requestDisableVideoModeration,
requestEnableAudioModeration,
requestEnableVideoModeration
} from '../../react/features/av-moderation/actions';
import { isEnabledFromState } from '../../react/features/av-moderation/functions';
import { import {
getCurrentConference, getCurrentConference,
sendTones, sendTones,
@ -25,7 +36,8 @@ import {
pinParticipant, pinParticipant,
kickParticipant, kickParticipant,
raiseHand, raiseHand,
isParticipantModerator isParticipantModerator,
isLocalParticipantModerator
} from '../../react/features/base/participants'; } from '../../react/features/base/participants';
import { updateSettings } from '../../react/features/base/settings'; import { updateSettings } from '../../react/features/base/settings';
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks'; import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
@ -50,6 +62,7 @@ import {
resizeLargeVideo resizeLargeVideo
} from '../../react/features/large-video/actions.web'; } from '../../react/features/large-video/actions.web';
import { toggleLobbyMode } from '../../react/features/lobby/actions'; import { toggleLobbyMode } from '../../react/features/lobby/actions';
import { isForceMuted } from '../../react/features/participants-pane/functions';
import { RECORDING_TYPES } from '../../react/features/recording/constants'; import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions'; import { getActiveSession } from '../../react/features/recording/functions';
import { isScreenAudioSupported } from '../../react/features/screen-share'; import { isScreenAudioSupported } from '../../react/features/screen-share';
@ -100,6 +113,20 @@ let videoAvailable = true;
*/ */
function initCommands() { function initCommands() {
commands = { commands = {
'approve-video': participantId => {
if (!isLocalParticipantModerator(APP.store.getState())) {
return;
}
APP.store.dispatch(approveParticipantVideo(participantId));
},
'ask-to-unmute': participantId => {
if (!isLocalParticipantModerator(APP.store.getState())) {
return;
}
APP.store.dispatch(approveParticipantAudio(participantId));
},
'display-name': displayName => { 'display-name': displayName => {
sendAnalytics(createApiEvent('display.name.changed')); sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName); APP.conference.changeLocalDisplayName(displayName);
@ -150,6 +177,15 @@ function initCommands() {
'proxy-connection-event': event => { 'proxy-connection-event': event => {
APP.conference.onProxyConnectionEvent(event); APP.conference.onProxyConnectionEvent(event);
}, },
'reject-participant': (participantId, mediaType) => {
if (!isLocalParticipantModerator(APP.store.getState())) {
return;
}
const reject = mediaType === MEDIA_TYPE.VIDEO ? rejectParticipantVideo : rejectParticipantAudio;
APP.store.dispatch(reject(participantId));
},
'resize-large-video': (width, height) => { 'resize-large-video': (width, height) => {
logger.debug('Resize large video command received'); logger.debug('Resize large video command received');
sendAnalytics(createApiEvent('largevideo.resized')); sendAnalytics(createApiEvent('largevideo.resized'));
@ -218,6 +254,24 @@ function initCommands() {
sendAnalytics(createApiEvent('chat.toggled')); sendAnalytics(createApiEvent('chat.toggled'));
APP.store.dispatch(toggleChat()); APP.store.dispatch(toggleChat());
}, },
'toggle-moderation': (enabled, mediaType) => {
const state = APP.store.getState();
if (!isLocalParticipantModerator(state)) {
return;
}
const enable = mediaType === MEDIA_TYPE.VIDEO
? requestEnableVideoModeration : requestEnableAudioModeration;
const disable = mediaType === MEDIA_TYPE.VIDEO
? requestDisableVideoModeration : requestDisableAudioModeration;
if (enabled) {
APP.store.dispatch(enable());
} else {
APP.store.dispatch(disable());
}
},
'toggle-raise-hand': () => { 'toggle-raise-hand': () => {
const localParticipant = getLocalParticipant(APP.store.getState()); const localParticipant = getLocalParticipant(APP.store.getState());
@ -541,6 +595,22 @@ function initCommands() {
case 'is-audio-muted': case 'is-audio-muted':
callback(APP.conference.isLocalAudioMuted()); callback(APP.conference.isLocalAudioMuted());
break; break;
case 'is-moderation-on': {
const { mediaType } = request;
const type = mediaType || MEDIA_TYPE.AUDIO;
callback(isEnabledFromState(type, APP.store.getState()));
break;
}
case 'is-participant-force-muted': {
const state = APP.store.getState();
const { participantId, mediaType } = request;
const type = mediaType || MEDIA_TYPE.AUDIO;
const participant = getParticipantById(state, participantId);
callback(isForceMuted(participant, type, state));
break;
}
case 'is-video-muted': case 'is-video-muted':
callback(APP.conference.isLocalVideoMuted()); callback(APP.conference.isLocalVideoMuted());
break; break;
@ -806,6 +876,51 @@ class API {
}); });
} }
/**
* Notify the external application that the moderation status has changed.
*
* @param {string} mediaType - Media type for which the moderation changed.
* @param {boolean} enabled - Whether or not the new moderation status is enabled.
* @returns {void}
*/
notifyModerationChanged(mediaType: string, enabled: boolean) {
this._sendEvent({
name: 'moderation-status-changed',
mediaType,
enabled
});
}
/**
* Notify the external application that a participant was approved on moderation.
*
* @param {string} participantId - The ID of the participant that got approved.
* @param {string} mediaType - Media type for which the participant was approved.
* @returns {void}
*/
notifyParticipantApproved(participantId: string, mediaType: string) {
this._sendEvent({
name: 'moderation-participant-approved',
id: participantId,
mediaType
});
}
/**
* Notify the external application that a participant was rejected on moderation.
*
* @param {string} participantId - The ID of the participant that got rejected.
* @param {string} mediaType - Media type for which the participant was rejected.
* @returns {void}
*/
notifyParticipantRejected(participantId: string, mediaType: string) {
this._sendEvent({
name: 'moderation-participant-rejected',
id: participantId,
mediaType
});
}
/** /**
* Notify external application that the video quality setting has changed. * Notify external application that the video quality setting has changed.
* *

View File

@ -27,6 +27,8 @@ const ALWAYS_ON_TOP_FILENAMES = [
* commands expected by jitsi-meet * commands expected by jitsi-meet
*/ */
const commands = { const commands = {
approveVideo: 'approve-video',
askToUnmute: 'ask-to-unmute',
avatarUrl: 'avatar-url', avatarUrl: 'avatar-url',
cancelPrivateChat: 'cancel-private-chat', cancelPrivateChat: 'cancel-private-chat',
displayName: 'display-name', displayName: 'display-name',
@ -40,6 +42,7 @@ const commands = {
overwriteConfig: 'overwrite-config', overwriteConfig: 'overwrite-config',
password: 'password', password: 'password',
pinParticipant: 'pin-participant', pinParticipant: 'pin-participant',
rejectParticipant: 'reject-participant',
resizeLargeVideo: 'resize-large-video', resizeLargeVideo: 'resize-large-video',
sendChatMessage: 'send-chat-message', sendChatMessage: 'send-chat-message',
sendEndpointTextMessage: 'send-endpoint-text-message', sendEndpointTextMessage: 'send-endpoint-text-message',
@ -60,6 +63,7 @@ const commands = {
toggleCameraMirror: 'toggle-camera-mirror', toggleCameraMirror: 'toggle-camera-mirror',
toggleChat: 'toggle-chat', toggleChat: 'toggle-chat',
toggleFilmStrip: 'toggle-film-strip', toggleFilmStrip: 'toggle-film-strip',
toggleModeration: 'toggle-moderation',
toggleRaiseHand: 'toggle-raise-hand', toggleRaiseHand: 'toggle-raise-hand',
toggleShareAudio: 'toggle-share-audio', toggleShareAudio: 'toggle-share-audio',
toggleShareScreen: 'toggle-share-screen', toggleShareScreen: 'toggle-share-screen',
@ -92,6 +96,9 @@ const events = {
'incoming-message': 'incomingMessage', 'incoming-message': 'incomingMessage',
'log': 'log', 'log': 'log',
'mic-error': 'micError', 'mic-error': 'micError',
'moderation-participant-approved': 'moderationParticipantApproved',
'moderation-participant-rejected': 'moderationParticipantRejected',
'moderation-status-changed': 'moderationStatusChanged',
'mouse-enter': 'mouseEnter', 'mouse-enter': 'mouseEnter',
'mouse-leave': 'mouseLeave', 'mouse-leave': 'mouseLeave',
'mouse-move': 'mouseMove', 'mouse-move': 'mouseMove',
@ -896,6 +903,36 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}); });
} }
/**
* Returns the moderation on status on the given mediaType.
*
* @param {string} mediaType - The media type for which to check moderation.
* @returns {Promise} - Resolves with the moderation on status and rejects on
* failure.
*/
isModerationOn(mediaType) {
return this._transport.sendRequest({
name: 'is-moderation-on',
mediaType
});
}
/**
* Returns force muted status of the given participant id for the given media type.
*
* @param {string} participantId - The id of the participant to check.
* @param {string} mediaType - The media type for which to check.
* @returns {Promise} - Resolves with the force muted status and rejects on
* failure.
*/
isParticipantForceMuted(participantId, mediaType) {
return this._transport.sendRequest({
name: 'is-participant-force-muted',
participantId,
mediaType
});
}
/** /**
* Returns screen sharing status. * Returns screen sharing status.
* *

View File

@ -23,28 +23,53 @@ import {
import { isEnabledFromState } from './functions'; import { isEnabledFromState } from './functions';
/** /**
* Action used by moderator to approve audio and video for a participant. * Action used by moderator to approve audio for a participant.
* *
* @param {staring} id - The id of the participant to be approved. * @param {staring} id - The id of the participant to be approved.
* @returns {void} * @returns {void}
*/ */
export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => { export const approveParticipantAudio = (id: string) => (dispatch: Function, getState: Function) => {
const state = getState(); const state = getState();
const { conference } = getConferenceState(state); const { conference } = getConferenceState(state);
const participant = getParticipantById(state, id);
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
const isAudioModerationOn = isEnabledFromState(MEDIA_TYPE.AUDIO, state); const isAudioModerationOn = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
const isVideoModerationOn = isEnabledFromState(MEDIA_TYPE.VIDEO, state); const isVideoModerationOn = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
if (isAudioModerationOn || !isVideoModerationOn) { if (isAudioModerationOn || !isVideoModerationOn) {
conference.avModerationApprove(MEDIA_TYPE.AUDIO, id); conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
} }
};
/**
* Action used by moderator to approve video for a participant.
*
* @param {staring} id - The id of the participant to be approved.
* @returns {void}
*/
export const approveParticipantVideo = (id: string) => (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = getConferenceState(state);
const participant = getParticipantById(state, id);
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
const isVideoModerationOn = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
if (isVideoModerationOn && isVideoForceMuted) { if (isVideoModerationOn && isVideoForceMuted) {
conference.avModerationApprove(MEDIA_TYPE.VIDEO, id); conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
} }
}; };
/**
* Action used by moderator to approve audio and video for a participant.
*
* @param {staring} id - The id of the participant to be approved.
* @returns {void}
*/
export const approveParticipant = (id: string) => (dispatch: Function) => {
dispatch(approveParticipantAudio(id));
dispatch(approveParticipantVideo(id));
};
/** /**
* Action used by moderator to reject audio for a participant. * Action used by moderator to reject audio for a participant.
* *

View File

@ -22,7 +22,13 @@ import {
import { muteLocal } from '../video-menu/actions.any'; import { muteLocal } from '../video-menu/actions.any';
import { import {
DISABLE_MODERATION,
ENABLE_MODERATION,
LOCAL_PARTICIPANT_APPROVED,
LOCAL_PARTICIPANT_MODERATION_NOTIFICATION, LOCAL_PARTICIPANT_MODERATION_NOTIFICATION,
LOCAL_PARTICIPANT_REJECTED,
PARTICIPANT_APPROVED,
PARTICIPANT_REJECTED,
REQUEST_DISABLE_AUDIO_MODERATION, REQUEST_DISABLE_AUDIO_MODERATION,
REQUEST_DISABLE_VIDEO_MODERATION, REQUEST_DISABLE_VIDEO_MODERATION,
REQUEST_ENABLE_AUDIO_MODERATION, REQUEST_ENABLE_AUDIO_MODERATION,
@ -51,6 +57,8 @@ import {
} from './functions'; } from './functions';
import { ASKED_TO_UNMUTE_FILE } from './sounds'; import { ASKED_TO_UNMUTE_FILE } from './sounds';
declare var APP: Object;
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { type } = action; const { type } = action;
const { conference } = getConferenceState(getState()); const { conference } = getConferenceState(getState());
@ -148,6 +156,46 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break; 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); return next(action);