Merge pull request #20978 from prlanzarin/u30/fix/audio-ff-perm-api

fix(audio): ensure correct audio device labels in Firefox
This commit is contained in:
Paulo Lanzarin 2024-08-20 12:21:29 -03:00 committed by GitHub
commit a0fb450522
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 29 deletions

View File

@ -280,6 +280,9 @@ class AudioSettings extends React.Component {
// Only generate input streams if they're going to be used with something // Only generate input streams if they're going to be used with something
// In this case, the volume meter or local echo test. // In this case, the volume meter or local echo test.
if (produceStreams) { if (produceStreams) {
this.setState({
producingStreams: true,
});
this.generateInputStream(deviceId).then((stream) => { this.generateInputStream(deviceId).then((stream) => {
// Extract the deviceId again from the stream to guarantee consistency // Extract the deviceId again from the stream to guarantee consistency
// between stream DID vs chosen DID. That's necessary in scenarios where, // between stream DID vs chosen DID. That's necessary in scenarios where,
@ -302,8 +305,13 @@ class AudioSettings extends React.Component {
this.setState({ this.setState({
inputDeviceId: extractedDeviceId, inputDeviceId: extractedDeviceId,
stream, 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) => { }).catch((error) => {
logger.warn({ logger.warn({
logCode: 'audiosettings_gum_failed', logCode: 'audiosettings_gum_failed',
@ -314,6 +322,13 @@ class AudioSettings extends React.Component {
}, },
}, `Audio settings gUM failed: ${error.name}`); }, `Audio settings gUM failed: ${error.name}`);
handleGUMFailure(error); handleGUMFailure(error);
}).finally(() => {
// Component unmounted after gUM resolution -> skip echo rendering
if (!this._isMounted) return;
this.setState({
producingStreams: false,
});
}); });
} else { } else {
this.setState({ this.setState({
@ -378,6 +393,15 @@ class AudioSettings extends React.Component {
audioInputDevices, audioInputDevices,
audioOutputDevices, 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}}`);
}); });
} }

View File

@ -73,10 +73,45 @@ const useIsUsingAudio = () => {
return Boolean(isConnected || isConnecting || isHangingUp || isEchoTest); 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 ({ const hasMicrophonePermission = async ({
permissionStatus, permissionStatus = null,
gumOnPrompt = false, 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 { try {
let status = permissionStatus; let status = permissionStatus;
@ -91,36 +126,19 @@ const hasMicrophonePermission = async ({
switch (status) { switch (status) {
case 'denied': case 'denied':
return false; 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': case 'granted':
default:
return true; return true;
case null:
case 'prompt':
return checkWithGUM();
default:
return null;
} }
} catch (error) { } catch (error) {
logger.error({ logger.warn({
logCode: 'audio_check_microphone_permission_error', logCode: 'audio_check_microphone_permission_error',
extraInfo: { extraInfo: {
errorName: error.name, errorName: error.name,
@ -128,8 +146,7 @@ const hasMicrophonePermission = async ({
}, },
}, `Error checking microphone permission: ${error.message}`); }, `Error checking microphone permission: ${error.message}`);
// Null = could not determine permission status return checkWithGUM();
return null;
} }
}; };