feat: Added mute video moderation feature (#8630)

* Added mute video feature

* Fixed export

* Fixed some issues

* Added remote video mute notification

* Fixed import

* Fixed conference event handling

* Fixed some linting issues

* Fixed more linter errors

* turn screenshare off on remote video mute

* Fix linter issue

* translations added for mute video feature

* Added video mute button to interface config

* Updated lib-jitsi-meet

* Fix copy paste error

Co-authored-by: nurjinn jafar <nurjin.jafar@nordeck.net>
This commit is contained in:
Steffen Kolmer 2021-02-24 22:45:07 +01:00 committed by GitHub
parent 42d926eef3
commit 23bb824731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 754 additions and 46 deletions

View File

@ -2008,7 +2008,10 @@ export default {
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
if (participantThatMutedUs) {
APP.store.dispatch(participantMutedUs(participantThatMutedUs));
APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
if (this.isSharingScreen && track.isVideoTrack()) {
this._turnScreenSharingOff(false);
}
}
});

View File

@ -6,7 +6,6 @@
min-width: 75px;
text-align: left;
padding: 0px;
width: 180px;
white-space: nowrap;
&__item {

View File

@ -206,7 +206,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'security'
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
],
TOOLBAR_TIMEOUT: 4000,

View File

@ -239,12 +239,19 @@
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
"muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneTitle": "Alle stummschalten?",
"muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Teilnehmern deaktivieren möchten? Sie können sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
"muteEveryoneSelf": "sich selbst",
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Person stummschalten?",
"muteParticipantsVideoButton": "Kamera ausschalten",
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"Ok": "OK",
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
@ -484,6 +491,8 @@
"mutedTitle": "Stummschaltung aktiv!",
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
"videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
"raisedHand": "{{name}} möchte sprechen.",
@ -714,12 +723,16 @@
"moreOptions": "Menü „Weitere Optionen“",
"mute": "„Audio stummschalten“ ein-/ausschalten",
"muteEveryone": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "„Melden“ ein-/ausschalten",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten",
"remoteVideoMute": "Kamera von dieser Person ausschalten",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen ein-/ausschalten",
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
@ -764,6 +777,7 @@
"moreOptions": "Weitere Optionen",
"mute": "Stummschaltung aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
"noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
"noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
@ -849,13 +863,16 @@
},
"videothumbnail": {
"domute": "Stummschalten",
"domuteVideo": "Kamera ausschalten",
"domuteOthers": "Alle anderen stummschalten",
"domuteVideoOfOthers": "Alle anderen Kameras auschalten",
"flip": "Spiegeln",
"grantModerator": "Moderationsrechte vergeben",
"kick": "Hinauswerfen",
"moderator": "Moderation",
"mute": "Person ist stumm geschaltet",
"muted": "Stummgeschaltet",
"videoMuted": "Kamera ausgeschaltet",
"remoteControl": "Fernsteuerung",
"show": "Im Vordergrund anzeigen",
"videomute": "Person hat die Kamera angehalten"

View File

@ -241,12 +241,19 @@
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
"muteEveryoneDialog": "Are you sure you want to mute everyone? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneTitle": "Mute everyone?",
"muteEveryoneElsesVideoDialog": "Once the camera is disabled, you won't be able to turn it back on, but they can turn it back on at any time.",
"muteEveryoneElsesVideoTitle": "Disable everyone's camera except {{whom}}?",
"muteEveryonesVideoDialog": "Are you sure you want to disable everyone's camera? You won't be able to turn it back on, but they can turn it back on at any time.",
"muteEveryonesVideoTitle": "Disable everyone's camera?",
"muteEveryoneSelf": "yourself",
"muteEveryoneStartMuted": "Everyone starts muted from now on",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantsVideoButton": "Disable camera",
"muteParticipantsVideoTitle": "Disable camera of this participant?",
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
"Ok": "OK",
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
@ -484,6 +491,8 @@
"mutedTitle": "You're muted!",
"mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
"videoMutedRemotelyTitle": "Your camera has been disabled by {{participantDisplayName}}!",
"videoMutedRemotelyDescription": "You can always turn it on again.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raisedHand": "{{name}} would like to speak.",
@ -716,12 +725,16 @@
"moreOptions": "Show more options",
"mute": "Toggle mute audio",
"muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Toggle raise hand",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant",
"security": "Security options",
"Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing",
@ -766,6 +779,7 @@
"moreOptions": "More options",
"mute": "Mute / Unmute",
"muteEveryone": "Mute everyone",
"muteEveryonesVideo": "Disable everyone's camera",
"noAudioSignalTitle": "There is no input coming from your mic!",
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
@ -850,13 +864,16 @@
"videothumbnail": {
"connectionInfo": "Connection Info",
"domute": "Mute",
"domuteVideo": "Disable camera",
"domuteOthers": "Mute everyone else",
"domuteVideoOfOthers": "Disable camera of everyone else",
"flip": "Flip",
"grantModerator": "Grant Moderator",
"kick": "Kick out",
"moderator": "Moderator",
"mute": "Participant is muted",
"muted": "Muted",
"videoMuted": "Camera disabled",
"remoteControl": "Start / Stop remote control",
"show": "Show on stage",
"videomute": "Participant has stopped the camera"

View File

@ -14,6 +14,7 @@ import {
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../react/features/base/media';
import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
import { openChat } from '../../react/features/chat/actions.web';
@ -79,7 +80,9 @@ function initCommands() {
sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName);
},
'mute-everyone': () => {
'mute-everyone': mediaType => {
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
sendAnalytics(createApiEvent('muted-everyone'));
const participants = APP.store.getState()['features/base/participants'];
const localIds = participants
@ -87,7 +90,7 @@ function initCommands() {
.filter(participant => participant.role === 'moderator')
.map(participant => participant.id);
APP.store.dispatch(muteAllParticipants(localIds));
APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
},
'toggle-lobby': isLobbyEnabled => {
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));

4
package-lock.json generated
View File

@ -10343,8 +10343,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265",
"from": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265",
"version": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
"from": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",

View File

@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6a7b16c33e27481b03e5a37636e72426c0848265",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9beb47fe5f5ae9caf0b206588e14fa676479ce31",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",

View File

@ -504,15 +504,17 @@ export function createRejoinedEvent({ url, lastConferenceDuration, timeSinceLeft
*
* @param {string} participantId - The ID of the participant that was remotely
* muted.
* @param {string} mediaType - The media type of the channel to mute.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRemoteMuteConfirmedEvent(participantId) {
export function createRemoteMuteConfirmedEvent(participantId, mediaType) {
return {
action: 'clicked',
actionSubject: 'remote.mute.dialog.confirm.button',
attributes: {
'participant_id': participantId
'participant_id': participantId,
'media_type': mediaType
},
source: 'remote.mute.dialog',
type: TYPE_UI

View File

@ -149,9 +149,9 @@ function _addConferenceListeners(conference, dispatch) {
conference.on(
JitsiConferenceEvents.TRACK_MUTE_CHANGED,
(_, participantThatMutedUs) => {
(track, participantThatMutedUs) => {
if (participantThatMutedUs) {
dispatch(participantMutedUs(participantThatMutedUs));
dispatch(participantMutedUs(participantThatMutedUs, track));
}
});

View File

@ -68,6 +68,8 @@ export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
export { default as IconModerator } from './star.svg';
export { default as IconMuteEveryone } from './mute-everyone.svg';
export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
export { default as IconMuteVideoEveryone } from './mute-video-everyone.svg';
export { default as IconMuteVideoEveryoneElse } from './mute-video-everyone-else.svg';
export { default as IconNotificationJoin } from './navigate_next.svg';
export { default as IconOpenInNew } from './open_in_new.svg';
export { default as IconOutlook } from './office365.svg';

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path fill="#FFFFFF" d="M3.136,7.1l14.448,14.447l-1.033,1.032l-2.598-2.599c-0.115,0.077-0.307,0.153-0.459,0.153H3.709
c-0.458,0-0.803-0.346-0.803-0.804v-8.179c0-0.459,0.344-0.803,0.803-0.803h0.612L2.104,8.131L3.136,7.1z M17.584,10.769v8.714
l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
<path fill="#FFFFFF" d="M14.688,0.818l8.164,8.165l-0.584,0.583L20.8,8.098c-0.065,0.043-0.174,0.086-0.26,0.086h-5.528
c-0.259,0-0.454-0.195-0.454-0.454V3.108c0-0.26,0.195-0.454,0.454-0.454h0.345l-1.253-1.253L14.688,0.818z M22.852,2.892v4.924
l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path fill="#A4B8D1" d="M3.136,7.1l14.448,14.447l-1.033,1.032l-2.598-2.599c-0.115,0.077-0.307,0.153-0.459,0.153H3.709
c-0.458,0-0.803-0.346-0.803-0.804v-8.179c0-0.459,0.344-0.803,0.803-0.803h0.612L2.104,8.131L3.136,7.1z M17.584,10.769v8.714
l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
<path fill="#A4B8D1" d="M14.688,0.818l8.164,8.165l-0.584,0.583L20.8,8.098c-0.065,0.043-0.174,0.086-0.26,0.086h-5.528
c-0.259,0-0.454-0.195-0.454-0.454V3.108c0-0.26,0.195-0.454,0.454-0.454h0.345l-1.253-1.253L14.688,0.818z M22.852,2.892v4.924
l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -16,7 +16,9 @@ import {
PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
import {
DISCO_REMOTE_CONTROL_FEATURE
} from './constants';
import {
getLocalParticipant,
getNormalizedDisplayName,
@ -192,15 +194,18 @@ export function localParticipantRoleChanged(role) {
* Create an action for muting another participant in the conference.
*
* @param {string} id - Participant's ID.
* @param {MEDIA_TYPE} mediaType - The media to mute.
* @returns {{
* type: MUTE_REMOTE_PARTICIPANT,
* id: string
* id: string,
* mediaType: MEDIA_TYPE
* }}
*/
export function muteRemoteParticipant(id) {
export function muteRemoteParticipant(id, mediaType) {
return {
type: MUTE_REMOTE_PARTICIPANT,
id
id,
mediaType
};
}
@ -450,17 +455,20 @@ export function participantUpdated(participant = {}) {
* Action to signal that a participant has muted us.
*
* @param {JitsiParticipant} participant - Information about participant.
* @param {JitsiLocalTrack} track - Information about the track that has been muted.
* @returns {Promise}
*/
export function participantMutedUs(participant) {
export function participantMutedUs(participant, track) {
return (dispatch, getState) => {
if (!participant) {
return;
}
const isAudio = track.isAudioTrack();
dispatch(showNotification({
descriptionKey: 'notify.mutedRemotelyDescription',
titleKey: 'notify.mutedRemotelyTitle',
descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription',
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
titleArguments: {
participantDisplayName:
getParticipantDisplayName(getState, participant.getId())

View File

@ -112,7 +112,7 @@ MiddlewareRegistry.register(store => next => action => {
case MUTE_REMOTE_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
conference.muteParticipant(action.id);
conference.muteParticipant(action.id, action.mediaType);
break;
}

View File

@ -26,6 +26,7 @@ import {
getURLWithoutParams
} from '../../base/connection';
import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../base/media';
import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
@ -270,7 +271,7 @@ function _registerForNativeEvents(store) {
});
eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
dispatch(muteLocal(muted === 'true'));
dispatch(muteLocal(muted === 'true', MEDIA_TYPE.AUDIO));
});
eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {

View File

@ -6,10 +6,16 @@ import {
AUDIO_MUTE,
createRemoteMuteConfirmedEvent,
createToolbarEvent,
sendAnalytics
sendAnalytics,
VIDEO_MUTE
} from '../analytics';
import { hideDialog } from '../base/dialog';
import { setAudioMuted } from '../base/media';
import {
MEDIA_TYPE,
setAudioMuted,
setVideoMuted,
VIDEO_MUTISM_AUTHORITY
} from '../base/media';
import {
getLocalParticipant,
muteRemoteParticipant
@ -32,17 +38,26 @@ export function hideRemoteVideoMenu() {
* Mutes the local participant.
*
* @param {boolean} enable - Whether to mute or unmute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @returns {Function}
*/
export function muteLocal(enable: boolean) {
export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>) => {
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
dispatch(setAudioMuted(enable, /* ensureTrack */ true));
const isAudio = mediaType === MEDIA_TYPE.AUDIO;
if (!isAudio && mediaType !== MEDIA_TYPE.VIDEO) {
console.error(`Unsupported media type: ${mediaType}`);
return;
}
sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable }));
dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
: setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
// FIXME: The old conference logic as well as the shared video feature
// still rely on this event being emitted.
typeof APP === 'undefined'
|| APP.UI.emitEvent(UIEvents.AUDIO_MUTED, enable, true);
|| APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable, true);
};
}
@ -50,12 +65,18 @@ export function muteLocal(enable: boolean) {
* Mutes the remote participant with the given ID.
*
* @param {string} participantId - ID of the participant to mute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @returns {Function}
*/
export function muteRemote(participantId: string) {
export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>) => {
sendAnalytics(createRemoteMuteConfirmedEvent(participantId));
dispatch(muteRemoteParticipant(participantId));
if (mediaType !== MEDIA_TYPE.AUDIO && mediaType !== MEDIA_TYPE.VIDEO) {
console.error(`Unsupported media type: ${mediaType}`);
return;
}
sendAnalytics(createRemoteMuteConfirmedEvent(participantId, mediaType));
dispatch(muteRemoteParticipant(participantId, mediaType));
};
}
@ -63,9 +84,10 @@ export function muteRemote(participantId: string) {
* Mutes all participants.
*
* @param {Array<string>} exclude - Array of participant IDs to not mute.
* @param {MEDIA_TYPE} mediaType - The media type to mute.
* @returns {Function}
*/
export function muteAllParticipants(exclude: Array<string>) {
export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const localId = getLocalParticipant(state).id;
@ -75,7 +97,7 @@ export function muteAllParticipants(exclude: Array<string>) {
/* eslint-disable no-confusing-arrow */
participantIds
.filter(id => !exclude.includes(id))
.map(id => id === localId ? muteLocal(true) : muteRemote(id))
.map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType))
.map(dispatch);
/* eslint-enable no-confusing-arrow */
};

View File

@ -3,6 +3,7 @@
import React from 'react';
import { Dialog } from '../../base/dialog';
import { MEDIA_TYPE } from '../../base/media';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
@ -69,7 +70,7 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
return true;
}

View File

@ -0,0 +1,48 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconMuteVideoEveryone } from '../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryonesVideoDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to keep unmuted.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which disables the camera of all the other participants.
*/
export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo';
icon = IconMuteVideoEveryone;
label = 'videothumbnail.domuteVideoOfOthers';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
}
}

View File

@ -0,0 +1,103 @@
// @flow
import React from 'react';
import { Dialog } from '../../base/dialog';
import { MEDIA_TYPE } from '../../base/media';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantsVideoDialog, {
type Props as AbstractProps
} from './AbstractMuteRemoteParticipantsVideoDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryonesVideoDialog}.
*/
export type Props = AbstractProps & {
content: string,
exclude: Array<string>,
title: string
};
/**
*
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before disabling all remote participants cameras.
*
* @extends AbstractMuteRemoteParticipantsVideoDialog
*/
export default class AbstractMuteEveryonesVideoDialog<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this.props;
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesVideoDialog}'s props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object, ownProps: Props) {
const { exclude, t } = ownProps;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => id === getLocalParticipant(state).id
? t('dialog.muteEveryoneSelf')
: getParticipantDisplayName(state, id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElsesVideoDialog'),
title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
} : {
content: t('dialog.muteEveryonesVideoDialog'),
title: t('dialog.muteEveryonesVideoTitle')
};
}

View File

@ -2,6 +2,7 @@
import { Component } from 'react';
import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';
/**
@ -57,7 +58,7 @@ export default class AbstractMuteRemoteParticipantDialog<P:Props = Props>
_onSubmit() {
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID));
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
return true;
}

View File

@ -0,0 +1,65 @@
// @flow
import { Component } from 'react';
import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteRemoteParticipantsVideoDialog}.
*/
export type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The ID of the remote participant to be muted.
*/
participantID: string,
/**
* Function to translate i18n labels.
*/
t: Function
};
/**
* Abstract dialog to confirm a remote participant video ute action.
*
* @extends Component
*/
export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props>
extends Component<P> {
/**
* Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSubmit = this._onSubmit.bind(this);
}
_onSubmit: () => boolean;
/**
* Handles the submit button action.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
return true;
}
}

View File

@ -0,0 +1,103 @@
// @flow
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconCameraDisabled } from '../../base/icons';
import { MEDIA_TYPE } from '../../base/media';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { isRemoteTrackMuted } from '../../base/tracks';
import { MuteRemoteParticipantsVideoDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* Boolean to indicate if the video track of the participant is muted or
* not.
*/
_videoTrackMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to
* mute/unmute.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which mutes the remote participant.
*/
export default class AbstractMuteVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute';
icon = IconCameraDisabled;
label = 'videothumbnail.domuteVideo';
toggledLabel = 'videothumbnail.videoMuted';
/**
* Handles clicking / pressing the button, and mutes the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
}
/**
* Renders the item disabled if the participant is muted.
*
* @inheritdoc
*/
_isDisabled() {
return this.props._videoTrackMuted;
}
/**
* Renders the item toggled if the participant is muted.
*
* @inheritdoc
*/
_isToggled() {
return this.props._videoTrackMuted;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {{
* _videoTrackMuted: boolean
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const tracks = state['features/base/tracks'];
return {
_videoTrackMuted: isRemoteTrackMuted(
tracks, MEDIA_TYPE.VIDEO, ownProps.participantID)
};
}

View File

@ -0,0 +1,54 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconMuteVideoEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElsesVideoButton, {
type Props
} from '../AbstractMuteEveryoneElsesVideoButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given
* participantID
*/
class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
return (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
displayClass = { 'mutelink' }
icon = { IconMuteVideoEveryoneElse }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void;
}
export default translate(connect()(MuteEveryoneElsesVideoButton));

View File

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
from '../AbstractMuteEveryonesVideoDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before disabling all remote participants cameras.
*
* @extends AbstractMuteEveryonesVideoDialog
*/
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleString = { this.props.title }
width = 'small'>
<div>
{ this.props.content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog));

View File

@ -0,0 +1,41 @@
/* @flow */
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteRemoteParticipantsVideoDialog
from '../AbstractMuteRemoteParticipantsVideoDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before disabling a remote participants camera.
*
* @extends Component
*/
class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleKey = 'dialog.muteParticipantsVideoTitle'
width = 'small'>
<div>
{ this.props.t('dialog.muteParticipantsVideoBody') }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(MuteRemoteParticipantsVideoDialog));

View File

@ -0,0 +1,67 @@
/* @flow */
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconCameraDisabled } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteVideoButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteVideoButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for disabling
* the camera of a participant in the conference.
*
* NOTE: At the time of writing this is a button that doesn't use the
* {@code AbstractButton} base component, but is inherited from the same
* super class ({@code AbstractMuteVideoButton} that extends {@code AbstractButton})
* for the sake of code sharing between web and mobile. Once web uses the
* {@code AbstractButton} base component, this can be fully removed.
*/
class MuteVideoButton extends AbstractMuteVideoButton {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _videoTrackMuted, participantID, t } = this.props;
const muteConfig = _videoTrackMuted ? {
translationKey: 'videothumbnail.videoMuted',
muteClassName: 'mutelink disabled'
} : {
translationKey: 'videothumbnail.domuteVideo',
muteClassName: 'mutelink'
};
return (
<RemoteVideoMenuButton
buttonText = { t(muteConfig.translationKey) }
displayClass = { muteConfig.muteClassName }
icon = { IconCameraDisabled }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void
}
export default translate(connect(_mapStateToProps)(MuteVideoButton));

View File

@ -3,20 +3,20 @@
import React, { Component } from 'react';
import { Icon, IconMenuThumb } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import {
GrantModeratorButton,
MuteButton,
MuteVideoButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
@ -43,11 +43,6 @@ type Props = {
*/
_disableRemoteMute: Boolean,
/**
* Whether or not the participant is currently muted.
*/
_isAudioMuted: boolean,
/**
* Whether or not the participant is a conference moderator.
*/
@ -151,7 +146,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
const {
_disableKick,
_disableRemoteMute,
_isAudioMuted,
_isModerator,
dispatch,
initialVolumeValue,
@ -166,7 +160,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
if (!_disableRemoteMute) {
buttons.push(
<MuteButton
isAudioMuted = { _isAudioMuted }
key = 'mute'
participantID = { participantID } />
);
@ -175,6 +168,16 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
key = 'mute-others'
participantID = { participantID } />
);
buttons.push(
<MuteVideoButton
key = 'mute-video'
participantID = { participantID } />
);
buttons.push(
<MuteEveryoneElsesVideoButton
key = 'mute-others-video'
participantID = { participantID } />
);
}
buttons.push(
@ -247,7 +250,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const tracks = state['features/base/tracks'];
const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick } = remoteVideoMenu;
@ -286,7 +288,6 @@ function _mapStateToProps(state, ownProps) {
}
return {
_isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute),

View File

@ -5,9 +5,13 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export { default as MuteVideoButton } from './MuteVideoButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
* @returns {void}
*/
_setAudioMuted(audioMuted: boolean) {
this.props.dispatch(muteLocal(audioMuted));
this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
}
/**

View File

@ -0,0 +1,76 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { IconMuteVideoEveryone } from '../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
type Props = AbstractButtonProps & {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/*
** Whether the local participant is a moderator or not.
*/
isModerator: Boolean,
/**
* The ID of the local participant.
*/
localParticipantId: string
};
/**
* Implements a React {@link Component} which displays a button for disabling the camera of
* every participant (except the local one)
*/
class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
icon = IconMuteVideoEveryone;
label = 'toolbar.muteEveryonesVideo';
tooltip = 'toolbar.muteVideoEveryone';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, localParticipantId } = this.props;
sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
dispatch(openDialog(MuteEveryonesVideoDialog, {
exclude: [ localParticipantId ]
}));
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @param {Props} ownProps - The component's own props.
* @returns {Object}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
const localParticipant = getLocalParticipant(state);
const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
const { visible } = ownProps;
const { disableRemoteMute } = state['features/base/config'];
return {
isModerator,
localParticipantId: localParticipant.id,
visible: visible && isModerator && !disableRemoteMute
};
}
export default translate(connect(_mapStateToProps)(MuteEveryonesVideoButton));

View File

@ -82,6 +82,7 @@ import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
import AudioSettingsButton from './AudioSettingsButton';
import OverflowMenuButton from './OverflowMenuButton';
@ -1079,6 +1080,10 @@ class Toolbox extends Component<Props, State> {
&& <MuteEveryoneButton
key = 'mute-everyone'
showLabel = { true } />,
this._shouldShowButton('mute-video-everyone')
&& <MuteEveryonesVideoButton
key = 'mute-video-everyone'
showLabel = { true } />,
this._shouldShowButton('stats')
&& <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }