diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
index 1a77804204..742dcf96d2 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
@@ -5,6 +5,7 @@ import logger from '/imports/startup/client/logger';
import Auth from '/imports/ui/services/auth';
import {
getAudioConstraints,
+ doGUM,
} from '/imports/api/audio/client/bridge/service';
const MEDIA = Meteor.settings.public.media;
@@ -94,9 +95,8 @@ export default class BaseAudioBridge {
this.inputStream.getAudioTracks().forEach((track) => track.stop());
}
- newStream = await navigator.mediaDevices.getUserMedia(constraints);
+ newStream = await doGUM(constraints);
await this.setInputStream(newStream);
- this.inputDeviceId = deviceId;
if (backupStream && backupStream.active) {
backupStream.getAudioTracks().forEach((track) => track.stop());
backupStream = null;
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/service.js b/bigbluebutton-html5/imports/api/audio/client/bridge/service.js
index 55879f6816..d2c2ce443d 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/service.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/service.js
@@ -75,7 +75,8 @@ const filterSupportedConstraints = (audioDeviceConstraints) => {
}
};
-const getAudioConstraints = ({ deviceId = '' }) => {
+const getAudioConstraints = (constraintFields = {}) => {
+ const { deviceId = '' } = constraintFields;
const userSettingsConstraints = Settings.application.microphoneConstraints;
const audioDeviceConstraints = userSettingsConstraints
|| AUDIO_MICROPHONE_CONSTRAINTS || {};
@@ -91,6 +92,29 @@ const getAudioConstraints = ({ deviceId = '' }) => {
return matchConstraints;
};
+const doGUM = async (constraints, retryOnFailure = false) => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ return stream;
+ } catch (error) {
+ // This is probably a deviceId mistmatch. Retry with base constraints
+ // without an exact deviceId.
+ if (error.name === 'OverconstrainedError' && retryOnFailure) {
+ logger.warn({
+ logCode: 'audio_overconstrainederror_rollback',
+ extraInfo: {
+ constraints,
+ },
+ }, 'Audio getUserMedia returned OverconstrainedError, rollback');
+
+ return navigator.mediaDevices.getUserMedia({ audio: getAudioConstraints() });
+ }
+
+ // Not OverconstrainedError - bubble up the error.
+ throw error;
+ }
+};
+
export {
DEFAULT_INPUT_DEVICE_ID,
DEFAULT_OUTPUT_DEVICE_ID,
@@ -106,4 +130,5 @@ export {
storeAudioInputDeviceId,
getStoredAudioOutputDeviceId,
storeAudioOutputDeviceId,
+ doGUM,
};
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js
index 44fd7a4524..f23c3ada89 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sfu-audio-bridge.js
@@ -13,6 +13,7 @@ import {
getAudioSessionNumber,
getAudioConstraints,
filterSupportedConstraints,
+ doGUM,
} from '/imports/api/audio/client/bridge/service';
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
@@ -104,6 +105,11 @@ export default class SFUAudioBridge extends BaseAudioBridge {
return null;
}
+ // eslint-disable-next-line class-methods-use-this
+ mediaStreamFactory(constraints) {
+ return doGUM(constraints, true);
+ }
+
handleTermination() {
return this.callback({ status: this.baseCallStates.ended, bridge: this.bridgeName });
}
@@ -258,6 +264,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
signalCandidates: SIGNAL_CANDIDATES,
traceLogs: TRACE_LOGS,
networkPriority: NETWORK_PRIORITY,
+ mediaStreamFactory: this.mediaStreamFactory,
};
this.broker = new AudioBroker(
@@ -316,9 +323,7 @@ export default class SFUAudioBridge extends BaseAudioBridge {
if (IS_CHROME) {
matchConstraints.deviceId = this.inputDeviceId;
- const stream = await navigator.mediaDevices.getUserMedia(
- { audio: matchConstraints },
- );
+ const stream = await doGUM({ audio: matchConstraints });
await this.setInputStream(stream);
} else {
this.inputStream.getAudioTracks()
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 1c8de64d2f..7f3f9fa5a6 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -23,6 +23,7 @@ import {
getAudioSessionNumber,
getAudioConstraints,
filterSupportedConstraints,
+ doGUM,
} from '/imports/api/audio/client/bridge/service';
const MEDIA = Meteor.settings.public.media;
@@ -384,7 +385,8 @@ class SIPSession {
if (!constraints.audio && !constraints.video) {
return Promise.resolve(new MediaStream());
}
- return navigator.mediaDevices.getUserMedia(constraints);
+
+ return doGUM(constraints, true);
}
createUserAgent(iceServers) {
@@ -1117,9 +1119,7 @@ class SIPSession {
if (isChrome) {
matchConstraints.deviceId = this.inputDeviceId;
- const stream = await navigator.mediaDevices.getUserMedia(
- { audio: matchConstraints },
- );
+ const stream = await doGUM({ audio: matchConstraints });
this.currentSession.sessionDescriptionHandler
.setLocalMediaStream(stream);
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 127900b508..db7caeb84b 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
@@ -11,6 +11,7 @@ import LocalEchoContainer from '/imports/ui/components/audio/local-echo/containe
import DeviceSelector from '/imports/ui/components/audio/device-selector/component';
import {
getAudioConstraints,
+ doGUM,
} from '/imports/api/audio/client/bridge/service';
import MediaStreamUtils from '/imports/utils/media-stream-utils';
@@ -245,7 +246,7 @@ class AudioSettings extends React.Component {
audio: getAudioConstraints({ deviceId: inputDeviceId }),
};
- return navigator.mediaDevices.getUserMedia(constraints);
+ return doGUM(constraints, true);
}
renderOutputTest() {
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 6eb3a7aab3..388b8a6b27 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -717,19 +717,15 @@ class AudioManager {
// a new one will be created for the new stream
this.inputStream = null;
return this.bridge.liveChangeInputDevice(deviceId).then((stream) => {
- logger.debug({
- logCode: 'audiomanager_input_live_device_change',
- extraInfo: {
- deviceId: currentDeviceId,
- newDeviceId: deviceId,
- },
- }, `Microphone input device (live) changed: from ${currentDeviceId} to ${deviceId}`);
- this.setSenderTrackEnabled(!this.isMuted);
- this.inputDeviceId = deviceId;
+ this.inputStream = stream;
+ const extractedDeviceId = MediaStreamUtils.extractDeviceIdFromStream(this.inputStream, 'audio');
+ if (extractedDeviceId && extractedDeviceId !== this.inputDeviceId) {
+ this.changeInputDevice(extractedDeviceId);
+ }
// Live input device change - add device ID to session storage so it
// can be re-used on refreshes/other sessions
- storeAudioInputDeviceId(deviceId);
- this.inputStream = stream;
+ storeAudioInputDeviceId(extractedDeviceId);
+ this.setSenderTrackEnabled(!this.isMuted);
}).catch((error) => {
logger.error({
logCode: 'audiomanager_input_live_device_change_failure',
diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/audio-broker.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/audio-broker.js
index 6db89aa778..d4a963e69b 100644
--- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/audio-broker.js
+++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/audio-broker.js
@@ -84,6 +84,7 @@ class AudioBroker extends BaseBroker {
},
trace: this.traceLogs,
networkPriorities: this.networkPriority ? { audio: this.networkPriority } : undefined,
+ mediaStreamFactory: this.mediaStreamFactory,
};
const peerRole = this.role === 'sendrecv' ? this.role : 'recvonly';
diff --git a/bigbluebutton-html5/imports/ui/services/webrtc-base/peer.js b/bigbluebutton-html5/imports/ui/services/webrtc-base/peer.js
index 92726726d2..3eccf968c2 100644
--- a/bigbluebutton-html5/imports/ui/services/webrtc-base/peer.js
+++ b/bigbluebutton-html5/imports/ui/services/webrtc-base/peer.js
@@ -43,7 +43,7 @@ export default class WebRtcPeer extends EventEmitter2 {
this.on('candidategatheringdone', this.oncandidategatheringdone);
}
if (typeof this.options.mediaStreamFactory === 'function') {
- this.mediaStreamFactory = this.options.mediaStreamFactory.bind(this);
+ this._mediaStreamFactory = this.options.mediaStreamFactory.bind(this);
}
}
@@ -145,8 +145,7 @@ export default class WebRtcPeer extends EventEmitter2 {
return Promise.resolve();
}
- this.logger.info('BBB::WebRtcPeer::mediaStreamFactory - running default factory', this.mediaConstraints);
- return navigator.mediaDevices.getUserMedia(this.mediaConstraints).then((stream) => {
+ const handleGUMResolution = (stream) => {
if (stream.getAudioTracks().length > 0) {
this.audioStream = stream;
this.logger.debug('BBB::WebRtcPeer::mediaStreamFactory - generated audio', this.audioStream);
@@ -155,10 +154,22 @@ export default class WebRtcPeer extends EventEmitter2 {
this.videoStream = stream;
this.logger.debug('BBB::WebRtcPeer::mediaStreamFactory - generated video', this.videoStream);
}
- }).catch((error) => {
- this.logger.error('BBB::WebRtcPeer::mediaStreamFactory - gUM failed', error);
- throw error;
- });
+
+ return stream;
+ }
+
+ if (typeof this._mediaStreamFactory === 'function') {
+ return this._mediaStreamFactory(this.mediaConstraints).then(handleGUMResolution);
+ }
+
+ this.logger.info('BBB::WebRtcPeer::mediaStreamFactory - running default factory', this.mediaConstraints);
+
+ return navigator.mediaDevices.getUserMedia(this.mediaConstraints)
+ .then(handleGUMResolution)
+ .catch((error) => {
+ this.logger.error('BBB::WebRtcPeer::mediaStreamFactory - gUM failed', error);
+ throw error;
+ });
}
set peerConnection(pc) {