/** * Audio Session Configuration Helpers (iOS + Android) * * CRITICAL: This must be configured BEFORE connecting to LiveKit room! * Without proper AudioSession setup, microphone won't work on iOS. * On Android, this controls speaker/earpiece routing. */ import { Platform } from 'react-native'; // AudioSession module - use 'any' to avoid complex typing issues with @livekit/react-native // The actual AudioSession from LiveKit has specific enum types that are hard to match statically let audioSessionModule: any = null; /** * Import AudioSession module lazily * This is needed because @livekit/react-native must be imported after registerGlobals() */ async function getAudioSession(): Promise { if (!audioSessionModule) { const livekit = await import('@livekit/react-native'); audioSessionModule = livekit.AudioSession; } return audioSessionModule; } /** * Configure AudioSession for bidirectional voice call (iOS + Android) * * MUST be called BEFORE connecting to LiveKit room! * * iOS Configuration: * - Category: playAndRecord (both speaker and mic) * - Mode: voiceChat (optimized for voice calls) * - Options: Bluetooth, speaker, mix with others * * Android Configuration: * - audioTypeOptions: communication (for voice calls) * - forceHandleAudioRouting: true (to control speaker/earpiece) */ export async function configureAudioForVoiceCall(): Promise { console.log(`[AudioSession] Configuring for voice call on ${Platform.OS}...`); try { const AudioSession = await getAudioSession(); if (!AudioSession) { console.error('[AudioSession] Failed to get AudioSession module'); return; } if (Platform.OS === 'ios') { // iOS-specific configuration - FORCE SPEAKER OUTPUT // Using videoChat mode + defaultSpeakerOutput option for guaranteed speaker console.log('[AudioSession] Configuring iOS for SPEAKER output...'); try { // Primary config: videoChat mode with defaultSpeakerOutput await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: [ 'allowBluetooth', 'mixWithOthers', 'defaultToSpeaker', // KEY: Forces speaker as default output ], audioMode: 'videoChat', // videoChat mode uses speaker by default }); console.log('[AudioSession] iOS videoChat + defaultToSpeaker configured!'); } catch (err) { console.warn('[AudioSession] Primary iOS config failed, trying fallback:', err); // Fallback: just videoChat without defaultToSpeaker option await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], audioMode: 'videoChat', }); } console.log('[AudioSession] Starting iOS audio session...'); await AudioSession.startAudioSession(); // Additionally set default output to speaker (belt and suspenders) try { console.log('[AudioSession] Setting iOS default output to speaker...'); await AudioSession.configureAudio({ ios: { defaultOutput: 'speaker', }, }); console.log('[AudioSession] iOS speaker output set!'); } catch (outputErr) { console.warn('[AudioSession] Could not set speaker output:', outputErr); } } else if (Platform.OS === 'android') { // Android-specific configuration - FORCE SPEAKER OUTPUT // SOLUTION: Use 'inCommunication' for echo cancellation + forceHandleAudioRouting + explicit speaker selection console.log('[AudioSession] Configuring Android audio for SPEAKER with echo cancellation...'); await AudioSession.configureAudio({ android: { // Force speaker as preferred output preferredOutputList: ['speaker'], // CRITICAL: This flag forces audio routing even in communication mode forceHandleAudioRouting: true, audioTypeOptions: { manageAudioFocus: true, // Use 'inCommunication' for echo cancellation (important for voice calls!) audioMode: 'inCommunication', audioFocusMode: 'gain', // Voice call stream type for proper routing audioStreamType: 'voiceCall', audioAttributesUsageType: 'voiceCommunication', audioAttributesContentType: 'speech', }, }, }); console.log('[AudioSession] Starting Android audio session...'); await AudioSession.startAudioSession(); // CRITICAL: Explicitly select speaker AFTER session starts // This overrides the default earpiece routing of inCommunication mode try { console.log('[AudioSession] Explicitly selecting speaker output...'); await AudioSession.selectAudioOutput('speaker'); console.log('[AudioSession] Speaker output explicitly selected!'); } catch (speakerErr) { console.warn('[AudioSession] selectAudioOutput failed, trying showAudioRoutePicker:', speakerErr); // Fallback: try to show audio route picker or use alternative method try { if (AudioSession.showAudioRoutePicker) { await AudioSession.showAudioRoutePicker(); } } catch (pickerErr) { console.warn('[AudioSession] showAudioRoutePicker also failed:', pickerErr); } } console.log('[AudioSession] Android speaker mode with echo cancellation configured!'); } console.log('[AudioSession] Configuration complete!'); } catch (error) { console.error('[AudioSession] Configuration error:', error); throw error; } } /** * Stop AudioSession (iOS + Android) * * Should be called when disconnecting from voice call */ export async function stopAudioSession(): Promise { if (Platform.OS !== 'ios' && Platform.OS !== 'android') { return; } console.log(`[AudioSession] Stopping audio session on ${Platform.OS}...`); try { const AudioSession = await getAudioSession(); if (!AudioSession) { return; } await AudioSession.stopAudioSession(); console.log('[AudioSession] Stopped'); } catch (error) { console.error('[AudioSession] Error stopping:', error); // Don't throw - cleanup errors are not critical } } /** * Reconfigure audio session after remote track arrives (iOS + Android) * * Sometimes the OS needs a kick to properly route audio after remote participant joins */ export async function reconfigureAudioForPlayback(): Promise { if (Platform.OS !== 'ios' && Platform.OS !== 'android') { return; } console.log(`[AudioSession] Reconfiguring for playback (SPEAKER) on ${Platform.OS}...`); try { const AudioSession = await getAudioSession(); if (!AudioSession) { return; } if (Platform.OS === 'ios') { // Reconfigure iOS - force speaker output await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: [ 'allowBluetooth', 'mixWithOthers', 'defaultToSpeaker', // Force speaker ], audioMode: 'videoChat', // videoChat = speaker by default }); // Also set default output to speaker await AudioSession.configureAudio({ ios: { defaultOutput: 'speaker', }, }); console.log('[AudioSession] iOS reconfigured for speaker playback'); } else if (Platform.OS === 'android') { // Reconfigure Android - force speaker while keeping echo cancellation await AudioSession.configureAudio({ android: { preferredOutputList: ['speaker'], forceHandleAudioRouting: true, audioTypeOptions: { manageAudioFocus: true, audioMode: 'inCommunication', // Keep for echo cancellation audioFocusMode: 'gain', audioStreamType: 'voiceCall', audioAttributesUsageType: 'voiceCommunication', audioAttributesContentType: 'speech', }, }, }); // Explicitly select speaker output try { await AudioSession.selectAudioOutput('speaker'); console.log('[AudioSession] Android speaker explicitly selected'); } catch (err) { console.warn('[AudioSession] selectAudioOutput failed in reconfigure:', err); } console.log('[AudioSession] Android reconfigured for speaker playback'); } console.log('[AudioSession] Reconfigured successfully'); } catch (error) { console.error('[AudioSession] Reconfigure error:', error); // Don't throw - this is a best-effort operation } } /** * Switch audio output between speaker and earpiece (iOS + Android) * * @param useSpeaker - true for speaker, false for earpiece */ export async function setAudioOutput(useSpeaker: boolean): Promise { console.log(`[AudioSession] Setting audio output to ${useSpeaker ? 'SPEAKER' : 'EARPIECE'} on ${Platform.OS}...`); try { const AudioSession = await getAudioSession(); if (!AudioSession) { console.error('[AudioSession] Failed to get AudioSession module'); return; } if (Platform.OS === 'ios') { // iOS: Use videoChat mode + defaultToSpeaker for speaker, voiceChat for earpiece await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: useSpeaker ? ['allowBluetooth', 'mixWithOthers', 'defaultToSpeaker'] : ['allowBluetooth', 'mixWithOthers'], audioMode: useSpeaker ? 'videoChat' : 'voiceChat', }); // Also set default output await AudioSession.configureAudio({ ios: { defaultOutput: useSpeaker ? 'speaker' : 'earpiece', }, }); } else if (Platform.OS === 'android') { // Android: Keep inCommunication mode for echo cancellation, use explicit output selection await AudioSession.configureAudio({ android: { preferredOutputList: useSpeaker ? ['speaker'] : ['earpiece'], forceHandleAudioRouting: true, audioTypeOptions: { manageAudioFocus: true, // Always use inCommunication for echo cancellation audioMode: 'inCommunication', audioFocusMode: 'gain', audioStreamType: 'voiceCall', audioAttributesUsageType: 'voiceCommunication', audioAttributesContentType: 'speech', }, }, }); // Explicitly select output device try { await AudioSession.selectAudioOutput(useSpeaker ? 'speaker' : 'earpiece'); } catch (err) { console.warn('[AudioSession] selectAudioOutput failed:', err); } } console.log(`[AudioSession] Audio output set to ${useSpeaker ? 'SPEAKER' : 'EARPIECE'}`); } catch (error) { console.error('[AudioSession] setAudioOutput error:', error); } }