// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import type { AudioInputDevice, DeviceChangeObserver, } from 'amazon-chime-sdk-js'; import React, { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { AudioInputContextType, DeviceLabels } from '../../types'; import { useAudioVideo } from '../AudioVideoProvider'; import { useLogger } from '../LoggerProvider'; import { useMeetingManager } from '../MeetingProvider'; interface Props { onDeviceReplacement?: ( nextDevice: string, currentDevice: AudioInputDevice | undefined ) => Promise; } const Context = createContext(null); export const AudioInputProvider: React.FC> = ({ children, onDeviceReplacement, }) => { const logger = useLogger(); const meetingManager = useMeetingManager(); const audioVideo = useAudioVideo(); const [audioInputs, setAudioInputs] = useState([]); const [selectedAudioInputDevice, setSelectedAudioInputDevice] = useState( meetingManager.selectedAudioInputDevice ); const selectedInputRef = useRef(selectedAudioInputDevice); selectedInputRef.current = selectedAudioInputDevice; const replaceDevice = async (device: string): Promise => { if (onDeviceReplacement) { return onDeviceReplacement( device, meetingManager.selectedAudioInputDevice ); } return device; }; useEffect(() => { meetingManager.subscribeToSelectedAudioInputDevice( setSelectedAudioInputDevice ); return (): void => { meetingManager.unsubscribeFromSelectedAudioInputDevice( setSelectedAudioInputDevice ); }; }, []); useEffect(() => { let isMounted = true; const observer: DeviceChangeObserver = { audioInputsChanged: async (newAudioInputs: MediaDeviceInfo[]) => { logger.info('AudioInputProvider - audio inputs updated'); if ( meetingManager.getDeviceLabels() !== DeviceLabels.Audio && meetingManager.getDeviceLabels() !== DeviceLabels.AudioAndVideo ) { logger.info( 'Device labels do not allow audio, skipping audio input selection on audioInputsChanged' ); return; } const hasSelectedDevice = newAudioInputs.some( (device) => device.deviceId === selectedInputRef.current ); let nextInput: string = 'default'; if ( selectedInputRef.current && !hasSelectedDevice && newAudioInputs.length ) { logger.info( 'Previously selected audio input lost. Selecting a default device.' ); nextInput = newAudioInputs[0].deviceId; // Safari and Firefox don't have this "default" as device Id // Only Chrome have this "default" device } else if (selectedInputRef.current === 'default') { logger.info( `Audio devices updated and "default" device is selected. Reselecting input.` ); } const nextDevice = await replaceDevice(nextInput); try { await meetingManager.startAudioInputDevice(nextDevice); } catch (e) { logger.error( `Failed to select audio input device on audioInputsChanged: ${e}` ); } setAudioInputs(newAudioInputs); }, }; async function initAudioInput(): Promise { if (!audioVideo) { return; } const devices = await audioVideo.listAudioInputDevices(); if (isMounted) { setAudioInputs(devices); audioVideo.addDeviceChangeObserver(observer); } } const callback = (): void => { initAudioInput(); }; meetingManager.subscribeToDeviceLabelTrigger(callback); initAudioInput(); return () => { isMounted = false; audioVideo?.removeDeviceChangeObserver(observer); meetingManager.unsubscribeFromDeviceLabelTrigger(callback); }; }, [audioVideo, onDeviceReplacement]); const contextValue: AudioInputContextType = useMemo( () => ({ devices: audioInputs, selectedDevice: selectedAudioInputDevice, }), [audioInputs, selectedAudioInputDevice] ); return {children}; }; export const useAudioInputs = (): AudioInputContextType => { const context = useContext(Context); if (!context) { throw new Error('useAudioInputs must be used within AudioInputProvider'); } return context; };