diff --git a/modules/API/API.js b/modules/API/API.js index a82cd49ad6..ecdb197cc2 100755 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -126,7 +126,7 @@ import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functio import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions'; import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/functions'; import { setTileView, toggleTileView } from '../../react/features/video-layout/actions.any'; -import { muteAllParticipants } from '../../react/features/video-menu/actions'; +import { muteAllParticipants, muteRemote } from '../../react/features/video-menu/actions'; import { setVideoQuality } from '../../react/features/video-quality/actions'; import { toggleBackgroundEffect, toggleBlurredBackgroundEffect } from '../../react/features/virtual-background/actions'; import { VIRTUAL_BACKGROUND_TYPE } from '../../react/features/virtual-background/constants'; @@ -239,6 +239,17 @@ function initCommands() { APP.store.dispatch(muteAllParticipants(exclude, muteMediaType)); }, + 'mute-remote-participant': (participantId, mediaType) => { + if (!isLocalParticipantModerator(APP.store.getState())) { + logger.error('Missing moderator rights to mute remote participant'); + + return; + } + + const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO; + + APP.store.dispatch(muteRemote(participantId, muteMediaType)); + }, 'toggle-lobby': isLobbyEnabled => { APP.store.dispatch(toggleLobbyMode(isLobbyEnabled)); }, @@ -1401,6 +1412,25 @@ class API { }); } + /** + * Notify the external application that a participant's mute status changed. + * + * @param {string} participantId - The ID of the participant. + * @param {boolean} isMuted - True if muted, false if unmuted. + * @param {string} mediaType - Media type that was muted ('audio', 'video', or 'desktop'). + * @param {boolean} isSelfMuted - True if participant muted themselves, false if muted by moderator. + * @returns {void} + */ + notifyParticipantMuted(participantId, isMuted, mediaType, isSelfMuted = true) { + this._sendEvent({ + name: 'participant-muted', + id: participantId, + isMuted, + mediaType, + isSelfMuted + }); + } + /** * Notify the external app that a notification has been triggered. * diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index 05c604b8d4..b139558b78 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -47,6 +47,7 @@ const commands = { localSubject: 'local-subject', kickParticipant: 'kick-participant', muteEveryone: 'mute-everyone', + muteRemoteParticipant: 'mute-remote-participant', overwriteConfig: 'overwrite-config', overwriteNames: 'overwrite-names', password: 'password', @@ -150,6 +151,7 @@ const events = { 'participant-joined': 'participantJoined', 'participant-kicked-out': 'participantKickedOut', 'participant-left': 'participantLeft', + 'participant-muted': 'participantMuted', 'participant-role-changed': 'participantRoleChanged', 'participants-pane-toggled': 'participantsPaneToggled', 'password-required': 'passwordRequired', diff --git a/react/features/base/tracks/middleware.web.ts b/react/features/base/tracks/middleware.web.ts index c737a574c5..1df33ecd25 100644 --- a/react/features/base/tracks/middleware.web.ts +++ b/react/features/base/tracks/middleware.web.ts @@ -143,6 +143,13 @@ MiddlewareRegistry.register(store => next => action => { if (typeof action.track?.muted !== 'undefined' && participantID && !local) { logTracksForParticipant(store.getState()['features/base/tracks'], participantID, 'Track updated'); + + // Notify external API when remote participant mutes/unmutes themselves + const mediaType = isVideoTrack + ? (jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP ? 'desktop' : 'video') + : 'audio'; + + APP.API.notifyParticipantMuted(participantID, action.track.muted, mediaType, true); } return result; diff --git a/react/features/video-menu/actions.any.ts b/react/features/video-menu/actions.any.ts index a48ab22f81..a324624b88 100644 --- a/react/features/video-menu/actions.any.ts +++ b/react/features/video-menu/actions.any.ts @@ -73,6 +73,11 @@ export function muteRemote(participantId: string, mediaType: MediaType) { const muteMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType; dispatch(muteRemoteParticipant(participantId, muteMediaType)); + + // Notify external API that participant was muted by moderator + if (typeof APP !== 'undefined') { + APP.API.notifyParticipantMuted(participantId, true, muteMediaType, false); + } }; }