/** * 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 console.log('[AudioSession] Step 1: Setting Apple audio config...'); await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', // Note: removed 'allowBluetoothA2DP' - it's incompatible with playAndRecord // on some iOS versions and causes "status -50" error. // 'allowBluetooth' (HFP profile) is sufficient for voice calls. audioCategoryOptions: [ 'allowBluetooth', 'defaultToSpeaker', 'mixWithOthers', ], audioMode: 'voiceChat', }); console.log('[AudioSession] Step 2: Setting default output...'); await AudioSession.configureAudio({ ios: { defaultOutput: 'speaker', }, }); console.log('[AudioSession] Step 3: Starting audio session...'); await AudioSession.startAudioSession(); } 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') { // Just reconfigure the same settings - this "refreshes" the audio routing await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', // Note: removed 'allowBluetoothA2DP' - it's incompatible with playAndRecord // on some iOS versions and causes "status -50" error. // 'allowBluetooth' (HFP profile) is sufficient for voice calls. audioCategoryOptions: [ 'allowBluetooth', 'defaultToSpeaker', 'mixWithOthers', ], audioMode: 'voiceChat', }); } 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: Configure audio output await AudioSession.configureAudio({ ios: { defaultOutput: useSpeaker ? 'speaker' : 'earpiece', }, }); // Also update the full configuration to ensure it takes effect // Note: removed 'allowBluetoothA2DP' - causes "status -50" error await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: useSpeaker ? ['allowBluetooth', 'defaultToSpeaker', 'mixWithOthers'] : ['allowBluetooth', 'mixWithOthers'], audioMode: 'voiceChat', }); } 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); } }