diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index 6b936afb086a01d6aca1d90acfc499371033cb8d..9394f84e2fd0db5ad73f72c2adc29ffcafdd172e 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -4,6 +4,7 @@ import { withModalMounter } from '/imports/ui/components/modal/service'; import AudioManager from '/imports/ui/services/audio-manager'; import { makeCall } from '/imports/ui/services/api'; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; +import logger from '/imports/startup/client/logger'; import AudioControls from './component'; import AudioModalContainer from '../audio-modal/container'; import Service from '../service'; @@ -32,16 +33,37 @@ const processToggleMuteFromOutside = (e) => { } }; +const handleLeaveAudio = () => { + Service.exitAudio(); + logger.info({ + logCode: 'audiocontrols_leave_audio', + extraInfo: { logType: 'user_action' }, + }, 'audio connection closed by user'); +}; + +const { + currentUser, + isConnected, + isListenOnly, + isEchoTest, + isMuted, + isConnecting, + isHangingUp, + isTalking, + toggleMuteMicrophone, + joinListenOnly, +} = Service; + export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => ({ processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg), - showMute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest() && !userLocks.userMic, - muted: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(), - inAudio: Service.isConnected() && !Service.isEchoTest(), - listenOnly: Service.isConnected() && Service.isListenOnly(), - disable: Service.isConnecting() || Service.isHangingUp() || !Meteor.status().connected, - talking: Service.isTalking() && !Service.isMuted(), - currentUser: Service.currentUser(), - handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(), - handleJoinAudio: () => (Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />)), - handleLeaveAudio: () => Service.exitAudio(), + showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic, + muted: isConnected() && !isListenOnly() && isMuted(), + inAudio: isConnected() && !isEchoTest(), + listenOnly: isConnected() && isListenOnly(), + disable: isConnecting() || isHangingUp() || !Meteor.status().connected, + talking: isTalking() && !isMuted(), + currentUser: currentUser(), + handleToggleMuteMicrophone: () => toggleMuteMicrophone(), + handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)), + handleLeaveAudio, }))(AudioControlsContainer))); diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js index 8a84c3a19dc4fa8629455aa02310222410ba359c..c70a5e910dc30f317c874bc1d3afeeee09e7d33c 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/service.js @@ -3,6 +3,9 @@ import Auth from '/imports/ui/services/auth'; import AudioManager from '/imports/ui/services/audio-manager'; import Meetings from '/imports/api/meetings'; import mapUser from '/imports/ui/services/user/mapUser'; +import { makeCall } from '/imports/ui/services/api'; +import VoiceUsers from '/imports/api/voice-users'; +import logger from '/imports/startup/client/logger'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; @@ -34,6 +37,24 @@ const init = (messages, intl) => { const currentUser = () => mapUser(Users.findOne({ intId: Auth.userID })); +const toggleMuteMicrophone = () => { + makeCall('toggleSelfVoice'); + const cvu = VoiceUsers.findOne({ meetingId: Auth.meetingID, intId: Auth.userID }); + if (cvu) { + if (cvu.muted) { + logger.info({ + logCode: 'audiomanager_unmute_audio', + extraInfo: { logType: 'user_action' }, + }, 'microphone unmuted by user'); + } else { + logger.info({ + logCode: 'audiomanager_mute_audio', + extraInfo: { logType: 'user_action' }, + }, 'microphone muted by user'); + } + } +}; + export default { init, exitAudio: () => AudioManager.exitAudio(), @@ -41,7 +62,7 @@ export default { joinListenOnly: () => AudioManager.joinListenOnly(), joinMicrophone: () => AudioManager.joinMicrophone(), joinEchoTest: () => AudioManager.joinEchoTest(), - toggleMuteMicrophone: () => AudioManager.toggleMuteMicrophone(), + toggleMuteMicrophone, changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId), changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId), isConnected: () => AudioManager.isConnected, diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx index faefdb13a699115546ef897b84093d1765cb2d07..faaa783a87501a91d7dc9c6c6e9d8ef8a0128afc 100755 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx @@ -1,7 +1,9 @@ import React, { Component } from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; import { withModalMounter } from '/imports/ui/components/modal/service'; import Modal from '/imports/ui/components/modal/fullscreen/component'; +import logger from '/imports/startup/client/logger'; +import PropTypes from 'prop-types'; import AudioService from '../audio/service'; import VideoService from '../video-provider/service'; import { styles } from './styles'; @@ -37,6 +39,19 @@ const intlMessages = defineMessages({ }, }); +const propTypes = { + intl: intlShape.isRequired, + breakout: PropTypes.objectOf(Object).isRequired, + getURL: PropTypes.func.isRequired, + mountModal: PropTypes.func.isRequired, + breakoutURL: PropTypes.string.isRequired, + isFreeJoin: PropTypes.bool.isRequired, + currentVoiceUser: PropTypes.objectOf(Object).isRequired, + requestJoinURL: PropTypes.func.isRequired, + breakouts: PropTypes.arrayOf(Object).isRequired, + breakoutName: PropTypes.string.isRequired, +}; + class BreakoutJoinConfirmation extends Component { constructor(props) { super(props); @@ -56,10 +71,21 @@ class BreakoutJoinConfirmation extends Component { mountModal, breakoutURL, isFreeJoin, + currentVoiceUser, } = this.props; - const url = isFreeJoin ? getURL(this.state.selectValue) : breakoutURL; - // leave main room's audio when joining a breakout room - AudioService.exitAudio(); + + const { selectValue } = this.state; + const url = isFreeJoin ? getURL(selectValue) : breakoutURL; + + if (currentVoiceUser && currentVoiceUser.joined) { + // leave main room's audio when joining a breakout room + AudioService.exitAudio(); + logger.info({ + logCode: 'breakoutjoinconfirmation_ended_audio', + extraInfo: { logType: 'user_action' }, + }, 'joining breakout room closed audio in the main room'); + } + VideoService.exitVideo(); window.open(url); mountModal(null); @@ -67,21 +93,32 @@ class BreakoutJoinConfirmation extends Component { handleSelectChange(e) { const { value } = e.target; + const { requestJoinURL } = this.props; this.setState({ selectValue: value }); - this.props.requestJoinURL(value); + requestJoinURL(value); } renderSelectMeeting() { const { breakouts, intl } = this.props; + const { selectValue } = this.state; return ( <div className={styles.selectParent}> {`${intl.formatMessage(intlMessages.freeJoinMessage)}`} <select className={styles.select} - value={this.state.selectValue} + value={selectValue} onChange={this.handleSelectChange} > - {breakouts.map(({ name, breakoutId }) => (<option key={breakoutId} value={breakoutId}>{name}</option>))} + { + breakouts.map(({ name, breakoutId }) => ( + <option + key={breakoutId} + value={breakoutId} + > + {name} + </option> + )) + } </select> </div> ); @@ -89,6 +126,7 @@ class BreakoutJoinConfirmation extends Component { render() { const { intl, breakoutName, isFreeJoin } = this.props; + return ( <Modal title={intl.formatMessage(intlMessages.title)} @@ -110,3 +148,5 @@ class BreakoutJoinConfirmation extends Component { } export default withModalMounter(injectIntl(BreakoutJoinConfirmation)); + +BreakoutJoinConfirmation.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx index 15a99b6f0cac37125e31a36517ab74cafee85ccb..56473f9bfe62c0aa0ba75f3e40ad525c297b4309 100755 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx @@ -2,12 +2,16 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import Breakouts from '/imports/api/breakouts'; import Auth from '/imports/ui/services/auth'; +import VoiceUsers from '/imports/api/voice-users/'; import { makeCall } from '/imports/ui/services/api'; import breakoutService from '/imports/ui/components/breakout-room/service'; import BreakoutJoinConfirmationComponent from './component'; -const BreakoutJoinConfirmationContrainer = props => - (<BreakoutJoinConfirmationComponent {...props} />); +const BreakoutJoinConfirmationContrainer = props => ( + <BreakoutJoinConfirmationComponent + {...props} + /> +); const getURL = (breakoutId) => { const currentUserId = Auth.userID; @@ -31,6 +35,8 @@ export default withTracker(({ breakout, mountModal, breakoutName }) => { requestJoinURL(breakoutId); } + const currentVoiceUser = VoiceUsers.findOne({ meetingId: Auth.meetingID, intId: Auth.userID }); + return { isFreeJoin, mountModal, @@ -39,5 +45,6 @@ export default withTracker(({ breakout, mountModal, breakoutName }) => { breakouts: breakoutService.getBreakouts(), requestJoinURL, getURL, + currentVoiceUser, }; })(BreakoutJoinConfirmationContrainer); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 8601ccef6ac6a0952bef5c32b8cc9d63a828fe53..48841c0a425ec0437701a199b3ee94997cba61e8 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -11,6 +11,8 @@ import { EMOJI_STATUSES } from '/imports/utils/statuses'; import { makeCall } from '/imports/ui/services/api'; import _ from 'lodash'; import KEY_CODES from '/imports/utils/keyCodes'; +import AudioService from '/imports/ui/components/audio/service'; +import logger from '/imports/startup/client/logger'; const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; @@ -382,9 +384,13 @@ const removeUser = (userId) => { const toggleVoice = (userId) => { if (userId === Auth.userID) { - makeCall('toggleSelfVoice'); + AudioService.toggleMuteMicrophone(); } else { makeCall('toggleVoice', userId); + logger.info({ + logCode: 'usermenu_option_mute_audio', + extraInfo: { logType: 'moderator_action' }, + }, 'moderator muted user microphone'); } }; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx index c4ee43e7760c97e6cdef698d72379b924c9d7ee5..dde693a807fbc19bd8b0950adda43406b5b096fd 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import Auth from '/imports/ui/services/auth'; import Service from '/imports/ui/components/actions-bar/service'; import userListService from '/imports/ui/components/user-list/service'; +import logger from '/imports/startup/client/logger'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; import { notify } from '/imports/ui/services/notification'; import UserOptions from './component'; @@ -43,9 +44,38 @@ const UserOptionsContainer = withTracker((props) => { ); }; + const isMeetingMuteOnStart = () => { + const { voiceProp } = meeting; + const { muteOnStart } = voiceProp; + return muteOnStart; + }; + + const meetingMuteDisabledLog = () => logger.info({ + logCode: 'useroptions_unmute_all', + extraInfo: { logType: 'moderator_action' }, + }, 'moderator disabled meeting mute'); + return { - toggleMuteAllUsers: () => muteAllUsers(Auth.userID), - toggleMuteAllUsersExceptPresenter: () => muteAllExceptPresenter(Auth.userID), + toggleMuteAllUsers: () => { + muteAllUsers(Auth.userID); + if (isMeetingMuteOnStart()) { + return meetingMuteDisabledLog(); + } + return logger.info({ + logCode: 'useroptions_mute_all', + extraInfo: { logType: 'moderator_action' }, + }, 'moderator enabled meeting mute, all users muted'); + }, + toggleMuteAllUsersExceptPresenter: () => { + muteAllExceptPresenter(Auth.userID); + if (isMeetingMuteOnStart()) { + return meetingMuteDisabledLog(); + } + return logger.info({ + logCode: 'useroptions_mute_all_except_presenter', + extraInfo: { logType: 'moderator_action' }, + }, 'moderator enabled meeting mute, all users muted except presenter'); + }, toggleStatus, isMeetingMuted: meeting.voiceProp.muteOnStart, isUserPresenter: Service.isUserPresenter(), diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 3c69c7c345dc51e96dac95947906b9258d5933b8..aed6453f353fee5313f3763b2a14a67dc2c17d3c 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -1,5 +1,4 @@ import { Tracker } from 'meteor/tracker'; -import { makeCall } from '/imports/ui/services/api'; import KurentoBridge from '/imports/api/audio/client/bridge/kurento'; import Auth from '/imports/ui/services/auth'; import VoiceUsers from '/imports/api/voice-users'; @@ -138,11 +137,13 @@ class AudioManager { extension: ECHO_TEST_NUMBER, inputStream: this.inputStream, }; + logger.info({ logCode: 'audiomanager_join_echotest', extraInfo: { logType: 'user_action' } }, 'User requested to join audio conference with mic'); return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)); }); } - async joinListenOnly(retries = 0) { + async joinListenOnly(r = 0) { + let retries = r; this.isListenOnly = true; this.isEchoTest = false; @@ -180,6 +181,11 @@ class AudioManager { }); const handleListenOnlyError = async (err) => { + const error = { + type: 'MEDIA_ERROR', + message: this.messages.error.MEDIA_ERROR, + }; + if (iceGatheringTimeout) { clearTimeout(iceGatheringTimeout); } @@ -191,12 +197,12 @@ class AudioManager { retries, }, }, 'Listen only error'); - throw { - type: 'MEDIA_ERROR', - message: this.messages.error.MEDIA_ERROR, - }; + + throw error; }; + logger.info({ logCode: 'audiomanager_join_listenonly', extraInfo: { logType: 'user_action' } }, 'user requested to connect to audio conference as listen only'); + return this.onAudioJoining() .then(() => Promise.race([ bridge.joinAudio(callOptions, this.callStateCallback.bind(this)), @@ -214,13 +220,16 @@ class AudioManager { } try { - await this.joinListenOnly(++retries); + retries += 1; + await this.joinListenOnly(retries); } catch (error) { return handleListenOnlyError(error); } } else { - handleListenOnlyError(err); + return handleListenOnlyError(err); } + + return null; }); } @@ -247,10 +256,6 @@ class AudioManager { return this.bridge.transferCall(this.onAudioJoin.bind(this)); } - toggleMuteMicrophone() { - makeCall('toggleSelfVoice'); - } - onAudioJoin() { this.isConnecting = false; this.isConnected = true; @@ -362,9 +367,11 @@ class AudioManager { this.listenOnlyAudioContext.close(); } - this.listenOnlyAudioContext = window.AudioContext - ? new window.AudioContext() - : new window.webkitAudioContext(); + const { AudioContext, WebkitAudioContext } = window; + + this.listenOnlyAudioContext = AudioContext + ? new AudioContext() + : new WebkitAudioContext(); const dest = this.listenOnlyAudioContext.createMediaStreamDestination(); @@ -402,9 +409,11 @@ class AudioManager { return Promise.resolve(inputDevice); }; - const handleChangeInputDeviceError = () => Promise.reject({ - type: 'MEDIA_ERROR', - message: this.messages.error.MEDIA_ERROR, + const handleChangeInputDeviceError = () => new Promise((reject) => { + reject({ + type: 'MEDIA_ERROR', + message: this.messages.error.MEDIA_ERROR, + }); }); if (!deviceId) {