diff --git a/app/package.json b/app/package.json index ec92c5b..fae4e97 100644 --- a/app/package.json +++ b/app/package.json @@ -27,6 +27,7 @@ "react": "^16.10.2", "react-cookie-consent": "^2.5.0", "react-dom": "^16.10.2", + "react-flip-toolkit": "^7.0.9", "react-intl": "^3.4.0", "react-redux": "^7.1.1", "react-router-dom": "^5.1.2", diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index 3f0ce49..b591c16 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -42,6 +42,17 @@ var config = { tcp : true }, + defaultAudio : + { + sampleRate : 48000, + channelCount : 1, + volume : 1.0, + autoGainControl : true, + echoCancellation : true, + noiseSuppression : true, + sampleSize : 16 + }, + background : 'images/background.jpg', defaultLayout : 'democratic', // democratic, filmstrip lastN : 4, mobileLastN : 1, @@ -49,7 +60,6 @@ var config = maxLastN : 5, // If truthy, users can NOT change number of speakers visible lockLastN : false, - background : 'images/background.jpg', // Add file and uncomment for adding logo to appbar // logo : 'images/logo.svg', title : 'Multiparty meeting', diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 8a502be..df276ae 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -475,9 +475,9 @@ export default class RoomClient window.open(url, 'loginWindow'); } - logout() + logout(roomId = this._roomId) { - window.open('/auth/logout', 'logoutWindow'); + window.open(`/auth/logout?peerId=${this._peerId}&roomId=${roomId}`, 'logoutWindow'); } receiveLoginChildWindow(data) @@ -953,20 +953,65 @@ export default class RoomClient } } - async getAudioTrack() + disconnectLocalHark() { - await navigator.mediaDevices.getUserMedia( - { - audio : true, video : false - }); + logger.debug('disconnectLocalHark() | Stopping harkStream.'); + if (this._harkStream != null) + { + this._harkStream.getAudioTracks()[0].stop(); + this._harkStream = null; + } + + if (this._hark != null) + { + logger.debug('disconnectLocalHark() Stopping hark.'); + this._hark.stop(); + } } - async getVideoTrack() + connectLocalHark(track) { - await navigator.mediaDevices.getUserMedia( + logger.debug('connectLocalHark() | Track:%o', track); + this._harkStream = new MediaStream(); + + this._harkStream.addTrack(track.clone()); + this._harkStream.getAudioTracks()[0].enabled = true; + + if (!this._harkStream.getAudioTracks()[0]) + throw new Error('getMicStream():something went wrong with hark'); + + this._hark = hark(this._harkStream, { play: false }); + + // eslint-disable-next-line no-unused-vars + this._hark.on('volume_change', (dBs, threshold) => + { + // The exact formula to convert from dBs (-100..0) to linear (0..1) is: + // Math.pow(10, dBs / 20) + // However it does not produce a visually useful output, so let exagerate + // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to + // minimize component renderings. + let volume = Math.round(Math.pow(10, dBs / 85) * 10); + + if (volume === 1) + volume = 0; + + volume = Math.round(volume); + + if (this._micProducer && volume !== this._micProducer.volume) { - audio : false, video : true - }); + this._micProducer.volume = volume; + + store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); + } + }); + this._hark.on('speaking', function() + { + store.dispatch(meActions.setIsSpeaking(true)); + }); + this._hark.on('stopped_speaking', function() + { + store.dispatch(meActions.setIsSpeaking(false)); + }); } async changeAudioDevice(deviceId) @@ -977,7 +1022,7 @@ export default class RoomClient meActions.setAudioInProgress(true)); try - { + { const device = this._audioDevices[deviceId]; if (!device) @@ -987,29 +1032,30 @@ export default class RoomClient 'changeAudioDevice() | new selected webcam [device:%o]', device); - if (this._hark != null) - this._hark.stop(); - - if (this._harkStream != null) - { - logger.debug('Stopping hark.'); - this._harkStream.getAudioTracks()[0].stop(); - this._harkStream = null; - } + this.disconnectLocalHark(); if (this._micProducer && this._micProducer.track) this._micProducer.track.stop(); - logger.debug('changeAudioDevice() | calling getUserMedia()'); + logger.debug('changeAudioDevice() | calling getUserMedia() %o', store.getState().settings); const stream = await navigator.mediaDevices.getUserMedia( { audio : { - deviceId : { exact: device.deviceId } + deviceId : { ideal: device.deviceId }, + sampleRate : store.getState().settings.sampleRate, + channelCount : store.getState().settings.channelCount, + volume : store.getState().settings.volume, + autoGainControl : store.getState().settings.autoGainControl, + echoCancellation : store.getState().settings.echoCancellation, + noiseSuppression : store.getState().settings.noiseSuppression, + sampleSize : store.getState().settings.sampleSize } - }); + } + ); + logger.debug('Constraints: %o', stream.getAudioTracks()[0].getConstraints()); const track = stream.getAudioTracks()[0]; if (this._micProducer) @@ -1017,47 +1063,8 @@ export default class RoomClient if (this._micProducer) this._micProducer.volume = 0; + this.connectLocalHark(track); - this._harkStream = new MediaStream(); - - this._harkStream.addTrack(track.clone()); - this._harkStream.getAudioTracks()[0].enabled = true; - - if (!this._harkStream.getAudioTracks()[0]) - throw new Error('changeAudioDevice(): given stream has no audio track'); - - this._hark = hark(this._harkStream, { play: false }); - - // eslint-disable-next-line no-unused-vars - this._hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exaggerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - volume = Math.round(volume); - - if (this._micProducer && volume !== this._micProducer.volume) - { - this._micProducer.volume = volume; - - store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); - } - }); - this._hark.on('speaking', function() - { - store.dispatch(meActions.setIsSpeaking(true)); - }); - this._hark.on('stopped_speaking', function() - { - store.dispatch(meActions.setIsSpeaking(false)); - }); if (this._micProducer && this._micProducer.id) store.dispatch( producerActions.setProducerTrack(this._micProducer.id, track)); @@ -1533,6 +1540,26 @@ export default class RoomClient } } + async lowerPeerHand(peerId) + { + logger.debug('lowerPeerHand() [peerId:"%s"]', peerId); + + store.dispatch( + peerActions.setPeerRaisedHandInProgress(peerId, true)); + + try + { + await this.sendRequest('moderator:lowerHand', { peerId }); + } + catch (error) + { + logger.error('lowerPeerHand() | [error:"%o"]', error); + } + + store.dispatch( + peerActions.setPeerRaisedHandInProgress(peerId, false)); + } + async setRaisedHand(raisedHand) { logger.debug('setRaisedHand: ', raisedHand); @@ -2534,6 +2561,13 @@ export default class RoomClient break; } + case 'moderator:lowerHand': + { + this.setRaisedHand(false); + + break; + } + case 'gotRole': { const { peerId, role } = notification.data; @@ -2811,7 +2845,9 @@ export default class RoomClient { text : intl.formatMessage({ id : 'roles.gotRole', - defaultMessage : `You got the role: ${role}` + defaultMessage : 'You got the role: {role}' + }, { + role }) })); } @@ -3233,11 +3269,20 @@ export default class RoomClient const stream = await navigator.mediaDevices.getUserMedia( { audio : { - deviceId : { ideal: deviceId } + deviceId : { ideal: device.deviceId }, + sampleRate : store.getState().settings.sampleRate, + channelCount : store.getState().settings.channelCount, + volume : store.getState().settings.volume, + autoGainControl : store.getState().settings.autoGainControl, + echoCancellation : store.getState().settings.echoCancellation, + noiseSuppression : store.getState().settings.noiseSuppression, + sampleSize : store.getState().settings.sampleSize } } ); + logger.debug('Constraints: %o', stream.getAudioTracks()[0].getConstraints()); + track = stream.getAudioTracks()[0]; this._micProducer = await this._sendTransport.produce( @@ -3291,51 +3336,8 @@ export default class RoomClient this._micProducer.volume = 0; - if (this._hark != null) - this._hark.stop(); + this.connectLocalHark(track); - if (this._harkStream != null) - this._harkStream.getAudioTracks()[0].stop(); - - this._harkStream = new MediaStream(); - - this._harkStream.addTrack(track.clone()); - - if (!this._harkStream.getAudioTracks()[0]) - throw new Error('enableMic(): given stream has no audio track'); - - this._hark = hark(this._harkStream, { play: false }); - - // eslint-disable-next-line no-unused-vars - this._hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exaggerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - volume = Math.round(volume); - - if (this._micProducer && volume !== this._micProducer.volume) - { - this._micProducer.volume = volume; - - store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); - } - }); - this._hark.on('speaking', function() - { - store.dispatch(meActions.setIsSpeaking(true)); - }); - this._hark.on('stopped_speaking', function() - { - store.dispatch(meActions.setIsSpeaking(false)); - }); } catch (error) { diff --git a/app/src/actions/peerActions.js b/app/src/actions/peerActions.js index fee30a5..414b744 100644 --- a/app/src/actions/peerActions.js +++ b/app/src/actions/peerActions.js @@ -40,6 +40,12 @@ export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) => payload : { peerId, raisedHand, raisedHandTimestamp } }); +export const setPeerRaisedHandInProgress = (peerId, flag) => + ({ + type : 'SET_PEER_RAISED_HAND_IN_PROGRESS', + payload : { peerId, flag } + }); + export const setPeerPicture = (peerId, picture) => ({ type : 'SET_PEER_PICTURE', diff --git a/app/src/actions/settingsActions.js b/app/src/actions/settingsActions.js index 112dd0b..63c12bf 100644 --- a/app/src/actions/settingsActions.js +++ b/app/src/actions/settingsActions.js @@ -38,6 +38,50 @@ export const togglePermanentTopBar = () => type : 'TOGGLE_PERMANENT_TOPBAR' }); +export const toggleShowNotifications = () => + ({ + type : 'TOGGLE_SHOW_NOTIFICATIONS' + }); + +export const setEchoCancellation = (echoCancellation) => + ({ + type : 'SET_ECHO_CANCELLATION', + payload : { echoCancellation } + }); + +export const setAutoGainControl = (autoGainControl) => + ({ + type : 'SET_AUTO_GAIN_CONTROL', + payload : { autoGainControl } + }); + +export const setNoiseSuppression = (noiseSuppression) => + ({ + type : 'SET_NOISE_SUPPRESSION', + payload : { noiseSuppression } + }); + +export const setDefaultAudio = (audio) => + ({ + type : 'SET_DEFAULT_AUDIO', + payload : { audio } + }); + +export const toggleEchoCancellation = () => + ({ + type : 'TOGGLE_ECHO_CANCELLATION' + }); + +export const toggleAutoGainControl = () => + ({ + type : 'TOGGLE_AUTO_GAIN_CONTROL' + }); + +export const toggleNoiseSuppression = () => + ({ + type : 'TOGGLE_NOISE_SUPPRESSION' + }); + export const toggleHiddenControls = () => ({ type : 'TOGGLE_HIDDEN_CONTROLS' diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index cbe4bee..6422788 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -16,11 +16,13 @@ import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import MenuItem from '@material-ui/core/MenuItem'; import Menu from '@material-ui/core/Menu'; +import Popover from '@material-ui/core/Popover'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; import Avatar from '@material-ui/core/Avatar'; import Badge from '@material-ui/core/Badge'; +import Paper from '@material-ui/core/Paper'; import ExtensionIcon from '@material-ui/icons/Extension'; import AccountCircle from '@material-ui/icons/AccountCircle'; import FullScreenIcon from '@material-ui/icons/Fullscreen'; @@ -33,6 +35,7 @@ import LockOpenIcon from '@material-ui/icons/LockOpen'; import VideoCallIcon from '@material-ui/icons/VideoCall'; import Button from '@material-ui/core/Button'; import Tooltip from '@material-ui/core/Tooltip'; +import MoreIcon from '@material-ui/icons/MoreVert'; const styles = (theme) => ({ @@ -77,9 +80,17 @@ const styles = (theme) => display : 'block' } }, - actionButtons : - { - display : 'flex' + sectionDesktop : { + display : 'none', + [theme.breakpoints.up('md')] : { + display : 'flex' + } + }, + sectionMobile : { + display : 'flex', + [theme.breakpoints.up('md')] : { + display : 'none' + } }, actionButton : { @@ -96,7 +107,7 @@ const styles = (theme) => }, moreAction : { - margin : theme.spacing(0, 0, 0, 1) + margin : theme.spacing(0.5, 0, 0.5, 1.5) } }); @@ -135,16 +146,36 @@ const TopBar = (props) => { const intl = useIntl(); - const [ moreActionsElement, setMoreActionsElement ] = useState(null); + const [ mobileMoreAnchorEl, setMobileMoreAnchorEl ] = useState(null); + const [ anchorEl, setAnchorEl ] = useState(null); + const [ currentMenu, setCurrentMenu ] = useState(null); - const handleMoreActionsOpen = (event) => + const handleExited = () => { - setMoreActionsElement(event.currentTarget); + setCurrentMenu(null); }; - const handleMoreActionsClose = () => + const handleMobileMenuOpen = (event) => { - setMoreActionsElement(null); + setMobileMoreAnchorEl(event.currentTarget); + }; + + const handleMobileMenuClose = () => + { + setMobileMoreAnchorEl(null); + }; + + const handleMenuOpen = (event, menu) => + { + setAnchorEl(event.currentTarget); + setCurrentMenu(menu); + }; + + const handleMenuClose = () => + { + setAnchorEl(null); + + handleMobileMenuClose(); }; const { @@ -171,7 +202,8 @@ const TopBar = (props) => classes } = props; - const isMoreActionsMenuOpen = Boolean(moreActionsElement); + const isMenuOpen = Boolean(anchorEl); + const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); const lockTooltip = room.locked ? intl.formatMessage({ @@ -239,10 +271,15 @@ const TopBar = (props) => { window.config.title ? window.config.title : 'Multiparty meeting' }
-{videoWidth}x{videoHeight}
} - { !isMe && + { !isMe &&If you report this error, please also report this - tracking ID which makes it possible to locate your session - in the logs which are available to the system administrator: - ${trackingId}
` - ); - logger.error( - 'Express error handler dump with tracking ID: %s, error dump: %o', - trackingId, err); - } - - app.use(errorHandler); - - // Log rooms status every 30 seconds. - setInterval(() => - { - for (const room of rooms.values()) + // start Prometheus exporter + if (config.prometheus) { - room.logStatus(); + await promExporter(rooms, peers, config.prometheus); } - }, 120000); - // check for deserted rooms - setInterval(() => - { - for (const room of rooms.values()) + if (typeof(config.auth) === 'undefined') { - room.checkEmpty(); + logger.warn('Auth is not configured properly!'); } - }, 10000); + else + { + await setupAuth(); + } + + // Run a mediasoup Worker. + await runMediasoupWorkers(); + + // Run HTTPS server. + await runHttpsServer(); + + // Run WebSocketServer. + await runWebSocketServer(); + + const errorHandler = (err, req, res, next) => + { + const trackingId = uuidv4(); + + res.status(500).send( + `If you report this error, please also report this + tracking ID which makes it possible to locate your session + in the logs which are available to the system administrator: + ${trackingId}
` + ); + logger.error( + 'Express error handler dump with tracking ID: %s, error dump: %o', + trackingId, err); + }; + + // eslint-disable-next-line no-unused-vars + app.use(errorHandler); + } + catch (error) + { + logger.error('run() [error:"%o"]', error); + } } function statusLog() @@ -197,8 +186,8 @@ function statusLog() if (statusLogger) { statusLogger.log({ - rooms : rooms.size, - peers : peers.size + rooms : rooms, + peers : peers }); } }