From 40795950c23b24d7cc5358ec27ff4efed7f23d4a Mon Sep 17 00:00:00 2001 From: gcampes <gabrieldecampes@gmail.com> Date: Wed, 4 Oct 2017 17:49:11 -0300 Subject: [PATCH] updates to the audio manager --- .../api/2.0/audio/client/bridge/sip.js | 91 +++++------- .../audio/audio-modal/component.jsx | 139 +++++++++++++----- .../audio/audio-modal/container.jsx | 19 ++- .../components/audio/audio-modal/styles.scss | 50 ++++++- .../audio/audio-settings/component.jsx | 33 +---- .../components/audio/echo-test/component.jsx | 17 ++- .../ui/components/audio/echo-test/styles.scss | 103 +------------ .../imports/ui/components/button/styles.scss | 7 + .../ui/services/audio-manager/index.js | 76 ++++++---- bigbluebutton-html5/private/locales/en.json | 1 + 10 files changed, 272 insertions(+), 264 deletions(-) diff --git a/bigbluebutton-html5/imports/api/2.0/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/2.0/audio/client/bridge/sip.js index 85910a922b..5c595cdf47 100644 --- a/bigbluebutton-html5/imports/api/2.0/audio/client/bridge/sip.js +++ b/bigbluebutton-html5/imports/api/2.0/audio/client/bridge/sip.js @@ -1,7 +1,5 @@ import BaseAudioBridge from './base'; -const RETRY_INTERVAL = Meteor.settings.public.media.WebRTCHangupRetryInterval; - export default class SIPBridge extends BaseAudioBridge { constructor(userData) { super(); @@ -9,43 +7,35 @@ export default class SIPBridge extends BaseAudioBridge { console.log('userdata', userData); this.isConnected = false; - this.callStates = { - callStarted: 'iceConnectionCompleted', - callEnded: 'bye', - callDisconnected: 'failed', - }; } joinAudio({ isListenOnly, extension, inputStream }, managerCallback) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { extension = extension || this.userData.voiceBridge; const callback = (message) => { managerCallback(message).then(() => resolve()); }; - this.makeCall({ extension, isListenOnly, inputStream }, callback); + return this.doCall({ extension, isListenOnly, inputStream }, callback) + .catch(reason => { + callback({ status: 'failed', error: reason }); + reject(reason); + }); }); } exitAudio() { return new Promise((resolve) => { - const attemptHangup = () => { - if (this.isConnected) { - console.log('Attempting to hangup on WebRTC call'); - window.webrtc_hangup(() => resolve()); - } else { - console.log('RETRYING hangup on WebRTC call in ' + - `${RETRY_INTERVAL} ms`); - setTimeout(attemptHangup, RETRY_INTERVAL); - } - }; - - return attemptHangup(); + this.currentSession.on('bye', () => { + this.hangup = true; + resolve(); + }); + this.currentSession.bye(); }); } - makeCall({ isListenOnly, extension, inputStream }, callback) { + doCall({ isListenOnly, extension, inputStream }, callback) { const { userId, username, @@ -102,8 +92,6 @@ export default class SIPBridge extends BaseAudioBridge { turnServers: turn, }); - console.log(this.userAgent); - this.userAgent.removeAllListeners('connected'); this.userAgent.removeAllListeners('disconnected'); @@ -117,6 +105,7 @@ export default class SIPBridge extends BaseAudioBridge { this.userAgent.stop(); this.userAgent = null; console.log('DISCONNECTED'); + reject('CONNECTION_ERROR'); }) this.userAgent.start(); @@ -150,45 +139,33 @@ export default class SIPBridge extends BaseAudioBridge { setupEventHandlers(currentSession, callback) { return new Promise((resolve) => { console.log('SETUPEVENTHANDLERS'); - currentSession.mediaHandler.on('iceGatheringComplete', function() { - callback({ status: 'iceGatheringComplete' }); - }); - - currentSession.on('connecting', () => { - callback({}) - console.log('connecting'); - }); - - currentSession.on('progress', (response) => { - console.log('progress'); - }); - - currentSession.on('failed', (response, cause) => { - console.log('failed', cause); - }); - - currentSession.on('bye', (request) => { - console.log('bye'); - callback({ status: 'bye' }); - }); - - currentSession.on('accepted', (data) => { - console.log('accepted'); - }); - currentSession.mediaHandler.on('iceConnectionFailed', () => { - console.log('iceConnectionFailed'); - }); - - currentSession.mediaHandler.on('iceConnectionConnected', () => { - console.log('iceConnectionConnected'); - }); + const { + causes, + } = window.SIP.C; + + currentSession.on('terminated', (message, cause) => { + console.log('TERMINATED', message, cause); + if (!message && !cause) { + return callback({ status: 'ended'}) + } else if(cause) { + if (cause === causes.REQUEST_TIMEOUT) { + return callback({ status: 'failed' , error: 'REQUEST_TIMEOUT'}); + } else if (cause === causes.CONNECTION_ERROR) { + return callback({ status: 'failed' , error: 'CONNECTION_ERROR'}); + } + } + return callback({ status: 'failed' , error: 'ERROR', message: cause}); + }) currentSession.mediaHandler.on('iceConnectionCompleted', () => { console.log('iceConnectionCompleted'); - callback({ status: 'iceConnectionCompleted' }); + this.hangup = false; + callback({ status: 'started' }); resolve(); }); + + this.currentSession = currentSession; }) } } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index 7ed27c9329..5d235edac6 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -68,28 +68,17 @@ class AudioModal extends Component { this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this); this.handleGoToAudioSettings = this.handleGoToAudioSettings.bind(this); this.handleGoToEchoTest = this.handleGoToEchoTest.bind(this); - this.handleClose = props.closeModal; + this.closeModal = props.closeModal.bind(this); this.handleJoinMicrophone = props.joinMicrophone; this.handleJoinListenOnly = props.joinListenOnly; this.joinEchoTest = props.joinEchoTest; this.exitAudio = props.exitAudio; + this.leaveEchoTest = props.leaveEchoTest; this.changeInputDevice = props.changeInputDevice; this.contents = { - echoTest: <EchoTest - handleNo={this.handleGoToAudioOptions} - handleYes={this.handleJoinMicrophone}/>, - settings: <AudioSettings - handleBack={this.handleBack} - handleJoin={this.handleJoinMicrophone} - joinEchoTest={this.joinEchoTest} - exitAudio={this.exitAudio} - changeInputDevice={this.changeInputDevice} - isConnecting={isConnecting} - isConnected={isConnected} - isEchoTest={isEchoTest} - inputDeviceId={inputDeviceId} - />, + echoTest: () => this.renderEchoTest(), + settings: () => this.renderAudioSettings(), }; } @@ -100,15 +89,30 @@ class AudioModal extends Component { } handleGoToAudioSettings() { - this.setState({ - content: 'settings', - }); + this.leaveEchoTest().then(() => { + this.setState({ + content: 'settings', + }); + }) } handleGoToEchoTest() { - this.setState({ - content: 'echoTest', - }); + this.joinEchoTest().then(() => { + this.setState({ + content: 'echoTest', + }); + }) + } + + componentWillUnmount() { + const { + isConnected, + isEchoTest, + } = this.props; + + if (isEchoTest) { + this.exitAudio(); + } } renderAudioOptions() { @@ -117,7 +121,7 @@ class AudioModal extends Component { } = this.props; return ( - <div className={styles.content}> + <span> <Button className={styles.audioBtn} label={intl.formatMessage(intlMessages.microphoneLabel)} @@ -134,37 +138,96 @@ class AudioModal extends Component { size={'jumbo'} onClick={this.handleJoinListenOnly} /> - </div> + </span> ); } render() { + const { + intl, + isConnecting, + } = this.props; + + return ( + <ModalBase + overlayClassName={styles.overlay} + className={styles.modal} + onRequestClose={this.closeModal}> + { isConnecting ? null : + <header className={styles.header}> + <h3 className={styles.title}>{intl.formatMessage(intlMessages.audioChoiceLabel)}</h3> + <Button + className={styles.closeBtn} + label={intl.formatMessage(intlMessages.closeLabel)} + icon={'close'} + size={'md'} + hideLabel + onClick={this.closeModal} + /> + </header> + } + <div className={styles.content}> + { this.renderContent() } + </div> + </ModalBase> + ); + } + + renderContent() { + const { + isConnecting, + isEchoTest, + } = this.props; + const { content, } = this.state; + if (isConnecting) { + return ( + <span className={styles.connecting}>Connecting</span> + ) + } + return content ? this.contents[content]() : this.renderAudioOptions(); + } + + renderEchoTest() { const { - intl, isConnecting, } = this.props; return ( - <ModalBase overlayClassName={styles.overlay} className={styles.modal}> - <header className={styles.header}> - <h3 className={styles.title}>{intl.formatMessage(intlMessages.audioChoiceLabel)}</h3> - <Button - className={styles.closeBtn} - label={intl.formatMessage(intlMessages.closeLabel)} - icon={'close'} - size={'md'} - hideLabel - onClick={this.handleClose} - /> - </header> - { content ? this.contents[content] : this.renderAudioOptions() } - </ModalBase> + <EchoTest + isConnecting={isConnecting} + joinEchoTest={this.joinEchoTest} + leaveEchoTest={this.leaveEchoTest} + handleNo={this.handleGoToAudioSettings} + handleYes={this.handleJoinMicrophone}/> ); } + + renderAudioSettings () { + const { + isConnecting, + isConnected, + isEchoTest, + inputDeviceId + } = this.props; + + return ( + <AudioSettings + handleBack={this.handleGoToAudioOptions} + handleRetry={this.handleGoToEchoTest} + joinEchoTest={this.joinEchoTest} + exitAudio={this.exitAudio} + changeInputDevice={this.changeInputDevice} + isConnecting={isConnecting} + isConnected={isConnected} + isEchoTest={isEchoTest} + inputDeviceId={inputDeviceId} + /> + ) + } } export default injectIntl(AudioModal); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx index 190e5693b3..06e9b0e685 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx @@ -28,12 +28,21 @@ export default withModalMounter(createContainer(({ mountModal }) => ({ closeModal: () => mountModal(null), - joinMicrophone: () => Service.joinMicrophone().then(() => mountModal(null)), - // Service.exitAudio().then(() => Service.joinMicrophone()) - // .then(() => mountModal(null)); - // }, + joinMicrophone: () => { + console.log('JOIN MIC FROM CONTAINER'); + Service.exitAudio().then(() => Service.joinMicrophone()) + .then(() => mountModal(null)); + }, joinListenOnly: () => { - Service.joinMicrophone().then(a => mountModal(null)); + Service.joinListenOnly().then(() => mountModal(null)) + .catch(reason => console.error(reason)); + }, + + leaveEchoTest: () => { + if (!Service.isEchoTest()) { + return Promise.resolve(); + } + return Service.exitAudio(); }, changeInputDevice: (inputDeviceId) => Service.changeInputDevice(inputDeviceId), joinEchoTest: () => Service.joinEchoTest(), diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss index 041b138d98..f12aaccbf2 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss @@ -12,7 +12,8 @@ display: flex; justify-content: center; padding: 0; - margin-top: 1.5rem; + margin-top: auto; + margin-bottom: auto; .audioBtn:first-child { margin-right: 3rem; @@ -32,6 +33,7 @@ .modal { @extend .modal; padding: 1.5rem; + min-height: 35vh; } .closeBtn { @@ -56,31 +58,36 @@ } } -Button.audioBtn { +.audioBtn { i{ color: #3c5764; } } // Modifies the audio button icon colour -Button.audioBtn span:first-child { +.audioBtn span:first-child { color: #1b3c4b; background-color: #f1f8ff; box-shadow: none; border: 5px solid #f1f8ff; + font-size: 3.5rem; + + @include mq($small-only) { + font-size: 2.5rem; + } } // When hovering over a button of class audioBtn, change the border colour of first span-child -Button.audioBtn:hover span:first-child, -Button.audioBtn:focus span:first-child { +.audioBtn:hover span:first-child, +.audioBtn:focus span:first-child { border: 5px solid $color-primary; background-color: #f1f8ff; } // Modifies the button label text -Button.audioBtn span:last-child { +.audioBtn span:last-child { color: black; - font-size: 0.8rem; + font-size: 1rem; font-weight: 600; } @@ -104,6 +111,35 @@ Button.audioBtn span:last-child { } } +.connecting { + font-size: 2rem; +} + +.connecting:after { + overflow: hidden; + display: inline-block; + vertical-align: bottom; + -webkit-animation: ellipsis steps(4,end) 900ms infinite; + animation: ellipsis steps(4,end) 900ms infinite; + content: "\2026"; /* ascii code for the ellipsis character */ + width: 0; + margin-right: 1.25em; +} + +@keyframes ellipsis { + to { + width: 1.25em; + margin-right: 0; + } +} + +@-webkit-keyframes ellipsis { + to { + width: 1.25em; + margin-right: 0; + } +} + .audioNote { color: $color-text; display: inline-block; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx index 478cd34f6b..b92e680119 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx @@ -34,9 +34,9 @@ const intlMessages = defineMessages({ id: 'app.audio.audioSettings.microphoneStreamLabel', description: 'Label for stream volume', }, - enterSessionLabel: { - id: 'app.audio.enterSessionLabel', - description: 'enter session button label', + retryLabel: { + id: 'app.audio.audioSettings.retryLabel', + description: 'Retry button label', }, }); @@ -48,8 +48,7 @@ class AudioSettings extends React.Component { this.handleInputChange = this.handleInputChange.bind(this); this.handleOutputChange = this.handleOutputChange.bind(this); this.handleBack = props.handleBack; - this.handleJoin = props.handleJoin; - this.joinEchoTest = props.joinEchoTest; + this.handleRetry = props.handleRetry; this.exitAudio = props.exitAudio; this.changeInputDevice = props.changeInputDevice; @@ -61,20 +60,6 @@ class AudioSettings extends React.Component { }; } - componentDidMount() { - // this.joinEchoTest(); - } - - componentWillUnmount() { - const { - isEchoTest, - } = this.props; - - if (isEchoTest) { - this.exitAudio(); - } - } - chooseAudio() { this.props.changeMenu(this.props.JOIN_AUDIO); } @@ -179,13 +164,9 @@ class AudioSettings extends React.Component { <Button size={'md'} color={'primary'} - onClick={this.handleJoin} - disabled={isConnecting} - > - <span className={isConnecting ? styles.calling : null}> - { isConnecting ? 'Calling echo test' : intl.formatMessage(intlMessages.enterSessionLabel)} - </span> - </Button> + label={intl.formatMessage(intlMessages.retryLabel)} + onClick={this.handleRetry} + /> </div> </div> ); diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx index 521a9f32a1..5da0184955 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx @@ -18,34 +18,39 @@ class EchoTest extends Component { constructor(props) { super(props); - this.handleYes = this.handleYes.bind(this); - this.handleNo = this.handleNo.bind(this); + this.handleYes = props.handleYes.bind(this); + this.handleNo = props.handleNo.bind(this); + this.joinEchoTest = props.joinEchoTest.bind(this); + this.leaveEchoTest = props.leaveEchoTest.bind(this); } render() { const { intl, + isConnecting, } = this.props; return ( - <div className={styles.content}> + <span> <Button - className={styles.audioBtn} + className={styles.button} label={intl.formatMessage(intlMessages.no)} icon={'thumbs_down'} circle + color={'danger'} size={'jumbo'} onClick={this.handleNo} /> <Button - className={styles.audioBtn} + className={styles.button} label={intl.formatMessage(intlMessages.yes)} icon={'thumbs_up'} circle + color={'success'} size={'jumbo'} onClick={this.handleYes} /> - </div> + </span> ); } } diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss index 041b138d98..a81d0b5761 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss @@ -1,111 +1,18 @@ @import "/imports/ui/stylesheets/variables/_all"; @import "/imports/ui/components/modal/simple/styles"; -.header { - margin: 0; - padding: 0; - border: none; - line-height: 2rem; -} - -.content { - display: flex; - justify-content: center; - padding: 0; - margin-top: 1.5rem; - - .audioBtn:first-child { +.button { + &:first-child { margin-right: 3rem; @include mq($small-only) { margin-right: 1rem; } } -} - -// - -.overlay { - @extend .overlay; -} - -.modal { - @extend .modal; - padding: 1.5rem; -} - -.closeBtn { - right: 0; - top: 0; - position: absolute; - background-color: $color-white; - border: none; - padding: .75rem; - - - i { - color: $color-gray-light; - } - - &:focus, - &:hover{ - background-color: $color-white; - i{ - color: $color-primary; - } - } -} - -Button.audioBtn { - i{ - color: #3c5764; - } -} - -// Modifies the audio button icon colour -Button.audioBtn span:first-child { - color: #1b3c4b; - background-color: #f1f8ff; - box-shadow: none; - border: 5px solid #f1f8ff; -} - -// When hovering over a button of class audioBtn, change the border colour of first span-child -Button.audioBtn:hover span:first-child, -Button.audioBtn:focus span:first-child { - border: 5px solid $color-primary; - background-color: #f1f8ff; -} -// Modifies the button label text -Button.audioBtn span:last-child { - color: black; - font-size: 0.8rem; - font-weight: 600; -} - -// Button.audioBtn:first-of-type { -// margin-right: 5%; -// } -// -// Button.audioBtn:last-of-type { -// margin-left: 5%; -// } - -.title { - text-align: center; - font-weight: 400; - font-size: 1.3rem; - white-space: normal; - - @include mq($small-only) { + span:last-child { + color: black; font-size: 1rem; - padding: 0 1rem; + font-weight: 600; } } - -.audioNote { - color: $color-text; - display: inline-block; - font-size: 0.9rem; -} diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss index 31f7eb2152..e5d0bac3f9 100644 --- a/bigbluebutton-html5/imports/ui/components/button/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss @@ -96,6 +96,13 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x; outline-offset: -2px; } } + + &[aria-disabled="true"] { + cursor: not-allowed; + opacity: .65; + box-shadow: none; + pointer-events: none; + } } .label { diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index e9bc500d85..2b65fb14de 100644 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -5,17 +5,26 @@ import SIPBridge from '/imports/api/2.0/audio/client/bridge/sip'; const USE_SIP = Meteor.settings.public.media.useSIPAudio; -const toggleMuteMicrophone = (cb) => { - cb(); +const ERROR_CODES = { + REQUEST_TIMEOUT: { + message: 'Request Timeout', + }, + CONNECTION_ERROR: { + message: 'Connection Error', + }, + ERROR: { + message: 'An Error Occurred', + }, }; -// const collection = new Mongo.Collection(null); +CALL_STATES = { + STARTED: 'started', + ENDED: 'ended', + FAILED: 'failed', +}; class AudioManager { constructor() { - - - navigator.mediaDevices.enumerateDevices().then(kappa => console.log(kappa)) this._inputDevice = { tracker: new Tracker.Dependency, }; @@ -23,7 +32,6 @@ class AudioManager { navigator.mediaDevices .getUserMedia({ audio: true }) .then((stream) => { - console.log('kappa'); const deviceLabel = stream.getAudioTracks()[0].label; navigator.mediaDevices.enumerateDevices().then(devices => { const device = devices.find(device => device.label === deviceLabel); @@ -53,19 +61,13 @@ class AudioManager { set: (value) => { this[privateKey].value = value; this[privateKey].tracker.changed(); - // console.log('set', privateKey, value); - // this.update(privateKey, value); }, get: () => { this[privateKey].tracker.depend(); return this[privateKey].value; - // console.log('get', privateKey, collection.findOne({})[privateKey]); - // return collection.findOne({})[privateKey]; }, }); }); - - // return collection.insert(obj); } joinAudio(options = {}, callbacks = {}) { @@ -83,10 +85,10 @@ class AudioManager { const callOptions = { isListenOnly, extension: isEchoTest ? '9196' : null, - inputStream: this.inputStream, + inputStream: isListenOnly ? this.createListenOnlyStream() : this.inputStream, } - console.log(this.inputStream); + console.log(callOptions.inputStream); console.log(this.inputDeviceId); return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)); @@ -99,7 +101,10 @@ class AudioManager { toggleMuteMicrophone() { console.log('toggleMuteMicrophone', this); - toggleMuteMicrophone(this.onToggleMicrophoneMute.bind(this)); + makeCall('toggleSelfVoice').then((res) => { + console.log(res); + this.onToggleMicrophoneMute(); + }); } callbackToAudioBridge(message) { @@ -123,6 +128,7 @@ class AudioManager { onAudioExit() { this.isConnected = false; + this.isConnecting = false; if (this.isListenOnly) { makeCall('listenOnlyToggle', false); @@ -145,21 +151,29 @@ class AudioManager { // collection.update(query, modifier); // } - callStateCallback({ status }) { - console.log('CALLSTATECALLBACK =====================', status); + callStateCallback(response) { return new Promise((resolve) => { const { - callStarted, - callEnded, - callDisconnected, - } = this.bridge.callStates; + STARTED, + ENDED, + FAILED, + } = CALL_STATES; - if (status === callStarted) { + const { + status, + error, + } = response; + + console.log('CALLSTATECALLBACK =====================', response); + + if (status === STARTED) { this.onAudioJoin(); - resolve(callStarted); - } else if (status === callEnded) { + resolve(STARTED); + } else if (status === ENDED) { + console.log('ENDED'); this.onAudioExit(); - } else if (status === callDisconnected) { + } else if (status === FAILED) { + console.log('FAILED'); this.onAudioExit(); } }) @@ -175,6 +189,15 @@ class AudioManager { return this._userData; } + createListenOnlyStream() { + if (this.listenOnlyAudioContext) { + this.listenOnlyAudioContext.close(); + } + + this.listenOnlyAudioContext = new window.AudioContext; + return this.listenOnlyAudioContext.createMediaStreamDestination().stream; + } + changeInputDevice(value) { if(this._inputDevice.audioContext) { this._inputDevice.audioContext.close().then(() => { @@ -219,7 +242,6 @@ class AudioManager { this._inputDevice.tracker.depend(); return this._inputDevice.id; } - // set outputDeviceId } const audioManager = new AudioManager(); diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index dd777910e3..d8fbe1df5a 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -212,6 +212,7 @@ "app.audio.audioSettings.microphoneSourceLabel": "Microphone source", "app.audio.audioSettings.speakerSourceLabel": "Speaker source", "app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume", + "app.audio.audioSettings.retryLabel": "Retry", "app.audio.listenOnly.backLabel": "Back", "app.audio.listenOnly.closeLabel": "Close", "app.error.kicked": "You have been kicked out of the meeting", -- GitLab