Merge branch 'feat-audio-settings' into develop

auto_join_3.3
Stefan Otto 2020-05-06 18:30:20 +02:00
commit 57bb55764f
6 changed files with 352 additions and 117 deletions

View File

@ -42,6 +42,17 @@ var config =
{ {
tcp : true 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 defaultLayout : 'democratic', // democratic, filmstrip
lastN : 4, lastN : 4,
mobileLastN : 1, mobileLastN : 1,
@ -49,7 +60,6 @@ var config =
maxLastN : 5, maxLastN : 5,
// If truthy, users can NOT change number of speakers visible // If truthy, users can NOT change number of speakers visible
lockLastN : false, lockLastN : false,
background : 'images/background.jpg',
// Add file and uncomment for adding logo to appbar // Add file and uncomment for adding logo to appbar
// logo : 'images/logo.svg', // logo : 'images/logo.svg',
title : 'Multiparty meeting', title : 'Multiparty meeting',

View File

@ -953,21 +953,59 @@ export default class RoomClient
} }
} }
async getAudioTrack() disconnectLocalHark() {
{ logger.debug('disconnectLocalHark() | Stopping harkStream.');
await navigator.mediaDevices.getUserMedia( if (this._harkStream != null) {
{ this._harkStream.getAudioTracks()[0].stop();
audio : true, video : false this._harkStream = null;
}
if (this._hark != null) {
logger.debug('disconnectLocalHark() Stopping hark.');
this._hark.stop();
}
}
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 getVideoTrack()
{
await navigator.mediaDevices.getUserMedia(
{
audio : false, video : true
});
}
async changeAudioDevice(deviceId) async changeAudioDevice(deviceId)
{ {
@ -987,29 +1025,30 @@ export default class RoomClient
'changeAudioDevice() | new selected webcam [device:%o]', 'changeAudioDevice() | new selected webcam [device:%o]',
device); device);
if (this._hark != null) this.disconnectLocalHark();
this._hark.stop();
if (this._harkStream != null)
{
logger.debug('Stopping hark.');
this._harkStream.getAudioTracks()[0].stop();
this._harkStream = null;
}
if (this._micProducer && this._micProducer.track) if (this._micProducer && this._micProducer.track)
this._micProducer.track.stop(); this._micProducer.track.stop();
logger.debug('changeAudioDevice() | calling getUserMedia()'); logger.debug('changeAudioDevice() | calling getUserMedia() %o', store.getState().settings);
const stream = await navigator.mediaDevices.getUserMedia( const stream = await navigator.mediaDevices.getUserMedia(
{ {
audio : 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]; const track = stream.getAudioTracks()[0];
if (this._micProducer) if (this._micProducer)
@ -1017,47 +1056,8 @@ export default class RoomClient
if (this._micProducer) if (this._micProducer)
this._micProducer.volume = 0; 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) if (this._micProducer && this._micProducer.id)
store.dispatch( store.dispatch(
producerActions.setProducerTrack(this._micProducer.id, track)); producerActions.setProducerTrack(this._micProducer.id, track));
@ -1106,6 +1106,37 @@ export default class RoomClient
meActions.setAudioOutputInProgress(false)); 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) async changeVideoResolution(resolution)
{ {
logger.debug('changeVideoResolution() [resolution: %s]', resolution); logger.debug('changeVideoResolution() [resolution: %s]', resolution);
@ -1795,6 +1826,49 @@ export default class RoomClient
this._recvTransport = null; 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')); store.dispatch(roomActions.setRoomState('connecting'));
}); });
@ -3262,11 +3336,20 @@ export default class RoomClient
const stream = await navigator.mediaDevices.getUserMedia( const stream = await navigator.mediaDevices.getUserMedia(
{ {
audio : { 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]; track = stream.getAudioTracks()[0];
this._micProducer = await this._sendTransport.produce( this._micProducer = await this._sendTransport.produce(
@ -3320,51 +3403,8 @@ export default class RoomClient
this._micProducer.volume = 0; this._micProducer.volume = 0;
if (this._hark != null) this.connectLocalHark(track);
this._hark.stop();
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) catch (error)
{ {

View File

@ -38,6 +38,45 @@ export const togglePermanentTopBar = () =>
type : 'TOGGLE_PERMANENT_TOPBAR' 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 = () => export const toggleHiddenControls = () =>
({ ({
type : 'TOGGLE_HIDDEN_CONTROLS' type : 'TOGGLE_HIDDEN_CONTROLS'

View File

@ -12,6 +12,10 @@ const peersKeySelector = createSelector(
peersSelector, peersSelector,
(peers) => Object.keys(peers) (peers) => Object.keys(peers)
); );
const peersValueSelector = createSelector(
peersSelector,
(peers) => Object.values(peers)
);
export const peersValueSelector = createSelector( export const peersValueSelector = createSelector(
peersSelector, peersSelector,

View File

@ -3,12 +3,15 @@ import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import { withRoomContext } from '../../RoomContext'; import { withRoomContext } from '../../RoomContext';
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 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 Select from '@material-ui/core/Select'; import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
const styles = (theme) => const styles = (theme) =>
({ ({
@ -23,6 +26,9 @@ const styles = (theme) =>
}); });
const MediaSettings = ({ const MediaSettings = ({
setEchoCancellation,
setAutoGainControl,
setNoiseSuppression,
roomClient, roomClient,
me, me,
settings, settings,
@ -247,6 +253,48 @@ const MediaSettings = ({
/> />
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.echoCancellation} onChange={
(event) => {
setEchoCancellation(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.echoCancellation',
defaultMessage : 'Echo Cancellation'
})}
/>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.autoGainControl} onChange={
(event) => {
setAutoGainControl(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.autoGainControl',
defaultMessage : 'Auto Gain Control'
})}
/>
<FormControlLabel
className={classes.setting}
control={
<Checkbox checked={settings.noiseSuppression} onChange={
(event) => {
setNoiseSuppression(event.target.checked);
roomClient.changeAudioDevice(settings.selectedAudioDevice);
}}
/>}
label={intl.formatMessage({
id : 'settings.noiseSuppression',
defaultMessage : 'Noise Suppression'
})}
/>
</form> </form>
</React.Fragment> </React.Fragment>
); );
@ -255,6 +303,9 @@ const MediaSettings = ({
MediaSettings.propTypes = MediaSettings.propTypes =
{ {
roomClient : PropTypes.any.isRequired, roomClient : PropTypes.any.isRequired,
setEchoCancellation : PropTypes.func.isRequired,
setAutoGainControl : PropTypes.func.isRequired,
setNoiseSuppression : PropTypes.func.isRequired,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
settings : PropTypes.object.isRequired, settings : PropTypes.object.isRequired,
classes : PropTypes.object.isRequired classes : PropTypes.object.isRequired
@ -268,9 +319,15 @@ const mapStateToProps = (state) =>
}; };
}; };
const mapDispatchToProps = {
setEchoCancellation : settingsActions.setEchoCancellation,
setAutoGainControl : settingsActions.toggleAutoGainControl,
setNoiseSuppression : settingsActions.toggleNoiseSuppression
};
export default withRoomContext(connect( export default withRoomContext(connect(
mapStateToProps, mapStateToProps,
null, mapDispatchToProps,
null, null,
{ {
areStatesEqual : (next, prev) => areStatesEqual : (next, prev) =>

View File

@ -4,12 +4,20 @@ const initialState =
selectedWebcam : null, selectedWebcam : null,
selectedAudioDevice : null, selectedAudioDevice : null,
advancedMode : false, advancedMode : false,
sampleRate : 48000,
channelCount : 1,
volume : 1.0,
autoGainControl : true,
echoCancellation : true,
noiseSuppression : true,
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,
notificationSounds : true notificationSounds : true,
...window.config.defaultAudio
}; };
const settings = (state = initialState, action) => const settings = (state = initialState, action) =>
@ -45,6 +53,83 @@ const settings = (state = initialState, action) =>
return { ...state, advancedMode }; 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': case 'SET_LAST_N':
{ {
const { lastN } = action.payload; const { lastN } = action.payload;