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

View File

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

View File

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

View File

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

View File

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

View File

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