fix(audio): ensure correct audio device labels in Firefox
Firefox incorretly displays placeholder audio device labels in the audio settings/echo test modal when audio is disconnected. This issue arises due to two quirks: - Firefox does not support the 'microphone' query from the Permissions API, causing a fallback gUM permission check. - Firefox omits device labels from `enumerateDevices` if no streams are active, even if gUM permission is granted. This behavior differs from other browsers and causes our `enumerateDevices` handling to assume that granted permission implies labels are present. This failed since we clear streams before resolving the fallback gUM. We now run an additional `enumerateDevices` call in `AudioSettings` when a selected input device is defined. This ensures `enumerateDevices` is re-run when a new stream is active, adding the correct device labels in Firefox and improving device listings in all browsers. We've also enhanced error handling in the enumeration process and fixed a false positive in `hasMicrophonePermission`.
This commit is contained in:
parent
ddfba135a6
commit
b9f66c2a10
@ -279,6 +279,9 @@ class AudioSettings extends React.Component {
|
||||
// Only generate input streams if they're going to be used with something
|
||||
// In this case, the volume meter or local echo test.
|
||||
if (produceStreams) {
|
||||
this.setState({
|
||||
producingStreams: true,
|
||||
});
|
||||
this.generateInputStream(deviceId).then((stream) => {
|
||||
// Extract the deviceId again from the stream to guarantee consistency
|
||||
// between stream DID vs chosen DID. That's necessary in scenarios where,
|
||||
@ -301,8 +304,13 @@ class AudioSettings extends React.Component {
|
||||
this.setState({
|
||||
inputDeviceId: extractedDeviceId,
|
||||
stream,
|
||||
producingStreams: false,
|
||||
});
|
||||
|
||||
// Update the device list after the stream has been generated.
|
||||
// This is necessary to guarantee the device list is up-to-date, mainly
|
||||
// in Firefox as it omit labels if no active stream is present (even if
|
||||
// gUM permission is flagged as granted).
|
||||
this.updateDeviceList();
|
||||
}).catch((error) => {
|
||||
logger.warn({
|
||||
logCode: 'audiosettings_gum_failed',
|
||||
@ -313,6 +321,13 @@ class AudioSettings extends React.Component {
|
||||
},
|
||||
}, `Audio settings gUM failed: ${error.name}`);
|
||||
handleGUMFailure(error);
|
||||
}).finally(() => {
|
||||
// Component unmounted after gUM resolution -> skip echo rendering
|
||||
if (!this._isMounted) return;
|
||||
|
||||
this.setState({
|
||||
producingStreams: false,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
@ -377,6 +392,15 @@ class AudioSettings extends React.Component {
|
||||
audioInputDevices,
|
||||
audioOutputDevices,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.warn({
|
||||
logCode: 'audiosettings_enumerate_devices_error',
|
||||
extraInfo: {
|
||||
errorName: error.name,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
}, `Audio settings: error enumerating devices - {${error.name}: ${error.message}}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -73,10 +73,45 @@ const useIsUsingAudio = () => {
|
||||
return Boolean(isConnected || isConnecting || isHangingUp || isEchoTest);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user has granted permission to use the microphone.
|
||||
*
|
||||
* @param {Object} options - Options object.
|
||||
* @param {string} options.permissionStatus - The current permission status.
|
||||
* @param {boolean} options.gumOnPrompt - Whether to check microphone permission by attempting to
|
||||
* get a media stream.
|
||||
* @returns {Promise<boolean|null>} - A promise that resolves to a boolean indicating whether the
|
||||
* user has granted permission to use the microphone. If the permission status is unknown, the
|
||||
* promise resolves to null.
|
||||
*/
|
||||
const hasMicrophonePermission = async ({
|
||||
permissionStatus,
|
||||
permissionStatus = null,
|
||||
gumOnPrompt = false,
|
||||
}) => {
|
||||
const checkWithGUM = () => {
|
||||
if (!gumOnPrompt) return Promise.resolve(null);
|
||||
|
||||
return doGUM({ audio: getAudioConstraints() })
|
||||
.then((stream) => {
|
||||
// Close the stream and remove all tracks - this is just a permission check
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
stream.removeTrack(track);
|
||||
});
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === 'NotAllowedError') return false;
|
||||
|
||||
// Give it the benefit of the doubt. It might be a device mismatch
|
||||
// or something else that's not a permissions issue, so let's try
|
||||
// to proceed. Rollbacks that happen downstream might fix the issue,
|
||||
// otherwise we'll land on the Help screen anyways
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
let status = permissionStatus;
|
||||
|
||||
@ -91,36 +126,19 @@ const hasMicrophonePermission = async ({
|
||||
switch (status) {
|
||||
case 'denied':
|
||||
return false;
|
||||
case 'prompt':
|
||||
// Prompt without any subsequent action is considered unknown
|
||||
if (!gumOnPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return doGUM({ audio: getAudioConstraints() }).then((stream) => {
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
stream.removeTrack(track);
|
||||
});
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Give it the benefit of the doubt. It might be a device mismatch
|
||||
// or something else that's not a permissions issue, so let's try
|
||||
// to proceed. Rollbacks that happen downstream might fix the issue,
|
||||
// otherwise we'll land on the Help screen anyways
|
||||
return null;
|
||||
});
|
||||
|
||||
case 'granted':
|
||||
default:
|
||||
return true;
|
||||
|
||||
case null:
|
||||
case 'prompt':
|
||||
return checkWithGUM();
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
logger.warn({
|
||||
logCode: 'audio_check_microphone_permission_error',
|
||||
extraInfo: {
|
||||
errorName: error.name,
|
||||
@ -128,8 +146,7 @@ const hasMicrophonePermission = async ({
|
||||
},
|
||||
}, `Error checking microphone permission: ${error.message}`);
|
||||
|
||||
// Null = could not determine permission status
|
||||
return null;
|
||||
return checkWithGUM();
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user