fix(audio): retry gUM without pre-set deviceIds on OverconstrainedError(s)

There are some situations where previously set deviceIds (
local/session storage) may become stale. This causes an unexpected
behavior where audio is temporarily borked until the user clears their
local storage.
This issue has been seen more recently on Safari endpoints when switching
back-and-forth breakout rooms in environments running under iframes.
Also seen randomly on endpoints with virtual input devices.

This centralizes audio gUM calling into a single method that retries the
gUM procedure without pre-set deviceIds only if the initial call fails
due with an OverconstrainedError - hopefully circumventing the issue.
This commit is contained in:
prlanzarin 2022-09-15 14:48:26 -03:00
parent 10c81bf689
commit b3eebbb926
6 changed files with 43 additions and 22 deletions

View File

@ -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;

View File

@ -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,
};

View File

@ -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';
@ -316,9 +317,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()

View File

@ -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);

View File

@ -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() {

View File

@ -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',