diff --git a/conference.js b/conference.js index 6b8292c67..717f6cab7 100644 --- a/conference.js +++ b/conference.js @@ -148,6 +148,7 @@ import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/Au import { createPresenterEffect } from './react/features/stream-effects/presenter'; import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise'; import { endpointMessageReceived } from './react/features/subtitles'; +import { muteLocal } from './react/features/video-menu/actions.any'; import UIEvents from './service/UI/UIEvents'; const logger = Logger.getLogger(__filename); @@ -1144,7 +1145,8 @@ export default { * Used by Jibri to detect when it's alone and the meeting should be terminated. */ get membersCount() { - return room.getParticipants().filter(p => !p.isHidden()).length + 1; + return room.getParticipants() + .filter(p => !p.isHidden() || !(config.iAmRecorder && p.isHiddenFromRecorder())).length + 1; }, /** @@ -2063,6 +2065,10 @@ export default { APP.store.dispatch(updateRemoteParticipantFeatures(user)); }); room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => { + if (config.iAmRecorder && user.isHiddenFromRecorder()) { + return; + } + // The logic shared between RN and web. commonUserJoinedHandling(APP.store, room, user); @@ -2112,6 +2118,14 @@ export default { return; } + if (config.iAmRecorder) { + const participant = room.getParticipantById(track.getParticipantId()); + + if (participant.isHiddenFromRecorder()) { + return; + } + } + APP.store.dispatch(trackAdded(track)); }); @@ -2599,13 +2613,24 @@ export default { * @returns {void} */ _onConferenceJoined() { + const { dispatch } = APP.store; + APP.UI.initConference(); if (!config.disableShortcuts) { APP.keyboardshortcut.init(); } - APP.store.dispatch(conferenceJoined(room)); + dispatch(conferenceJoined(room)); + + const jwt = APP.store.getState()['features/base/jwt']; + + if (jwt?.user?.hiddenFromRecorder) { + dispatch(muteLocal(true, MEDIA_TYPE.AUDIO)); + dispatch(muteLocal(true, MEDIA_TYPE.VIDEO)); + dispatch(setAudioUnmutePermissions(true, true)); + dispatch(setVideoUnmutePermissions(true, true)); + } }, /** diff --git a/config.js b/config.js index 3981072c6..ec78e9879 100644 --- a/config.js +++ b/config.js @@ -1167,6 +1167,7 @@ var config = { forceJVB121Ratio forceTurnRelay hiddenDomain + hiddenFromRecorderFeatureEnabled ignoreStartMuted websocketKeepAlive websocketKeepAliveUrl diff --git a/package-lock.json b/package-lock.json index e60b418ec..74c3cb9d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1360.0.0+9eb93e0e/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1361.0.0+9e98e989/lib-jitsi-meet.tgz", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "lodash": "4.17.21", "moment": "2.29.1", @@ -12003,8 +12003,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1360.0.0+9eb93e0e/lib-jitsi-meet.tgz", - "integrity": "sha512-qeQOjeZcAVd7aACEwRu8tBD85ZIPS9V+U8Htm9QvB8FpHi+5hYxN3N0SDgLEJlQEKMxsDzpfpjkInX5UCWt/KQ==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1361.0.0+9e98e989/lib-jitsi-meet.tgz", + "integrity": "sha512-dIg6vWsiWIu77TRHsTSGhTvbLqRw9MAlzScMEw/0ooZTq/ztSCvRZQqQ3quP64+4F0FGm6n0P3y0YBp5t44f4g==", "license": "Apache-2.0", "dependencies": { "@jitsi/js-utils": "2.0.0", @@ -29109,8 +29109,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1360.0.0+9eb93e0e/lib-jitsi-meet.tgz", - "integrity": "sha512-qeQOjeZcAVd7aACEwRu8tBD85ZIPS9V+U8Htm9QvB8FpHi+5hYxN3N0SDgLEJlQEKMxsDzpfpjkInX5UCWt/KQ==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1361.0.0+9e98e989/lib-jitsi-meet.tgz", + "integrity": "sha512-dIg6vWsiWIu77TRHsTSGhTvbLqRw9MAlzScMEw/0ooZTq/ztSCvRZQqQ3quP64+4F0FGm6n0P3y0YBp5t44f4g==", "requires": { "@jitsi/js-utils": "2.0.0", "@jitsi/logger": "2.0.0", diff --git a/package.json b/package.json index 83d11c0c5..5b5c2f605 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "jquery-i18next": "1.2.1", "js-md5": "0.6.1", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1360.0.0+9eb93e0e/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1361.0.0+9e98e989/lib-jitsi-meet.tgz", "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d", "lodash": "4.17.21", "moment": "2.29.1", diff --git a/react/features/base/jwt/middleware.js b/react/features/base/jwt/middleware.js index b9d2f0e2e..4f45e28ae 100644 --- a/react/features/base/jwt/middleware.js +++ b/react/features/base/jwt/middleware.js @@ -220,10 +220,11 @@ function _undoOverwriteLocalParticipant( * avatarURL: ?string, * email: ?string, * id: ?string, - * name: ?string + * name: ?string, + * hidden-from-recorder: ?boolean * }} */ -function _user2participant({ avatar, avatarUrl, email, id, name }) { +function _user2participant({ avatar, avatarUrl, email, id, name, 'hidden-from-recorder': hiddenFromRecorder }) { const participant = {}; if (typeof avatarUrl === 'string') { @@ -241,5 +242,9 @@ function _user2participant({ avatar, avatarUrl, email, id, name }) { participant.name = name.trim(); } + if (hiddenFromRecorder === 'true' || hiddenFromRecorder === true) { + participant.hiddenFromRecorder = true; + } + return Object.keys(participant).length ? participant : undefined; } diff --git a/react/features/base/media/actions.js b/react/features/base/media/actions.js index b85e9707d..b5a1fe71a 100644 --- a/react/features/base/media/actions.js +++ b/react/features/base/media/actions.js @@ -65,12 +65,14 @@ 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. + * @param {boolean|undefined} skipNotification - True if we want to skip showing the notification. * @returns {Function} */ -export function setAudioUnmutePermissions(blocked: boolean) { +export function setAudioUnmutePermissions(blocked: boolean, skipNotification: boolean = false) { return { type: SET_AUDIO_UNMUTE_PERMISSIONS, - blocked + blocked, + skipNotification }; } @@ -155,12 +157,14 @@ export function setVideoMuted( * Action to disable/enable the video mute icon. * * @param {boolean} blocked - True if the video mute icon needs to be disabled. + * @param {boolean|undefined} skipNotification - True if we want to skip showing the notification. * @returns {Function} */ -export function setVideoUnmutePermissions(blocked: boolean) { +export function setVideoUnmutePermissions(blocked: boolean, skipNotification: boolean = false) { return { type: SET_VIDEO_UNMUTE_PERMISSIONS, - blocked + blocked, + skipNotification }; } diff --git a/react/features/base/media/middleware.js b/react/features/base/media/middleware.js index e761718a4..c02e0ee39 100644 --- a/react/features/base/media/middleware.js +++ b/react/features/base/media/middleware.js @@ -86,12 +86,12 @@ MiddlewareRegistry.register(store => next => action => { } case SET_AUDIO_UNMUTE_PERMISSIONS: { - const { blocked } = action; + const { blocked, skipNotification } = action; const state = store.getState(); const tracks = state['features/base/tracks']; const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO); - if (blocked && isAudioMuted) { + if (blocked && isAudioMuted && !skipNotification) { store.dispatch(showWarningNotification({ descriptionKey: 'notify.audioUnmuteBlockedDescription', titleKey: 'notify.audioUnmuteBlockedTitle' @@ -111,13 +111,13 @@ MiddlewareRegistry.register(store => next => action => { } case SET_VIDEO_UNMUTE_PERMISSIONS: { - const { blocked } = action; + const { blocked, skipNotification } = action; const state = store.getState(); const tracks = state['features/base/tracks']; const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO); const isMediaShared = isScreenMediaShared(state); - if (blocked && isVideoMuted && !isMediaShared) { + if (blocked && isVideoMuted && !isMediaShared && !skipNotification) { store.dispatch(showWarningNotification({ descriptionKey: 'notify.videoUnmuteBlockedDescription', titleKey: 'notify.videoUnmuteBlockedTitle' diff --git a/react/features/follow-me/middleware.js b/react/features/follow-me/middleware.js index 44b4a4ed1..b2971ad83 100644 --- a/react/features/follow-me/middleware.js +++ b/react/features/follow-me/middleware.js @@ -102,16 +102,32 @@ function _onFollowMeCommand(attributes = {}, id, store) { const participantSendingCommand = getParticipantById(state, id); - // The Command(s) API will send us our own commands and we don't want - // to act upon them. - if (participantSendingCommand.local) { - return; - } + if (participantSendingCommand) { + // The Command(s) API will send us our own commands and we don't want + // to act upon them. + if (participantSendingCommand.local) { + return; + } - if (participantSendingCommand.role !== 'moderator') { - logger.warn('Received follow-me command not from moderator'); + if (participantSendingCommand.role !== 'moderator') { + logger.warn('Received follow-me command not from moderator'); - return; + return; + } + } else { + // This is the case of jibri receiving commands from a hidden participant. + const { iAmRecorder } = state['features/base/config']; + const { conference } = state['features/base/conference']; + + // As this participant is not stored in redux store we do the checks on the JitsiParticipant from lib-jitsi-meet + const participant = conference.getParticipantById(id); + + if (!iAmRecorder || !participant || participant.getRole() !== 'moderator' + || !participant.isHiddenFromRecorder()) { + logger.warn('Something went wrong with follow-me command'); + + return; + } } if (!isFollowMeActive(state)) {