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 9f7dd10..7fd9cb5 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -953,22 +953,60 @@ export default class RoomClient } } - async getAudioTrack() - { - await navigator.mediaDevices.getUserMedia( - { - audio : true, video : false - }); + disconnectLocalHark() { + 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() - { - await navigator.mediaDevices.getUserMedia( - { - audio : false, video : true - }); + connectLocalHark(track) { + 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) { + 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) { logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); @@ -977,7 +1015,7 @@ export default class RoomClient meActions.setAudioInProgress(true)); try - { + { const device = this._audioDevices[deviceId]; if (!device) @@ -987,29 +1025,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 +1056,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)); @@ -1106,6 +1106,37 @@ export default class RoomClient meActions.setAudioOutputInProgress(false)); } + async changeAudioOutputDevice(deviceId) + { + logger.debug('changeAudioOutputDevice() [deviceId: %s]', deviceId); + + store.dispatch( + meActions.setAudioOutputInProgress(true)); + + try + { + const device = this._audioOutputDevices[deviceId]; + + if (!device) + throw new Error('Selected audio output device no longer avaibale'); + + logger.debug( + 'changeAudioOutputDevice() | new selected [audio output device:%o]', + device); + + store.dispatch(settingsActions.setSelectedAudioOutputDevice(deviceId)); + + await this._updateAudioOutputDevices(); + } + catch (error) + { + logger.error('changeAudioOutputDevice() failed: %o', error); + } + + store.dispatch( + meActions.setAudioOutputInProgress(false)); + } + async changeVideoResolution(resolution) { logger.debug('changeVideoResolution() [resolution: %s]', resolution); @@ -1795,6 +1826,49 @@ export default class RoomClient this._recvTransport = null; } + store.dispatch(roomActions.setRoomState('connecting')); + }); + + store.dispatch( + producerActions.removeProducer(this._screenSharingProducer.id)); + + this._screenSharingProducer = null; + } + + if (this._webcamProducer) + { + this._webcamProducer.close(); + + store.dispatch( + producerActions.removeProducer(this._webcamProducer.id)); + + this._webcamProducer = null; + } + + if (this._micProducer) + { + this._micProducer.close(); + + store.dispatch( + producerActions.removeProducer(this._micProducer.id)); + + this._micProducer = null; + } + + if (this._sendTransport) + { + this._sendTransport.close(); + + this._sendTransport = null; + } + + if (this._recvTransport) + { + this._recvTransport.close(); + + this._recvTransport = null; + } + store.dispatch(roomActions.setRoomState('connecting')); }); @@ -3262,11 +3336,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( @@ -3320,51 +3403,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/settingsActions.js b/app/src/actions/settingsActions.js index 112dd0b..5416b02 100644 --- a/app/src/actions/settingsActions.js +++ b/app/src/actions/settingsActions.js @@ -38,6 +38,45 @@ export const togglePermanentTopBar = () => type : 'TOGGLE_PERMANENT_TOPBAR' }); +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/Selectors.js b/app/src/components/Selectors.js index b8e5e41..8f59d5d 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -12,6 +12,10 @@ const peersKeySelector = createSelector( peersSelector, (peers) => Object.keys(peers) ); +const peersValueSelector = createSelector( + peersSelector, + (peers) => Object.values(peers) +); export const peersValueSelector = createSelector( peersSelector, diff --git a/app/src/components/Settings/MediaSettings.js b/app/src/components/Settings/MediaSettings.js index fa9728b..392df88 100644 --- a/app/src/components/Settings/MediaSettings.js +++ b/app/src/components/Settings/MediaSettings.js @@ -3,12 +3,15 @@ import { connect } from 'react-redux'; import * as appPropTypes from '../appPropTypes'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../RoomContext'; +import * as settingsActions from '../../actions/settingsActions'; import PropTypes from 'prop-types'; import { useIntl, FormattedMessage } from 'react-intl'; import MenuItem from '@material-ui/core/MenuItem'; import FormHelperText from '@material-ui/core/FormHelperText'; import FormControl from '@material-ui/core/FormControl'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; import Select from '@material-ui/core/Select'; +import Checkbox from '@material-ui/core/Checkbox'; const styles = (theme) => ({ @@ -23,6 +26,9 @@ const styles = (theme) => }); const MediaSettings = ({ + setEchoCancellation, + setAutoGainControl, + setNoiseSuppression, roomClient, me, settings, @@ -247,6 +253,48 @@ const MediaSettings = ({ /> + { + setEchoCancellation(event.target.checked); + roomClient.changeAudioDevice(settings.selectedAudioDevice); + }} + />} + label={intl.formatMessage({ + id : 'settings.echoCancellation', + defaultMessage : 'Echo Cancellation' + })} + /> + { + setAutoGainControl(event.target.checked); + roomClient.changeAudioDevice(settings.selectedAudioDevice); + }} + />} + label={intl.formatMessage({ + id : 'settings.autoGainControl', + defaultMessage : 'Auto Gain Control' + })} + /> + { + setNoiseSuppression(event.target.checked); + roomClient.changeAudioDevice(settings.selectedAudioDevice); + }} + />} + label={intl.formatMessage({ + id : 'settings.noiseSuppression', + defaultMessage : 'Noise Suppression' + })} + /> ); @@ -254,10 +302,13 @@ const MediaSettings = ({ MediaSettings.propTypes = { - roomClient : PropTypes.any.isRequired, - me : appPropTypes.Me.isRequired, - settings : PropTypes.object.isRequired, - classes : PropTypes.object.isRequired + roomClient : PropTypes.any.isRequired, + setEchoCancellation : PropTypes.func.isRequired, + setAutoGainControl : PropTypes.func.isRequired, + setNoiseSuppression : PropTypes.func.isRequired, + me : appPropTypes.Me.isRequired, + settings : PropTypes.object.isRequired, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => @@ -268,9 +319,15 @@ const mapStateToProps = (state) => }; }; +const mapDispatchToProps = { + setEchoCancellation : settingsActions.setEchoCancellation, + setAutoGainControl : settingsActions.toggleAutoGainControl, + setNoiseSuppression : settingsActions.toggleNoiseSuppression +}; + export default withRoomContext(connect( mapStateToProps, - null, + mapDispatchToProps, null, { areStatesEqual : (next, prev) => diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index 549219c..da96ecc 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -4,12 +4,20 @@ const initialState = selectedWebcam : null, selectedAudioDevice : null, advancedMode : false, + sampleRate : 48000, + channelCount : 1, + volume : 1.0, + autoGainControl : true, + echoCancellation : true, + noiseSuppression : true, + sampleSize : 16, // low, medium, high, veryhigh, ultra resolution : window.config.defaultResolution || 'medium', lastN : 4, permanentTopBar : true, hiddenControls : false, - notificationSounds : true + notificationSounds : true, + ...window.config.defaultAudio }; const settings = (state = initialState, action) => @@ -45,6 +53,83 @@ const settings = (state = initialState, action) => return { ...state, advancedMode }; } + case 'SET_SAMPLE_RATE': + { + const { sampleRate } = action.payload; + + return { ...state, sampleRate }; + } + + case 'SET_CHANNEL_COUNT': + { + const { channelCount } = action.payload; + + return { ...state, channelCount }; + } + + case 'SET_VOLUME': + { + const { volume } = action.payload; + + return { ...state, volume }; + } + + case 'SET_AUTO_GAIN_CONTROL': + { + const { autoGainControl } = action.payload; + + return { ...state, autoGainControl }; + } + + case 'SET_ECHO_CANCELLATION': + { + const { echoCancellation } = action.payload; + + return { ...state, echoCancellation }; + } + + case 'SET_NOISE_SUPPRESSION': + { + const { noiseSuppression } = action.payload; + + return { ...state, noiseSuppression }; + } + + case 'SET_DEFAULT_AUDIO': + { + const { audio } = action.payload; + + return { ...state, audio }; + } + + case 'TOGGLE_AUTO_GAIN_CONTROL': + { + const autoGainControl = !state.autoGainControl; + + return { ...state, autoGainControl }; + } + + case 'TOGGLE_ECHO_CANCELLATION': + { + const echoCancellation = !state.echoCancellation; + + return { ...state, echoCancellation }; + } + + case 'TOGGLE_NOISE_SUPPRESSION': + { + const noiseSuppression = !state.noiseSuppression; + + return { ...state, noiseSuppression }; + } + + case 'SET_SAMPLE_SIZE': + { + const { sampleSize } = action.payload; + + return { ...state, sampleSize }; + } + case 'SET_LAST_N': { const { lastN } = action.payload;