diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js index cf63161f77ebcd6ec85590a6cfe4aeaf31cc1028..aef9da20807e2cb1d8f1b4bdd869aab729a72323 100755 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js @@ -17,6 +17,7 @@ import { Tracker } from 'meteor/tracker'; import VoiceCallStates from '/imports/api/voice-call-states'; import CallStateOptions from '/imports/api/voice-call-states/utils/callStates'; import Auth from '/imports/ui/services/auth'; +import Settings from '/imports/ui/services/settings'; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; @@ -36,7 +37,8 @@ const BRIDGE_NAME = 'sip'; const WEBSOCKET_KEEP_ALIVE_INTERVAL = MEDIA.websocketKeepAliveInterval || 0; const WEBSOCKET_KEEP_ALIVE_DEBOUNCE = MEDIA.websocketKeepAliveDebounce || 10; const TRACE_SIP = MEDIA.traceSip || false; -const AUDIO_MICROPHONE_CONSTRAINTS = MEDIA.audioMicrophoneConstraints; +const AUDIO_MICROPHONE_CONSTRAINTS = Meteor.settings.public.app.defaultSettings + .audio.microphoneConstraints; const getAudioSessionNumber = () => { let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10); @@ -580,19 +582,12 @@ class SIPSession { const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`); - const supportedConstraints = navigator - .mediaDevices.getSupportedConstraints() || []; - - const audioDeviceConstraints = AUDIO_MICROPHONE_CONSTRAINTS || {}; - - const matchConstraints = {}; + const userSettingsConstraints = Settings.audio.microphoneConstraints; + const audioDeviceConstraints = userSettingsConstraints + || AUDIO_MICROPHONE_CONSTRAINTS || {}; - Object.entries(audioDeviceConstraints).forEach( - ([constraintName, constraintValue]) => { - if (supportedConstraints[constraintName]) { - matchConstraints[constraintName] = constraintValue; - } - } + const matchConstraints = this.filterSupportedConstraints( + audioDeviceConstraints, ); if (this.inputDeviceId) { @@ -949,6 +944,86 @@ class SIPSession { resolve(); }); } + + /** + * Filter constraints set in audioDeviceConstraints, based on + * constants supported by browser. This avoids setting a constraint + * unsupported by browser. In currently safari version (13+), for example, + * setting an unsupported constraint crashes the audio. + * @param {Object} audioDeviceConstraints Constraints to be set + * see: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints + * @return {Object} A new Object of the same type as + * input, containing only the supported constraints. + */ + filterSupportedConstraints(audioDeviceConstraints) { + try { + const matchConstraints = {}; + const supportedConstraints = navigator + .mediaDevices.getSupportedConstraints() || {}; + Object.entries(audioDeviceConstraints).forEach( + ([constraintName, constraintValue]) => { + if (supportedConstraints[constraintName]) { + matchConstraints[constraintName] = constraintValue; + } + } + ); + + return matchConstraints; + } catch (error) { + logger.error({ + logCode: 'sipjs_unsupported_audio_constraint_error', + extraInfo: { + callerIdName: this.user.callerIdName, + }, + }, 'SIP.js unsupported constraint error'); + return {}; + } + } + + /** + * Update audio constraints for current local MediaStream (microphone) + * @param {Object} constraints MediaTrackConstraints object. See: + * https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints + * @return {Promise} A Promise for this process + */ + async updateAudioConstraints(constraints) { + try { + logger.info({ + logCode: 'sipjs_update_audio_constraint', + extraInfo: { + callerIdName: this.user.callerIdName, + }, + }, 'SIP.js updating audio constraint'); + + const matchConstraints = this.filterSupportedConstraints(constraints); + + //Chromium bug - see: https://bugs.chromium.org/p/chromium/issues/detail?id=796964&q=applyConstraints&can=2 + if (browser().name === 'chrome') { + matchConstraints.deviceId = this.inputDeviceId; + + const stream = await navigator.mediaDevices.getUserMedia( + { audio: matchConstraints }, + ); + + this.currentSession.sessionDescriptionHandler + .setLocalMediaStream(stream); + } else { + const { localMediaStream } = this.currentSession + .sessionDescriptionHandler; + + localMediaStream.getAudioTracks().forEach( + track => track.applyConstraints(matchConstraints), + ); + } + } catch (error) { + logger.error({ + logCode: 'sipjs_audio_constraint_error', + extraInfo: { + callerIdName: this.user.callerIdName, + }, + }, 'SIP.js failed to update audio constraint'); + } + } } export default class SIPBridge extends BaseAudioBridge { @@ -1099,4 +1174,8 @@ export default class SIPBridge extends BaseAudioBridge { return this.media.outputDeviceId || value; } + + async updateAudioConstraints(constraints) { + return this.activeSession.updateAudioConstraints(constraints); + } } diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx index 959864f442110075918d767f37a329ffe28f2ecf..ffa5d984cd38ab1fa8d7e0dd68f643d0788698b3 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx @@ -11,6 +11,7 @@ import VideoPreviewContainer from '/imports/ui/components/video-preview/containe import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; import Service from './service'; import AudioModalContainer from './audio-modal/container'; +import Settings from '/imports/ui/services/settings'; const APP_CONFIG = Meteor.settings.public.app; const KURENTO_CONFIG = Meteor.settings.public.kurento; @@ -105,6 +106,7 @@ const messages = { }; export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ mountModal, intl, userLocks }) => { + const { microphoneConstraints } = Settings.application; const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin); const { userWebcam, userMic } = userLocks; const openAudioModal = () => new Promise((resolve) => { @@ -115,12 +117,14 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m if (userWebcam) return resolve(); mountModal(<VideoPreviewContainer resolve={resolve} />); }); - if (userMic - && Service.isConnected() - && !Service.isListenOnly() - && !Service.isMuted()) { - Service.toggleMuteMicrophone(); - notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'audio_on'); + + if (Service.isConnected() && !Service.isListenOnly()) { + Service.updateAudioConstraints(microphoneConstraints); + + if (userMic && !Service.isMuted()) { + Service.toggleMuteMicrophone(); + notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'audio_on'); + } } Breakouts.find().observeChanges({ diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js index a20d22d8d47ef9b3ba726941c8d9c54fc979e386..ae580be0310c36131a897a64fb80337945f98751 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/service.js @@ -88,4 +88,6 @@ export default { autoplayBlocked: () => AudioManager.autoplayBlocked, handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(), playAlertSound: url => AudioManager.playAlertSound(url), + updateAudioConstraints: + constraints => AudioManager.updateAudioConstraints(constraints), }; diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx index 0db1410ba685da859567a8e2ac100b9db42c57fe..2c13176b268924d6a61ef344b05333b154857a73 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx @@ -73,6 +73,7 @@ const propTypes = { fallbackLocale: PropTypes.string, fontSize: PropTypes.string, locale: PropTypes.string, + microphoneConstraints: PropTypes.objectOf(Object), }).isRequired, updateSettings: PropTypes.func.isRequired, availableLocales: PropTypes.objectOf(PropTypes.array).isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx index a74469d2e8d4fbf9436095b89243b21778ac82d4..1b8fc662a145e9b8dcae47c05eb2d843ce9510fe 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx @@ -22,6 +22,10 @@ const intlMessages = defineMessages({ id: 'app.submenu.application.audioAlertLabel', description: 'audio notification label', }, + audioFilterLabel: { + id: 'app.submenu.application.audioFilterLabel', + description: 'audio filters label', + }, pushAlertLabel: { id: 'app.submenu.application.pushAlertLabel', description: 'push notifiation label', @@ -92,6 +96,8 @@ class ApplicationMenu extends BaseMenu { '18px', '20px', ], + audioFilterEnabled: ApplicationMenu.isAudioFilterEnabled(props + .settings.microphoneConstraints), }; } @@ -116,6 +122,49 @@ class ApplicationMenu extends BaseMenu { }); } + static isAudioFilterEnabled(_constraints) { + if (typeof _constraints === 'undefined') return true; + + const _isConstraintEnabled = (constraintValue) => { + switch (typeof constraintValue) { + case 'boolean': + return constraintValue; + case 'string': + return constraintValue === 'true'; + case 'object': + return !!(constraintValue.exact || constraintValue.ideal); + default: + return false; + } + }; + + let isAnyFilterEnabled = true; + + const constraints = _constraints && (typeof _constraints.advanced === 'object') + ? _constraints.advanced + : _constraints || {}; + + isAnyFilterEnabled = Object.values(constraints).find( + constraintValue => _isConstraintEnabled(constraintValue), + ); + + return isAnyFilterEnabled; + } + + handleAudioFilterChange() { + const _audioFilterEnabled = !ApplicationMenu.isAudioFilterEnabled(this + .state.settings.microphoneConstraints); + const _newConstraints = { + autoGainControl: _audioFilterEnabled, + echoCancellation: _audioFilterEnabled, + noiseSuppression: _audioFilterEnabled, + }; + + const obj = this.state; + obj.settings.microphoneConstraints = _newConstraints; + this.handleUpdateSettings(this.state.settings, obj.settings); + } + handleUpdateFontSize(size) { const obj = this.state; obj.settings.fontSize = size; @@ -321,6 +370,27 @@ class ApplicationMenu extends BaseMenu { </span> </div> </div> + + <div className={styles.row}> + <div className={styles.col} aria-hidden="true"> + <div className={styles.formElement}> + <label className={styles.label}> + {intl.formatMessage(intlMessages.audioFilterLabel)} + </label> + </div> + </div> + <div className={styles.col}> + <div className={cx(styles.formElement, styles.pullContentRight)}> + <Toggle + icons={false} + defaultChecked={this.state.audioFilterEnabled} + onChange={() => this.handleAudioFilterChange()} + ariaLabel={intl.formatMessage(intlMessages.audioFilterLabel)} + /> + </div> + </div> + </div> + <hr className={styles.separator} /> <div className={styles.row}> <div className={styles.col}> diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 6dd79666f1098abf52db8ed4b2d23081bb935996..644ed6ee7f7b0f2f0e56b61456d3cd5fb8fdf44a 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -634,6 +634,10 @@ class AudioManager { return audioAlert.play(); } + + async updateAudioConstraints(constraints) { + await this.bridge.updateAudioConstraints(constraints); + } } const audioManager = new AudioManager(); diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index fba81910ea86a790db1c4a783e5026a3669cadff..7f338a114e425fd15b03ccf87f76e1480295cf87 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -47,6 +47,25 @@ public: userJoinPushAlerts: false fallbackLocale: en overrideLocale: null + #Audio constraints for microphone. Use this to control browser's + #filters, such as AGC (Auto Gain Control) , Echo Cancellation, + #Noise Supression, etc. + #For more deails, see: + # https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints + #Currently, google chrome sets {ideal: true} for autoGainControl, + #echoCancellation and noiseSuppression, if not set. + #The accepted value for each constraint is an object of type + #https://developer.mozilla.org/en-US/docs/Web/API/ConstrainBoolean + #These values are used as initial constraints for every new participant, + #and can be changed by user in: Settings > Application > Microphone + #Audio Filters. + # microphoneConstraints: + # autoGainControl: + # ideal: true + # echoCancellation: + # ideal: true + # noiseSuppression: + # ideal: true audio: inputDeviceId: undefined outputDeviceId: undefined @@ -306,20 +325,6 @@ public: websocketKeepAliveDebounce: 10 #Trace sip/audio messages in browser. If not set, default value is false. traceSip: false - #Audio constraints for microphone. Use this to control browser's filters, - #such as AGC (Auto Gain Control) , Echo Cancellation, Noise Supression, etc. - #See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings#Properties_of_audio_tracks - #for more details. Currently, google chrome sets {ideal: true} - #for autoGainControl, echoCancellation and noiseSuppression, if not set. - #The accepted value for each constraint is an object of type - #https://developer.mozilla.org/en-US/docs/Web/API/ConstrainBoolean - # audioMicrophoneConstraints: - # autoGainControl: - # ideal: true - # echoCancellation: - # ideal: true - # noiseSuppression: - # ideal: true presentation: defaultPresentationFile: default.pdf panZoomThrottle: 32 diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index fc9cc93afcece186292cde545664a1fe522f6263..b0f7d3bfc571576388793be6edbfa4c7d2cc11c5 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -294,6 +294,7 @@ "app.submenu.application.applicationSectionTitle": "Application", "app.submenu.application.animationsLabel": "Animations", "app.submenu.application.audioAlertLabel": "Audio Alerts for Chat", + "app.submenu.application.audioFilterLabel": "Audio Filters for Microphone", "app.submenu.application.pushAlertLabel": "Popup Alerts for Chat", "app.submenu.application.userJoinAudioAlertLabel": "Audio Alerts for User Join", "app.submenu.application.userJoinPushAlertLabel": "Popup Alerts for User Join", diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json index 94709e4da4980f89a6b88672d2cdc31f3a5c3641..e5ce651fc3d9cc595d29683636192180734dfca6 100644 --- a/bigbluebutton-html5/private/locales/pt_BR.json +++ b/bigbluebutton-html5/private/locales/pt_BR.json @@ -289,6 +289,7 @@ "app.submenu.application.applicationSectionTitle": "Aplicação", "app.submenu.application.animationsLabel": "Animações", "app.submenu.application.audioAlertLabel": "Alertas de áudio para bate-papo", + "app.submenu.application.audioFilterLabel": "Filtros de áudio para o microfone", "app.submenu.application.pushAlertLabel": "Alertas de pop-up para bate-papo", "app.submenu.application.userJoinAudioAlertLabel": "Alertas de áudio quando novos participantes entram na sala", "app.submenu.application.userJoinPushAlertLabel": "Alertas de pop-up quando novos participantes entram na sala", @@ -674,4 +675,3 @@ "app.legacy.criosBrowser": "No iOS, use o Safari para obter suporte total." } -