chore(audio): add I/O device data to audio logs (#21502)

I/O device IDs are logged in some specific logCodes, but they aren't too
useful on their own without the rest of the MediaDeviceInfo object. We
need that extra data (label, group) to be able to better investigate
incorrect device issues and NotFoundError occurrences.

Register full I/O device info whenever the client fetches them and add
those, unfiltered, to the following logCodes:
  - audiomanager_error_getting_device
  - audiomanager_error_device_not_found
  - audiomanager_error_unknown
  - audio_joined
  - audio_ended
  - audio_failure
  - audiomanager_input_live_device_change_failure
  - audiomanager_output_device_change_failure
This commit is contained in:
Paulo Lanzarin 2024-10-24 22:19:21 -03:00 committed by GitHub
parent 63bf4f35bc
commit f55bd7b114
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 105 additions and 26 deletions

View File

@ -79,6 +79,8 @@ interface InputStreamLiveSelectorProps extends InputStreamLiveSelectorContainerP
away: boolean; away: boolean;
permissionStatus: string; permissionStatus: string;
supportsTransparentListenOnly: boolean; supportsTransparentListenOnly: boolean;
updateInputDevices: (devices: InputDeviceInfo[]) => void;
updateOutputDevices: (devices: MediaDeviceInfo[]) => void;
} }
const InputStreamLiveSelector: React.FC<InputStreamLiveSelectorProps> = ({ const InputStreamLiveSelector: React.FC<InputStreamLiveSelectorProps> = ({
@ -100,6 +102,8 @@ const InputStreamLiveSelector: React.FC<InputStreamLiveSelectorProps> = ({
permissionStatus, permissionStatus,
supportsTransparentListenOnly, supportsTransparentListenOnly,
openAudioSettings, openAudioSettings,
updateInputDevices,
updateOutputDevices,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const toggleVoice = useToggleVoice(); const toggleVoice = useToggleVoice();
@ -164,6 +168,9 @@ const InputStreamLiveSelector: React.FC<InputStreamLiveSelectorProps> = ({
const audioOutputDevices = devices.filter((i) => i.kind === AUDIO_OUTPUT); const audioOutputDevices = devices.filter((i) => i.kind === AUDIO_OUTPUT);
setInputDevices(audioInputDevices as InputDeviceInfo[]); setInputDevices(audioInputDevices as InputDeviceInfo[]);
setOutputDevices(audioOutputDevices); setOutputDevices(audioOutputDevices);
// Update audio devices in AudioManager
updateInputDevices(audioInputDevices as InputDeviceInfo[]);
updateOutputDevices(audioOutputDevices);
if (inAudio) updateRemovedDevices(audioInputDevices, audioOutputDevices); if (inAudio) updateRemovedDevices(audioInputDevices, audioOutputDevices);
}) })
@ -308,6 +315,12 @@ const InputStreamLiveSelectorContainer: React.FC<InputStreamLiveSelectorContaine
const permissionStatus = useReactiveVar(AudioManager._permissionStatus.value) as string; const permissionStatus = useReactiveVar(AudioManager._permissionStatus.value) as string;
// @ts-ignore - temporary while hybrid (meteor+GraphQl) // @ts-ignore - temporary while hybrid (meteor+GraphQl)
const supportsTransparentListenOnly = useReactiveVar(AudioManager._transparentListenOnlySupported.value) as boolean; const supportsTransparentListenOnly = useReactiveVar(AudioManager._transparentListenOnlySupported.value) as boolean;
const updateInputDevices = (devices: InputDeviceInfo[] = []) => {
AudioManager.inputDevices = devices;
};
const updateOutputDevices = (devices: MediaDeviceInfo[] = []) => {
AudioManager.outputDevices = devices;
};
return ( return (
<InputStreamLiveSelector <InputStreamLiveSelector
@ -330,6 +343,8 @@ const InputStreamLiveSelectorContainer: React.FC<InputStreamLiveSelectorContaine
openAudioSettings={openAudioSettings} openAudioSettings={openAudioSettings}
permissionStatus={permissionStatus} permissionStatus={permissionStatus}
supportsTransparentListenOnly={supportsTransparentListenOnly} supportsTransparentListenOnly={supportsTransparentListenOnly}
updateInputDevices={updateInputDevices}
updateOutputDevices={updateOutputDevices}
/> />
); );
}; };

View File

@ -38,6 +38,8 @@ const propTypes = {
leaveEchoTest: PropTypes.func.isRequired, leaveEchoTest: PropTypes.func.isRequired,
changeInputDevice: PropTypes.func.isRequired, changeInputDevice: PropTypes.func.isRequired,
changeOutputDevice: PropTypes.func.isRequired, changeOutputDevice: PropTypes.func.isRequired,
updateInputDevices: PropTypes.func.isRequired,
updateOutputDevices: PropTypes.func.isRequired,
isEchoTest: PropTypes.bool.isRequired, isEchoTest: PropTypes.bool.isRequired,
isConnecting: PropTypes.bool.isRequired, isConnecting: PropTypes.bool.isRequired,
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
@ -207,6 +209,8 @@ const AudioModal = ({
unmuteOnExit = false, unmuteOnExit = false,
permissionStatus = null, permissionStatus = null,
isTranscriptionEnabled, isTranscriptionEnabled,
updateInputDevices,
updateOutputDevices,
}) => { }) => {
const [content, setContent] = useState(initialContent); const [content, setContent] = useState(initialContent);
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
@ -528,6 +532,8 @@ const AudioModal = ({
permissionStatus={permissionStatus} permissionStatus={permissionStatus}
isTranscriptionEnabled={isTranscriptionEnabled} isTranscriptionEnabled={isTranscriptionEnabled}
skipAudioOptions={skipAudioOptions} skipAudioOptions={skipAudioOptions}
updateInputDevices={updateInputDevices}
updateOutputDevices={updateOutputDevices}
/> />
); );
}; };

View File

@ -129,6 +129,8 @@ const AudioModalContainer = (props) => {
liveChangeInputDevice={Service.liveChangeInputDevice} liveChangeInputDevice={Service.liveChangeInputDevice}
changeInputStream={Service.changeInputStream} changeInputStream={Service.changeInputStream}
changeOutputDevice={Service.changeOutputDevice} changeOutputDevice={Service.changeOutputDevice}
updateInputDevices={Service.updateInputDevices}
updateOutputDevices={Service.updateOutputDevices}
joinEchoTest={Service.joinEchoTest} joinEchoTest={Service.joinEchoTest}
exitAudio={Service.exitAudio} exitAudio={Service.exitAudio}
localEchoEnabled={LOCAL_ECHO_TEST_ENABLED} localEchoEnabled={LOCAL_ECHO_TEST_ENABLED}

View File

@ -21,6 +21,8 @@ const propTypes = {
changeInputDevice: PropTypes.func.isRequired, changeInputDevice: PropTypes.func.isRequired,
liveChangeInputDevice: PropTypes.func.isRequired, liveChangeInputDevice: PropTypes.func.isRequired,
changeOutputDevice: PropTypes.func.isRequired, changeOutputDevice: PropTypes.func.isRequired,
updateInputDevices: PropTypes.func.isRequired,
updateOutputDevices: PropTypes.func.isRequired,
handleBack: PropTypes.func.isRequired, handleBack: PropTypes.func.isRequired,
handleConfirmation: PropTypes.func.isRequired, handleConfirmation: PropTypes.func.isRequired,
handleGUMFailure: PropTypes.func.isRequired, handleGUMFailure: PropTypes.func.isRequired,
@ -384,11 +386,16 @@ class AudioSettings extends React.Component {
} }
updateDeviceList() { updateDeviceList() {
const { updateInputDevices, updateOutputDevices } = this.props;
return navigator.mediaDevices.enumerateDevices() return navigator.mediaDevices.enumerateDevices()
.then((devices) => { .then((devices) => {
const audioInputDevices = devices.filter((i) => i.kind === 'audioinput'); const audioInputDevices = devices.filter((i) => i.kind === 'audioinput');
const audioOutputDevices = devices.filter((i) => i.kind === 'audiooutput'); const audioOutputDevices = devices.filter((i) => i.kind === 'audiooutput');
// Update audio devices in AudioManager
updateInputDevices(audioInputDevices);
updateOutputDevices(audioOutputDevices);
this.setState({ this.setState({
audioInputDevices, audioInputDevices,
audioOutputDevices, audioOutputDevices,

View File

@ -186,6 +186,8 @@ export default {
outputDeviceId, outputDeviceId,
isLive, isLive,
) => AudioManager.changeOutputDevice(outputDeviceId, isLive), ) => AudioManager.changeOutputDevice(outputDeviceId, isLive),
updateInputDevices: (devices) => { AudioManager.inputDevices = devices },
updateOutputDevices: (devices) => { AudioManager.outputDevices = devices },
toggleMuteMicrophone, toggleMuteMicrophone,
toggleMuteMicrophoneSystem, toggleMuteMicrophoneSystem,
isConnectedToBreakout: () => { isConnectedToBreakout: () => {

View File

@ -88,6 +88,8 @@ class AudioManager {
this._outputDeviceId = { this._outputDeviceId = {
value: makeVar(null), value: makeVar(null),
}; };
this._inputDevices = [];
this._outputDevices = [];
this.BREAKOUT_AUDIO_TRANSFER_STATES = BREAKOUT_AUDIO_TRANSFER_STATES; this.BREAKOUT_AUDIO_TRANSFER_STATES = BREAKOUT_AUDIO_TRANSFER_STATES;
this._voiceActivityObserver = null; this._voiceActivityObserver = null;
@ -185,6 +187,34 @@ class AudioManager {
return this._outputDeviceId.value(); return this._outputDeviceId.value();
} }
set inputDevices(value) {
if (value?.length) {
this._inputDevices = value;
}
}
get inputDevices() {
return this._inputDevices;
}
get inputDevicesJSON() {
return this.inputDevices.map((device) => device.toJSON());
}
set outputDevices(value) {
if (value?.length) {
this._outputDevices = value;
}
}
get outputDevices() {
return this._outputDevices;
}
get outputDevicesJSON() {
return this.outputDevices.map((device) => device.toJSON());
}
shouldBypassGUM() { shouldBypassGUM() {
return this.supportsTransparentListenOnly() && this.inputDeviceId === 'listen-only'; return this.supportsTransparentListenOnly() && this.inputDeviceId === 'listen-only';
} }
@ -432,15 +462,17 @@ class AudioManager {
break; break;
case 'NotFoundError': case 'NotFoundError':
errorPayload.errCode = AudioErrors.MIC_ERROR.DEVICE_NOT_FOUND; errorPayload.errCode = AudioErrors.MIC_ERROR.DEVICE_NOT_FOUND;
// Reset the input device ID so the user can select a new one
this.changeInputDevice(null);
logger.error({ logger.error({
logCode: 'audiomanager_error_device_not_found', logCode: 'audiomanager_error_device_not_found',
extraInfo: { extraInfo: {
errorName: error.name, errorName: error.name,
errorMessage: error.message, errorMessage: error.message,
inputDeviceId: this.inputDeviceId,
inputDevices: this.inputDevicesJSON,
}, },
}, `Error getting microphone - {${error.name}: ${error.message}}`); }, `Error getting microphone - {${error.name}: ${error.message}}`);
// Reset the input device ID so the user can select a new one
this.changeInputDevice(null);
break; break;
default: default:
logger.error({ logger.error({
@ -448,6 +480,11 @@ class AudioManager {
extraInfo: { extraInfo: {
errorName: error.name, errorName: error.name,
errorMessage: error.message, errorMessage: error.message,
errorStack: error?.stack,
inputDeviceId: this.inputDeviceId,
inputDevices: this.inputDevicesJSON,
outputDeviceId: this.outputDeviceId,
outputDevices: this.outputDevicesJSON,
}, },
}, `Error enabling audio - {${name}: ${message}}`); }, `Error enabling audio - {${name}: ${message}}`);
break; break;
@ -607,10 +644,13 @@ class AudioManager {
extraInfo: { extraInfo: {
secondsToActivateAudio, secondsToActivateAudio,
inputDeviceId: this.inputDeviceId, inputDeviceId: this.inputDeviceId,
inputDevices: this.inputDevicesJSON,
outputDeviceId: this.outputDeviceId, outputDeviceId: this.outputDeviceId,
outputDevices: this.outputDevicesJSON,
isListenOnly: this.isListenOnly, isListenOnly: this.isListenOnly,
}, },
}, 'Audio Joined'); }, 'Audio Joined');
if (STATS.enabled) this.monitor(); if (STATS.enabled) this.monitor();
this.audioEventHandler({ this.audioEventHandler({
name: 'started', name: 'started',
@ -674,8 +714,17 @@ class AudioManager {
breakoutMeetingId: '', breakoutMeetingId: '',
status: BREAKOUT_AUDIO_TRANSFER_STATES.DISCONNECTED, status: BREAKOUT_AUDIO_TRANSFER_STATES.DISCONNECTED,
}); });
logger.info({ logCode: 'audio_ended' }, 'Audio ended without issue');
this.onAudioExit(); this.onAudioExit();
logger.info({
logCode: 'audio_ended',
extraInfo: {
inputDeviceId: this.inputDeviceId,
inputDevices: this.inputDevicesJSON,
outputDeviceId: this.outputDeviceId,
outputDevices: this.outputDevicesJSON,
isListenOnly: this.isListenOnly,
},
}, 'Audio ended without issue');
} else if (status === FAILED) { } else if (status === FAILED) {
this.isReconnecting = false; this.isReconnecting = false;
this.setBreakoutAudioTransferStatus({ this.setBreakoutAudioTransferStatus({
@ -693,7 +742,9 @@ class AudioManager {
cause: bridgeError, cause: bridgeError,
bridge, bridge,
inputDeviceId: this.inputDeviceId, inputDeviceId: this.inputDeviceId,
inputDevices: this.inputDevicesJSON,
outputDeviceId: this.outputDeviceId, outputDeviceId: this.outputDeviceId,
outputDevices: this.outputDevicesJSON,
isListenOnly: this.isListenOnly, isListenOnly: this.isListenOnly,
}, },
}, },
@ -768,18 +819,16 @@ class AudioManager {
this.setSenderTrackEnabled(!this.isMuted); this.setSenderTrackEnabled(!this.isMuted);
}) })
.catch((error) => { .catch((error) => {
logger.error( logger.error({
{
logCode: 'audiomanager_input_live_device_change_failure', logCode: 'audiomanager_input_live_device_change_failure',
extraInfo: { extraInfo: {
errorName: error.name, errorName: error.name,
errorMessage: error.message, errorMessage: error.message,
deviceId: currentDeviceId, deviceId: currentDeviceId,
newDeviceId: deviceId, newDeviceId: deviceId,
inputDevices: this.inputDevicesJSON,
}, },
}, }, `Input device live change failed - {${error.name}: ${error.message}}`);
`Input device live change failed - {${error.name}: ${error.message}}`
);
throw error; throw error;
}); });
@ -820,18 +869,16 @@ class AudioManager {
return this.outputDeviceId; return this.outputDeviceId;
} catch (error) { } catch (error) {
logger.error( logger.error({
{
logCode: 'audiomanager_output_device_change_failure', logCode: 'audiomanager_output_device_change_failure',
extraInfo: { extraInfo: {
errorName: error.name, errorName: error.name,
errorMessage: error.message, errorMessage: error.message,
deviceId: currentDeviceId, deviceId: currentDeviceId,
newDeviceId: targetDeviceId, newDeviceId: targetDeviceId,
}, outputDevices: this.outputDevicesJSON,
}, }
`Error changing output device - {${error.name}: ${error.message}}` }, `Error changing output device - {${error.name}: ${error.message}}`);
);
// Rollback/enforce current sinkId (if possible) // Rollback/enforce current sinkId (if possible)
if (sinkIdSupported) { if (sinkIdSupported) {