/** * 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 with fallback strategies // Try multiple configurations in order of preference const configs = [ // Strategy 1: videoChat mode (speaker by default, no problematic options) { name: 'videoChat', config: { audioCategory: 'playAndRecord', audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], audioMode: 'videoChat', }, }, // Strategy 2: voiceChat mode (more compatible, but earpiece by default) { name: 'voiceChat', config: { audioCategory: 'playAndRecord', audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], audioMode: 'voiceChat', }, }, // Strategy 3: Minimal config (most compatible) { name: 'minimal', config: { audioCategory: 'playAndRecord', audioCategoryOptions: [], audioMode: 'default', }, }, ]; let configSuccess = false; let lastError: any = null; for (const { name, config } of configs) { try { console.log(`[AudioSession] Trying ${name} configuration...`); await AudioSession.setAppleAudioConfiguration(config); console.log(`[AudioSession] ${name} configuration succeeded!`); configSuccess = true; break; } catch (err) { console.warn(`[AudioSession] ${name} config failed:`, err); lastError = err; // Continue to next strategy } } if (!configSuccess) { console.error('[AudioSession] All iOS configurations failed!'); throw lastError || new Error('All audio configurations failed'); } console.log('[AudioSession] Starting audio session...'); await AudioSession.startAudioSession(); // Try to set speaker output (non-critical, don't throw on failure) try { console.log('[AudioSession] Setting default output to speaker...'); await AudioSession.configureAudio({ ios: { defaultOutput: 'speaker', }, }); } catch (outputErr) { console.warn('[AudioSession] Could not set speaker output:', outputErr); // Continue anyway - audio will work, just maybe on earpiece } } else if (Platform.OS === 'android') { // Android-specific configuration // IMPORTANT: Using 'music' stream type to force output to speaker // 'voiceCall' stream type defaults to earpiece on many Android devices console.log('[AudioSession] Configuring Android audio for SPEAKER...'); await AudioSession.configureAudio({ android: { // Use MEDIA mode to ensure speaker output audioTypeOptions: { manageAudioFocus: true, audioMode: 'normal', audioFocusMode: 'gain', // Use 'music' stream - goes to speaker by default audioStreamType: 'music', audioAttributesUsageType: 'media', audioAttributesContentType: 'music', }, // Force speaker as output preferredOutputList: ['speaker'], // Allow us to control audio routing forceHandleAudioRouting: true, }, }); console.log('[AudioSession] Starting Android audio session...'); await AudioSession.startAudioSession(); } 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 on ${Platform.OS}...`); try { const AudioSession = await getAudioSession(); if (!AudioSession) { return; } if (Platform.OS === 'ios') { // Reconfigure with same safe settings - this "refreshes" the audio routing await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: [ 'allowBluetooth', 'mixWithOthers', ], // Use 'videoChat' - defaults to speaker audioMode: 'videoChat', }); } else if (Platform.OS === 'android') { // Reconfigure Android audio to ensure speaker output // Using 'music' stream type to force speaker await AudioSession.configureAudio({ android: { audioTypeOptions: { manageAudioFocus: true, audioMode: 'normal', audioFocusMode: 'gain', audioStreamType: 'music', audioAttributesUsageType: 'media', audioAttributesContentType: 'music', }, preferredOutputList: ['speaker'], forceHandleAudioRouting: true, }, }); } 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: Update configuration based on desired output // Use 'videoChat' mode for speaker, 'voiceChat' for earpiece await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], audioMode: useSpeaker ? 'videoChat' : 'voiceChat', }); // Also set default output await AudioSession.configureAudio({ ios: { defaultOutput: useSpeaker ? 'speaker' : 'earpiece', }, }); } else if (Platform.OS === 'android') { // Android: Switch stream type to control speaker/earpiece // - 'music' stream goes to speaker by default // - 'voiceCall' stream goes to earpiece by default await AudioSession.configureAudio({ android: { audioTypeOptions: { manageAudioFocus: true, audioMode: useSpeaker ? 'normal' : 'inCommunication', audioFocusMode: 'gain', // Key difference: music→speaker, voiceCall→earpiece audioStreamType: useSpeaker ? 'music' : 'voiceCall', audioAttributesUsageType: useSpeaker ? 'media' : 'voiceCommunication', audioAttributesContentType: useSpeaker ? 'music' : 'speech', }, // Also set preferred output list preferredOutputList: useSpeaker ? ['speaker'] : ['earpiece'], forceHandleAudioRouting: true, }, }); } console.log(`[AudioSession] Audio output set to ${useSpeaker ? 'SPEAKER' : 'EARPIECE'}`); } catch (error) { console.error('[AudioSession] setAudioOutput error:', error); } }