add audio logs (join/leave/mute/unmute) / fix lint issues

This commit is contained in:
KDSBrowne 2019-07-25 18:41:24 +00:00
parent fd7a5918c1
commit 4fba9978d3
7 changed files with 176 additions and 41 deletions

View File

@ -4,6 +4,7 @@ import { withModalMounter } from '/imports/ui/components/modal/service';
import AudioManager from '/imports/ui/services/audio-manager'; import AudioManager from '/imports/ui/services/audio-manager';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
import logger from '/imports/startup/client/logger';
import AudioControls from './component'; import AudioControls from './component';
import AudioModalContainer from '../audio-modal/container'; import AudioModalContainer from '../audio-modal/container';
import Service from '../service'; 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 }) => ({ export default lockContextContainer(withModalMounter(withTracker(({ mountModal, userLocks }) => ({
processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg), processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg),
showMute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest() && !userLocks.userMic, showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic,
muted: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(), muted: isConnected() && !isListenOnly() && isMuted(),
inAudio: Service.isConnected() && !Service.isEchoTest(), inAudio: isConnected() && !isEchoTest(),
listenOnly: Service.isConnected() && Service.isListenOnly(), listenOnly: isConnected() && isListenOnly(),
disable: Service.isConnecting() || Service.isHangingUp() || !Meteor.status().connected, disable: isConnecting() || isHangingUp() || !Meteor.status().connected,
talking: Service.isTalking() && !Service.isMuted(), talking: isTalking() && !isMuted(),
currentUser: Service.currentUser(), currentUser: currentUser(),
handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(), handleToggleMuteMicrophone: () => toggleMuteMicrophone(),
handleJoinAudio: () => (Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />)), handleJoinAudio: () => (isConnected() ? joinListenOnly() : mountModal(<AudioModalContainer />)),
handleLeaveAudio: () => Service.exitAudio(), handleLeaveAudio,
}))(AudioControlsContainer))); }))(AudioControlsContainer)));

View File

@ -3,6 +3,9 @@ import Auth from '/imports/ui/services/auth';
import AudioManager from '/imports/ui/services/audio-manager'; import AudioManager from '/imports/ui/services/audio-manager';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import mapUser from '/imports/ui/services/user/mapUser'; 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; 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 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 { export default {
init, init,
exitAudio: () => AudioManager.exitAudio(), exitAudio: () => AudioManager.exitAudio(),
@ -41,7 +62,7 @@ export default {
joinListenOnly: () => AudioManager.joinListenOnly(), joinListenOnly: () => AudioManager.joinListenOnly(),
joinMicrophone: () => AudioManager.joinMicrophone(), joinMicrophone: () => AudioManager.joinMicrophone(),
joinEchoTest: () => AudioManager.joinEchoTest(), joinEchoTest: () => AudioManager.joinEchoTest(),
toggleMuteMicrophone: () => AudioManager.toggleMuteMicrophone(), toggleMuteMicrophone,
changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId), changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId),
changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId), changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId),
isConnected: () => AudioManager.isConnected, isConnected: () => AudioManager.isConnected,

View File

@ -1,7 +1,9 @@
import React, { Component } from 'react'; 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 { withModalMounter } from '/imports/ui/components/modal/service';
import Modal from '/imports/ui/components/modal/fullscreen/component'; 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 AudioService from '../audio/service';
import VideoService from '../video-provider/service'; import VideoService from '../video-provider/service';
import { styles } from './styles'; 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 { class BreakoutJoinConfirmation extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -56,10 +71,21 @@ class BreakoutJoinConfirmation extends Component {
mountModal, mountModal,
breakoutURL, breakoutURL,
isFreeJoin, isFreeJoin,
currentVoiceUser,
} = this.props; } = this.props;
const url = isFreeJoin ? getURL(this.state.selectValue) : breakoutURL;
// leave main room's audio when joining a breakout room const { selectValue } = this.state;
AudioService.exitAudio(); 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(); VideoService.exitVideo();
window.open(url); window.open(url);
mountModal(null); mountModal(null);
@ -67,21 +93,32 @@ class BreakoutJoinConfirmation extends Component {
handleSelectChange(e) { handleSelectChange(e) {
const { value } = e.target; const { value } = e.target;
const { requestJoinURL } = this.props;
this.setState({ selectValue: value }); this.setState({ selectValue: value });
this.props.requestJoinURL(value); requestJoinURL(value);
} }
renderSelectMeeting() { renderSelectMeeting() {
const { breakouts, intl } = this.props; const { breakouts, intl } = this.props;
const { selectValue } = this.state;
return ( return (
<div className={styles.selectParent}> <div className={styles.selectParent}>
{`${intl.formatMessage(intlMessages.freeJoinMessage)}`} {`${intl.formatMessage(intlMessages.freeJoinMessage)}`}
<select <select
className={styles.select} className={styles.select}
value={this.state.selectValue} value={selectValue}
onChange={this.handleSelectChange} 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> </select>
</div> </div>
); );
@ -89,6 +126,7 @@ class BreakoutJoinConfirmation extends Component {
render() { render() {
const { intl, breakoutName, isFreeJoin } = this.props; const { intl, breakoutName, isFreeJoin } = this.props;
return ( return (
<Modal <Modal
title={intl.formatMessage(intlMessages.title)} title={intl.formatMessage(intlMessages.title)}
@ -110,3 +148,5 @@ class BreakoutJoinConfirmation extends Component {
} }
export default withModalMounter(injectIntl(BreakoutJoinConfirmation)); export default withModalMounter(injectIntl(BreakoutJoinConfirmation));
BreakoutJoinConfirmation.propTypes = propTypes;

View File

@ -2,12 +2,16 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import Breakouts from '/imports/api/breakouts'; import Breakouts from '/imports/api/breakouts';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import VoiceUsers from '/imports/api/voice-users/';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import breakoutService from '/imports/ui/components/breakout-room/service'; import breakoutService from '/imports/ui/components/breakout-room/service';
import BreakoutJoinConfirmationComponent from './component'; import BreakoutJoinConfirmationComponent from './component';
const BreakoutJoinConfirmationContrainer = props => const BreakoutJoinConfirmationContrainer = props => (
(<BreakoutJoinConfirmationComponent {...props} />); <BreakoutJoinConfirmationComponent
{...props}
/>
);
const getURL = (breakoutId) => { const getURL = (breakoutId) => {
const currentUserId = Auth.userID; const currentUserId = Auth.userID;
@ -31,6 +35,8 @@ export default withTracker(({ breakout, mountModal, breakoutName }) => {
requestJoinURL(breakoutId); requestJoinURL(breakoutId);
} }
const currentVoiceUser = VoiceUsers.findOne({ meetingId: Auth.meetingID, intId: Auth.userID });
return { return {
isFreeJoin, isFreeJoin,
mountModal, mountModal,
@ -39,5 +45,6 @@ export default withTracker(({ breakout, mountModal, breakoutName }) => {
breakouts: breakoutService.getBreakouts(), breakouts: breakoutService.getBreakouts(),
requestJoinURL, requestJoinURL,
getURL, getURL,
currentVoiceUser,
}; };
})(BreakoutJoinConfirmationContrainer); })(BreakoutJoinConfirmationContrainer);

View File

@ -11,6 +11,8 @@ import { EMOJI_STATUSES } from '/imports/utils/statuses';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import _ from 'lodash'; import _ from 'lodash';
import KEY_CODES from '/imports/utils/keyCodes'; 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 CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
@ -382,9 +384,13 @@ const removeUser = (userId) => {
const toggleVoice = (userId) => { const toggleVoice = (userId) => {
if (userId === Auth.userID) { if (userId === Auth.userID) {
makeCall('toggleSelfVoice'); AudioService.toggleMuteMicrophone();
} else { } else {
makeCall('toggleVoice', userId); makeCall('toggleVoice', userId);
logger.info({
logCode: 'usermenu_option_mute_audio',
extraInfo: { logType: 'moderator_action' },
}, 'moderator muted user microphone');
} }
}; };

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import Service from '/imports/ui/components/actions-bar/service'; import Service from '/imports/ui/components/actions-bar/service';
import userListService from '/imports/ui/components/user-list/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 { defineMessages, injectIntl, intlShape } from 'react-intl';
import { notify } from '/imports/ui/services/notification'; import { notify } from '/imports/ui/services/notification';
import UserOptions from './component'; 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 { return {
toggleMuteAllUsers: () => muteAllUsers(Auth.userID), toggleMuteAllUsers: () => {
toggleMuteAllUsersExceptPresenter: () => muteAllExceptPresenter(Auth.userID), 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, toggleStatus,
isMeetingMuted: meeting.voiceProp.muteOnStart, isMeetingMuted: meeting.voiceProp.muteOnStart,
isUserPresenter: Service.isUserPresenter(), isUserPresenter: Service.isUserPresenter(),

View File

@ -1,5 +1,4 @@
import { Tracker } from 'meteor/tracker'; import { Tracker } from 'meteor/tracker';
import { makeCall } from '/imports/ui/services/api';
import KurentoBridge from '/imports/api/audio/client/bridge/kurento'; import KurentoBridge from '/imports/api/audio/client/bridge/kurento';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import VoiceUsers from '/imports/api/voice-users'; import VoiceUsers from '/imports/api/voice-users';
@ -138,11 +137,13 @@ class AudioManager {
extension: ECHO_TEST_NUMBER, extension: ECHO_TEST_NUMBER,
inputStream: this.inputStream, 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)); return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
}); });
} }
async joinListenOnly(retries = 0) { async joinListenOnly(r = 0) {
let retries = r;
this.isListenOnly = true; this.isListenOnly = true;
this.isEchoTest = false; this.isEchoTest = false;
@ -180,6 +181,11 @@ class AudioManager {
}); });
const handleListenOnlyError = async (err) => { const handleListenOnlyError = async (err) => {
const error = {
type: 'MEDIA_ERROR',
message: this.messages.error.MEDIA_ERROR,
};
if (iceGatheringTimeout) { if (iceGatheringTimeout) {
clearTimeout(iceGatheringTimeout); clearTimeout(iceGatheringTimeout);
} }
@ -191,12 +197,12 @@ class AudioManager {
retries, retries,
}, },
}, 'Listen only error'); }, 'Listen only error');
throw {
type: 'MEDIA_ERROR', throw error;
message: this.messages.error.MEDIA_ERROR,
};
}; };
logger.info({ logCode: 'audiomanager_join_listenonly', extraInfo: { logType: 'user_action' } }, 'user requested to connect to audio conference as listen only');
return this.onAudioJoining() return this.onAudioJoining()
.then(() => Promise.race([ .then(() => Promise.race([
bridge.joinAudio(callOptions, this.callStateCallback.bind(this)), bridge.joinAudio(callOptions, this.callStateCallback.bind(this)),
@ -214,13 +220,16 @@ class AudioManager {
} }
try { try {
await this.joinListenOnly(++retries); retries += 1;
await this.joinListenOnly(retries);
} catch (error) { } catch (error) {
return handleListenOnlyError(error); return handleListenOnlyError(error);
} }
} else { } else {
handleListenOnlyError(err); return handleListenOnlyError(err);
} }
return null;
}); });
} }
@ -247,10 +256,6 @@ class AudioManager {
return this.bridge.transferCall(this.onAudioJoin.bind(this)); return this.bridge.transferCall(this.onAudioJoin.bind(this));
} }
toggleMuteMicrophone() {
makeCall('toggleSelfVoice');
}
onAudioJoin() { onAudioJoin() {
this.isConnecting = false; this.isConnecting = false;
this.isConnected = true; this.isConnected = true;
@ -362,9 +367,11 @@ class AudioManager {
this.listenOnlyAudioContext.close(); this.listenOnlyAudioContext.close();
} }
this.listenOnlyAudioContext = window.AudioContext const { AudioContext, WebkitAudioContext } = window;
? new window.AudioContext()
: new window.webkitAudioContext(); this.listenOnlyAudioContext = AudioContext
? new AudioContext()
: new WebkitAudioContext();
const dest = this.listenOnlyAudioContext.createMediaStreamDestination(); const dest = this.listenOnlyAudioContext.createMediaStreamDestination();
@ -402,9 +409,11 @@ class AudioManager {
return Promise.resolve(inputDevice); return Promise.resolve(inputDevice);
}; };
const handleChangeInputDeviceError = () => Promise.reject({ const handleChangeInputDeviceError = () => new Promise((reject) => {
type: 'MEDIA_ERROR', reject({
message: this.messages.error.MEDIA_ERROR, type: 'MEDIA_ERROR',
message: this.messages.error.MEDIA_ERROR,
});
}); });
if (!deviceId) { if (!deviceId) {