Merge branch 'feat-audio-settings' into develop

auto_join_3.3
Stefan Otto 2020-05-19 01:56:24 +02:00
commit 0f053d0282
10 changed files with 291 additions and 135 deletions

View File

@ -44,13 +44,14 @@ var config =
}, },
defaultAudio : defaultAudio :
{ {
sampleRate : 48000, sampleRate : 48000,
channelCount : 1, channelCount : 1,
volume : 1.0, volume : 1.0,
autoGainControl : true, autoGainControl : true,
echoCancellation : true, echoCancellation : true,
noiseSuppression : true, noiseSuppression : true,
sampleSize : 16 voiceActivityMute : false,
sampleSize : 16
}, },
/** /**

View File

@ -1003,37 +1003,50 @@ export default class RoomClient
if (!this._harkStream.getAudioTracks()[0]) if (!this._harkStream.getAudioTracks()[0])
throw new Error('getMicStream():something went wrong with hark'); throw new Error('getMicStream():something went wrong with hark');
this._hark = hark(this._harkStream, { play: false }); this._hark = hark(this._harkStream,
// 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; play : false,
interval : 5,
threshold : store.getState().settings.noiseThreshold,
history : 30
});
this._hark.lastVolume = -100;
this._hark.on('volume_change', (volume) =>
{
volume = Math.round(volume);
if (this._micProducer && volume !== Math.round(this._hark.lastVolume))
{
if (volume < this._hark.lastVolume * 1.02)
{
volume = this._hark.lastVolume * 1.02;
}
this._hark.lastVolume = volume;
store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume)); store.dispatch(peerVolumeActions.setPeerVolume(this._peerId, volume));
} }
}); });
this._hark.on('speaking', function() this._hark.on('speaking', () =>
{ {
store.dispatch(meActions.setIsSpeaking(true)); store.dispatch(meActions.setIsSpeaking(true));
if ((store.getState().settings.voiceActivatedUnmute ||
store.getState().me.isAutoMuted) &&
this._micProducer &&
this._micProducer.paused)
{
this._micProducer.resume();
}
store.dispatch(meActions.setAutoMuted(false)); // sanity action
}); });
this._hark.on('stopped_speaking', function() this._hark.on('stopped_speaking', () =>
{ {
store.dispatch(meActions.setIsSpeaking(false)); store.dispatch(meActions.setIsSpeaking(false));
if (store.getState().settings.voiceActivatedUnmute &&
this._micProducer &&
!this._micProducer.paused)
{
this._micProducer.pause();
store.dispatch(meActions.setAutoMuted(true));
}
}); });
} }
@ -1962,23 +1975,12 @@ export default class RoomClient
producerPaused producerPaused
} = request.data; } = request.data;
let codecOptions;
if (kind === 'audio')
{
codecOptions =
{
opusStereo : 1
};
}
const consumer = await this._recvTransport.consume( const consumer = await this._recvTransport.consume(
{ {
id, id,
producerId, producerId,
kind, kind,
rtpParameters, rtpParameters,
codecOptions,
appData : { ...appData, peerId } // Trick. appData : { ...appData, peerId } // Trick.
}); });
@ -2031,19 +2033,8 @@ export default class RoomClient
consumer.hark = hark(stream, { play: false }); consumer.hark = hark(stream, { play: false });
// eslint-disable-next-line no-unused-vars consumer.hark.on('volume_change', (volume) =>
consumer.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); volume = Math.round(volume);
if (consumer && volume !== consumer.volume) if (consumer && volume !== consumer.volume)
@ -3888,6 +3879,14 @@ export default class RoomClient
store.dispatch(meActions.setWebcamInProgress(false)); store.dispatch(meActions.setWebcamInProgress(false));
} }
async _setNoiseThreshold(threshold)
{
logger.debug('_setNoiseThreshold:%s', threshold);
this._hark.setThreshold(threshold);
store.dispatch(
settingsActions.setNoiseThreshold(threshold));
}
async _updateAudioDevices() async _updateAudioDevices()
{ {
logger.debug('_updateAudioDevices()'); logger.debug('_updateAudioDevices()');

View File

@ -110,3 +110,9 @@ export const setIsSpeaking = (flag) =>
type : 'SET_IS_SPEAKING', type : 'SET_IS_SPEAKING',
payload : { flag } payload : { flag }
}); });
export const setAutoMuted = (flag) =>
({
type : 'SET_AUTO_MUTED',
payload : { flag }
});

View File

@ -71,6 +71,18 @@ export const setNoiseSuppression = (noiseSuppression) =>
payload : { noiseSuppression } payload : { noiseSuppression }
}); });
export const setVoiceActivatedUnmute = (voiceActivatedUnmute) =>
({
type: 'SET_VOICE_ACTIVATED_UNMUTE',
payload: { voiceActivatedUnmute }
});
export const setNoiseThreshold = (noiseThreshold) =>
({
type: 'SET_NOISE_THRESHOLD',
payload: { noiseThreshold }
});
export const setDefaultAudio = (audio) => export const setDefaultAudio = (audio) =>
({ ({
type : 'SET_DEFAULT_AUDIO', type : 'SET_DEFAULT_AUDIO',

View File

@ -137,10 +137,10 @@ const styles = (theme) =>
transform : 'translate(-50%, 0%)', transform : 'translate(-50%, 0%)',
color : 'rgba(255, 255, 255, 0.7)', color : 'rgba(255, 255, 255, 0.7)',
fontSize : '1.3em', fontSize : '1.3em',
backgroundColor : 'rgba(255, 0, 0, 0.9)', backgroundColor : 'rgba(245, 0, 87, 0.70)',
margin : '4px', margin : '4px',
padding : theme.spacing(2), padding : theme.spacing(2),
zIndex : 31, zIndex : 1200,
borderRadius : '20px', borderRadius : '20px',
textAlign : 'center', textAlign : 'center',
opacity : 0, opacity : 0,
@ -176,6 +176,7 @@ const Me = (props) =>
screenProducer, screenProducer,
extraVideoProducers, extraVideoProducers,
canShareScreen, canShareScreen,
noiseVolume,
classes classes
} = props; } = props;
@ -440,7 +441,12 @@ const Me = (props) =>
})} })}
className={classes.smallContainer} className={classes.smallContainer}
disabled={!me.canSendMic || me.audioInProgress} disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'primary' : 'secondary'} color={
micState === 'on' ?
settings.voiceActivatedUnmute && !me.isAutoMuted ?
'primary'
: 'default'
: 'secondary'}
size='small' size='small'
onClick={() => onClick={() =>
{ {
@ -453,7 +459,10 @@ const Me = (props) =>
}} }}
> >
{ micState === 'on' ? { micState === 'on' ?
<MicIcon /> <MicIcon
color={me.isAutoMuted ? 'secondary' : 'primary'}
style={{ opacity: noiseVolume }}
/>
: :
<MicOffIcon /> <MicOffIcon />
} }
@ -468,7 +477,10 @@ const Me = (props) =>
})} })}
className={classes.fab} className={classes.fab}
disabled={!me.canSendMic || me.audioInProgress} disabled={!me.canSendMic || me.audioInProgress}
color={micState === 'on' ? 'default' : 'secondary'} color={micState === 'on' ?
settings.voiceActivatedUnmute && !me.isAutoMuted? 'primary'
: 'default'
: 'secondary'}
size='large' size='large'
onClick={() => onClick={() =>
{ {
@ -481,7 +493,11 @@ const Me = (props) =>
}} }}
> >
{ micState === 'on' ? { micState === 'on' ?
<MicIcon /> <MicIcon
color={me.isAutoMuted ? 'secondary' : 'primary'}
style={me.isAutoMuted ? { opacity: noiseVolume }
: { opacity: 1 }}
/>
: :
<MicOffIcon /> <MicOffIcon />
} }
@ -868,6 +884,7 @@ Me.propTypes =
style : PropTypes.object, style : PropTypes.object,
smallContainer : PropTypes.bool, smallContainer : PropTypes.bool,
canShareScreen : PropTypes.bool.isRequired, canShareScreen : PropTypes.bool.isRequired,
noiseVolume : PropTypes.number,
classes : PropTypes.object.isRequired, classes : PropTypes.object.isRequired,
theme : PropTypes.object.isRequired theme : PropTypes.object.isRequired
}; };
@ -878,12 +895,26 @@ const makeMapStateToProps = () =>
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
let volume;
// noiseVolume under threshold
if (state.peerVolumes[state.me.id] < state.settings.noiseThreshold)
{
// noiseVolume mapped to range 0.5 ... 1 (threshold switch)
volume = 1 + (Math.abs(state.peerVolumes[state.me.id] -
state.settings.noiseThreshold) /
state.settings.noiseThreshold) / 2;
}
// noiseVolume over threshold: no noise but voice
else { volume = 0; }
return { return {
me : state.me, me : state.me,
...meProducersSelector(state), ...meProducersSelector(state),
settings : state.settings, settings : state.settings,
activeSpeaker : state.me.id === state.room.activeSpeakerId, activeSpeaker : state.me.id === state.room.activeSpeakerId,
canShareScreen : hasPermission(state) canShareScreen : hasPermission(state),
noiseVolume : volume
}; };
}; };
@ -900,6 +931,8 @@ export default withRoomContext(connect(
return ( return (
prev.room === next.room && prev.room === next.room &&
prev.me === next.me && prev.me === next.me &&
Math.round(prev.peerVolumes[prev.me.id]) ===
Math.round(next.peerVolumes[next.me.id]) &&
prev.peers === next.peers && prev.peers === next.peers &&
prev.producers === next.producers && prev.producers === next.producers &&
prev.settings === next.settings prev.settings === next.settings

View File

@ -94,17 +94,17 @@ const styles = () =>
smallBar : smallBar :
{ {
flex : '0 0 auto', flex : '0 0 auto',
margin : '0.3rem',
backgroundSize : '75%', backgroundSize : '75%',
backgroundRepeat : 'no-repeat', backgroundRepeat : 'no-repeat',
backgroundColor : 'rgba(0, 0, 0, 1)', backgroundColor : 'rgba(0, 0, 0, 1)',
cursor : 'pointer', cursor : 'pointer',
transitionProperty : 'opacity, background-color', transitionProperty : 'opacity, background-color',
width : 3, width : 3,
borderRadius : 6, borderRadius : 2,
transitionDuration : '0.25s', transitionDuration : '0.25s',
position : 'absolute', position : 'absolute',
bottom : 0, top : '50%',
transform : 'translateY(-50%)',
'&.level0' : { height: 0 }, '&.level0' : { height: 0 },
'&.level1' : { height: '0.2vh' }, '&.level1' : { height: '0.2vh' },
'&.level2' : { height: '0.4vh' }, '&.level2' : { height: '0.4vh' },
@ -149,9 +149,16 @@ const makeMapStateToProps = (initialState, props) =>
{ {
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { if (state.peerVolumes[props.id]>state.settings.noiseThreshold)
volume : state.peerVolumes[props.id] {
}; return {
volume : Math.round((state.peerVolumes[props.id]+100) / 10)
};
}
else
{
return { volume: 0 };
}
}; };
return mapStateToProps; return mapStateToProps;

View File

@ -6,31 +6,66 @@ import { withRoomContext } from '../../RoomContext';
import * as settingsActions from '../../actions/settingsActions'; import * as settingsActions from '../../actions/settingsActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl, FormattedMessage } from 'react-intl'; import { useIntl, FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText'; import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl'; import FormControl from '@material-ui/core/FormControl';
import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormControlLabel from '@material-ui/core/FormControlLabel';
import Select from '@material-ui/core/Select'; import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox'; import Checkbox from '@material-ui/core/Checkbox';
import Slider from '@material-ui/core/Slider';
import Typography from '@material-ui/core/Typography';
const styles = (theme) => const NoiseSlider = withStyles(
({ {
setting : root :
{ {
padding : theme.spacing(2) color : '#3880ff',
height : 2,
padding : '15px 0'
}, },
formControl : track : {
{ height : 2
display : 'flex' },
rail : {
height : 2,
opacity : 0.2,
},
mark : {
backgroundColor : '#bfbfbf',
height : 10,
width : 3,
marginTop : -3
},
markActive : {
opacity : 1,
backgroundColor : 'currentColor'
} }
}); })(Slider);
const styles = (theme) => ({
setting :
{
padding : theme.spacing(2)
},
margin :
{
height : theme.spacing(3),
},
formControl :
{
display : 'flex'
}
});
const MediaSettings = ({ const MediaSettings = ({
setEchoCancellation, setEchoCancellation,
setAutoGainControl, setAutoGainControl,
setNoiseSuppression, setNoiseSuppression,
setVoiceActivatedUnmute,
roomClient, roomClient,
me, me,
volume,
settings, settings,
classes classes
}) => }) =>
@ -135,6 +170,32 @@ const MediaSettings = ({
} }
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControl className={classes.formControl}>
<Select
value={settings.resolution || ''}
onChange={(event) => {
if (event.target.value)
roomClient.changeVideoResolution(event.target.value);
}}
name='Video resolution'
autoWidth
className={classes.selectEmpty}
>
{resolutions.map((resolution, index) => {
return (
<MenuItem key={index} value={resolution.value}>
{resolution.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/>
</FormHelperText>
</FormControl>
</form> </form>
<form className={classes.setting} autoComplete='off'> <form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}> <FormControl className={classes.formControl}>
@ -148,7 +209,7 @@ const MediaSettings = ({
displayEmpty displayEmpty
name={intl.formatMessage({ name={intl.formatMessage({
id : 'settings.audio', id : 'settings.audio',
defaultMessage : 'Audio device' defaultMessage : 'Audio input device'
})} })}
autoWidth autoWidth
className={classes.selectEmpty} className={classes.selectEmpty}
@ -165,12 +226,12 @@ const MediaSettings = ({
{ audioDevices.length > 0 ? { audioDevices.length > 0 ?
intl.formatMessage({ intl.formatMessage({
id : 'settings.selectAudio', id : 'settings.selectAudio',
defaultMessage : 'Select audio device' defaultMessage : 'Select audio input device'
}) })
: :
intl.formatMessage({ intl.formatMessage({
id : 'settings.cantSelectAudio', id : 'settings.cantSelectAudio',
defaultMessage : 'Unable to select audio device' defaultMessage : 'Unable to select audio input device'
}) })
} }
</FormHelperText> </FormHelperText>
@ -225,34 +286,6 @@ const MediaSettings = ({
</form> </form>
} }
<form className={classes.setting} autoComplete='off'> <form className={classes.setting} autoComplete='off'>
<FormControl className={classes.formControl}>
<Select
value={settings.resolution || ''}
onChange={(event) =>
{
if (event.target.value)
roomClient.changeVideoResolution(event.target.value);
}}
name='Video resolution'
autoWidth
className={classes.selectEmpty}
>
{ resolutions.map((resolution, index) =>
{
return (
<MenuItem key={index} value={resolution.value}>
{resolution.label}
</MenuItem>
);
})}
</Select>
<FormHelperText>
<FormattedMessage
id='settings.resolution'
defaultMessage='Select your video resolution'
/>
</FormHelperText>
</FormControl>
<FormControlLabel <FormControlLabel
className={classes.setting} className={classes.setting}
control={ control={
@ -298,6 +331,41 @@ const MediaSettings = ({
defaultMessage : 'Noise suppression' defaultMessage : 'Noise suppression'
})} })}
/> />
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.voiceActivatedUnmute} onChange={
(event) =>
{
setVoiceActivatedUnmute(event.target.checked);
}}
/>}
label={intl.formatMessage({
id : 'settings.voiceActivatedUnmute',
defaultMessage : 'Voice activated unmute'
})}
/>
<div className={classes.margin} />
<Typography gutterBottom>
{
intl.formatMessage({
id : 'settings.noiseThreshold',
defaultMessage : 'Noise threshold'
})
}
</Typography>
<NoiseSlider className={classnames(classes.slider, classnames.setting)}
key={'noise-threshold-slider'}
min={-100}
value={settings.noiseThreshold}
max={0}
onChange={
(event, value) => {
roomClient._setNoiseThreshold(value);
}}
marks={[{ value: volume, label: 'level' }]} valueLabelDisplay='on'
/>
<div className={classes.margin} />
</form> </form>
</React.Fragment> </React.Fragment>
); );
@ -305,27 +373,31 @@ const MediaSettings = ({
MediaSettings.propTypes = MediaSettings.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
setEchoCancellation : PropTypes.func.isRequired, setEchoCancellation : PropTypes.func.isRequired,
setAutoGainControl : PropTypes.func.isRequired, setAutoGainControl : PropTypes.func.isRequired,
setNoiseSuppression : PropTypes.func.isRequired, setNoiseSuppression : PropTypes.func.isRequired,
me : appPropTypes.Me.isRequired, setVoiceActivatedUnmute : PropTypes.func.isRequired,
settings : PropTypes.object.isRequired, me : appPropTypes.Me.isRequired,
classes : PropTypes.object.isRequired volume : PropTypes.number,
settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
me : state.me, me : state.me,
volume : state.peerVolumes[state.me.id],
settings : state.settings settings : state.settings
}; };
}; };
const mapDispatchToProps = { const mapDispatchToProps = {
setEchoCancellation : settingsActions.setEchoCancellation, setEchoCancellation : settingsActions.setEchoCancellation,
setAutoGainControl : settingsActions.toggleAutoGainControl, setAutoGainControl : settingsActions.toggleAutoGainControl,
setNoiseSuppression : settingsActions.toggleNoiseSuppression setNoiseSuppression : settingsActions.toggleNoiseSuppression,
setVoiceActivatedUnmute : settingsActions.setVoiceActivatedUnmute
}; };
export default withRoomContext(connect( export default withRoomContext(connect(
@ -337,7 +409,8 @@ export default withRoomContext(connect(
{ {
return ( return (
prev.me === next.me && prev.me === next.me &&
prev.settings === next.settings prev.settings === next.settings &&
prev.peerVolumes[prev.me.id] === next[next.me.id]
); );
} }
} }

View File

@ -18,7 +18,8 @@ const initialState =
raisedHand : false, raisedHand : false,
raisedHandInProgress : false, raisedHandInProgress : false,
loggedIn : false, loggedIn : false,
isSpeaking : false isSpeaking : false,
isAutoMuted : true
}; };
const me = (state = initialState, action) => const me = (state = initialState, action) =>
@ -162,6 +163,13 @@ const me = (state = initialState, action) =>
return { ...state, isSpeaking: flag }; return { ...state, isSpeaking: flag };
} }
case 'SET_AUTO_MUTED':
{
const { flag } = action.payload;
return { ...state, isAutoMuted: flag };
}
default: default:
return state; return state;
} }

View File

@ -31,9 +31,10 @@ const peerVolumes = (state = initialState, action) =>
case 'SET_PEER_VOLUME': case 'SET_PEER_VOLUME':
{ {
const { peerId, volume } = action.payload; const { peerId } = action.payload;
const dBs = action.payload.volume < -100 ? -100 : action.payload.volume;
return { ...state, [peerId]: volume }; return { ...state, [peerId]: Math.round(dBs) };
} }
default: default:

View File

@ -1,26 +1,28 @@
const initialState = const initialState =
{ {
displayName : 'Guest', displayName : 'Guest',
selectedWebcam : null, selectedWebcam : null,
selectedAudioDevice : null, selectedAudioDevice : null,
advancedMode : false, advancedMode : false,
sampleRate : 48000, sampleRate : 48000,
channelCount : 1, channelCount : 1,
volume : 1.0, volume : 1.0,
autoGainControl : true, autoGainControl : false,
echoCancellation : true, echoCancellation : true,
noiseSuppression : true, noiseSuppression : true,
sampleSize : 16, voiceActivatedUnmute : false,
noiseThreshold : -50,
sampleSize : 16,
// low, medium, high, veryhigh, ultra // low, medium, high, veryhigh, ultra
resolution : window.config.defaultResolution || 'medium', resolution : window.config.defaultResolution || 'medium',
lastN : 4, lastN : 4,
permanentTopBar : true, permanentTopBar : true,
hiddenControls : false, hiddenControls : false,
showNotifications : true, showNotifications : true,
notificationSounds : true, notificationSounds : true,
buttonControlBar : window.config.buttonControlBar || false, buttonControlBar : window.config.buttonControlBar || false,
drawerOverlayed : window.config.drawerOverlayed || true, drawerOverlayed : window.config.drawerOverlayed || true,
autoMuteThreshold : window.config.autoMuteThreshold || 4, autoMuteThreshold : window.config.autoMuteThreshold || 4,
...window.config.defaultAudio ...window.config.defaultAudio
}; };
@ -99,6 +101,20 @@ const settings = (state = initialState, action) =>
return { ...state, noiseSuppression }; return { ...state, noiseSuppression };
} }
case 'SET_VOICE_ACTIVATED_UNMUTE':
{
const { voiceActivatedUnmute } = action.payload;
return { ...state, voiceActivatedUnmute };
}
case 'SET_NOISE_THRESHOLD':
{
const { noiseThreshold } = action.payload;
return { ...state, noiseThreshold };
}
case 'SET_DEFAULT_AUDIO': case 'SET_DEFAULT_AUDIO':
{ {
const { audio } = action.payload; const { audio } = action.payload;