bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/audio/service.js
Paulo Lanzarin f55bd7b114
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
2024-10-24 21:19:21 -04:00

230 lines
7.6 KiB
JavaScript
Executable File

import Auth from '/imports/ui/services/auth';
import AudioManager from '/imports/ui/services/audio-manager';
import logger from '/imports/startup/client/logger';
import Storage from '../../services/storage/session';
import { useReactiveVar } from '@apollo/client';
import {
getAudioConstraints,
doGUM,
} from '/imports/api/audio/client/bridge/service';
import {
toggleMuteMicrophone,
toggleMuteMicrophoneSystem,
} from '/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service';
import apolloContextHolder from '/imports/ui/core/graphql/apolloContextHolder/apolloContextHolder';
import { MEETING_IS_BREAKOUT } from '/imports/ui/components/audio/audio-graphql/audio-controls/queries';
const MUTED_KEY = 'muted';
const recoverMicState = (toggleVoice) => {
const recover = (storageKey) => {
const muted = Storage.getItem(storageKey);
if ((muted === undefined) || (muted === null) || AudioManager.inputDeviceId === 'listen-only') {
return;
}
logger.debug({
logCode: 'audio_recover_mic_state',
}, `Audio recover previous mic state: muted = ${muted}`);
toggleVoice(Auth.userID, muted);
};
apolloContextHolder.getClient().query({
query: MEETING_IS_BREAKOUT,
fetchPolicy: 'cache-first',
}).then((result) => {
const meeting = result?.data?.meeting?.[0];
const meetingId = meeting?.isBreakout && meeting?.breakoutPolicies?.parentId
? meeting.breakoutPolicies.parentId
: Auth.meetingID;
const storageKey = `${MUTED_KEY}_${meetingId}`;
recover(storageKey);
}).catch(() => {
// Fallback
const storageKey = `${MUTED_KEY}_${Auth.meetingID}`;
recover(storageKey);
});
};
const audioEventHandler = (toggleVoice) => (event) => {
if (!event) {
return;
}
switch (event.name) {
case 'started':
if (!event.isListenOnly) recoverMicState(toggleVoice);
break;
default:
break;
}
};
const init = (messages, intl, toggleVoice, speechLocale, voiceConf, username) => {
AudioManager.setAudioMessages(messages, intl);
if (AudioManager.initialized) return Promise.resolve(false);
const meetingId = Auth.meetingID;
const userId = Auth.userID;
const { sessionToken } = Auth;
const voiceBridge = voiceConf;
// FIX ME
const microphoneLockEnforced = false;
const userData = {
meetingId,
userId,
sessionToken,
username,
voiceBridge,
microphoneLockEnforced,
speechLocale,
};
return AudioManager.init(userData, audioEventHandler(toggleVoice));
};
const useIsUsingAudio = () => {
const isConnected = useReactiveVar(AudioManager._isConnected.value);
const isConnecting = useReactiveVar(AudioManager._isConnecting.value);
const isHangingUp = useReactiveVar(AudioManager._isHangingUp.value);
return Boolean(isConnected || isConnecting || isHangingUp);
};
/**
* 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 = 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;
// If the browser doesn't support the Permissions API, we can't check
// microphone permissions - return null (unknown)
if (navigator?.permissions?.query == null) return null;
if (!status) {
({ state: status } = await navigator.permissions.query({ name: 'microphone' }));
}
switch (status) {
case 'denied':
return false;
case 'granted':
return true;
case null:
case 'prompt':
return checkWithGUM();
default:
return null;
}
} catch (error) {
logger.warn({
logCode: 'audio_check_microphone_permission_error',
extraInfo: {
errorName: error.name,
errorMessage: error.message,
},
}, `Error checking microphone permission: ${error.message}`);
return checkWithGUM();
}
};
export default {
init,
exitAudio: () => AudioManager.exitAudio(),
forceExitAudio: () => AudioManager.forceExitAudio(),
transferCall: () => AudioManager.transferCall(),
joinListenOnly: () => AudioManager.joinListenOnly(),
joinMicrophone: (options) => AudioManager.joinMicrophone(options),
joinEchoTest: () => AudioManager.joinEchoTest(),
changeInputDevice: (inputDeviceId) => AudioManager.changeInputDevice(inputDeviceId),
changeInputStream: (newInputStream) => { AudioManager.inputStream = newInputStream; },
liveChangeInputDevice: (inputDeviceId) => AudioManager.liveChangeInputDevice(inputDeviceId),
changeOutputDevice: (
outputDeviceId,
isLive,
) => AudioManager.changeOutputDevice(outputDeviceId, isLive),
updateInputDevices: (devices) => { AudioManager.inputDevices = devices },
updateOutputDevices: (devices) => { AudioManager.outputDevices = devices },
toggleMuteMicrophone,
toggleMuteMicrophoneSystem,
isConnectedToBreakout: () => {
const transferStatus = AudioManager.getBreakoutAudioTransferStatus();
if (transferStatus.status
=== AudioManager.BREAKOUT_AUDIO_TRANSFER_STATES.CONNECTED) return true;
return false;
},
isConnected: () => {
const transferStatus = AudioManager.getBreakoutAudioTransferStatus();
if (!!transferStatus.breakoutMeetingId
&& transferStatus.breakoutMeetingId !== Auth.meetingID) return false;
return AudioManager.isConnected;
},
isUsingAudio: () => AudioManager.isUsingAudio(),
isConnecting: () => AudioManager.isConnecting,
isListenOnly: () => AudioManager.isListenOnly,
inputDeviceId: () => AudioManager.inputDeviceId,
outputDeviceId: () => AudioManager.outputDeviceId,
isEchoTest: () => AudioManager.isEchoTest,
isMuted: () => AudioManager.isMuted,
autoplayBlocked: () => AudioManager.autoplayBlocked,
handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(),
playAlertSound: (url) => AudioManager.playAlertSound(url),
updateAudioConstraints: (constraints) => AudioManager.updateAudioConstraints(constraints),
recoverMicState,
isReconnecting: () => AudioManager.isReconnecting,
setBreakoutAudioTransferStatus: (status) => AudioManager
.setBreakoutAudioTransferStatus(status),
getBreakoutAudioTransferStatus: () => AudioManager
.getBreakoutAudioTransferStatus(),
getStats: () => AudioManager.getStats(),
getAudioConstraints,
doGUM,
supportsTransparentListenOnly: () => AudioManager.supportsTransparentListenOnly(),
hasMicrophonePermission,
notify: (message, error, icon) => { AudioManager.notify(message, error, icon); },
useIsUsingAudio,
};