diff --git a/app.json b/app.json
index 4bf329f..32d1b63 100644
--- a/app.json
+++ b/app.json
@@ -55,7 +55,14 @@
"favicon": "./assets/images/favicon.png"
},
"plugins": [
- "@livekit/react-native-expo-plugin",
+ [
+ "@livekit/react-native-expo-plugin",
+ {
+ "android": {
+ "audioType": "media"
+ }
+ }
+ ],
"@config-plugins/react-native-webrtc",
"expo-router",
[
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 176ef82..f4b6bf0 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -85,6 +85,13 @@ export default function TabLayout() {
href: null,
}}
/>
+ {/* Audio Debug - hidden */}
+
{/* Beneficiaries - hidden from tab bar but keeps tab bar visible */}
{
- const devices = await getAvailableAudioOutputs();
-
- // If devices found from LiveKit API, use them
- if (devices.length > 0) {
- const buttons: any[] = devices.map(device => ({
- text: device.name,
- onPress: () => selectAudioOutput(device.id),
- }));
- buttons.push({ text: 'Cancel', style: 'cancel' });
- Alert.alert('Audio Output', 'Select audio device:', buttons);
- return;
- }
-
- // Fallback for Android (and iOS if no devices found)
- // Show simple Speaker/Earpiece toggle using setAudioOutput()
- Alert.alert(
- 'Audio Output',
- 'Select audio output:',
- [
- {
- text: '🔊 Speaker',
- onPress: () => setAudioOutput(true),
- },
- {
- text: '📱 Earpiece',
- onPress: () => setAudioOutput(false),
- },
- { text: 'Cancel', style: 'cancel' },
- ]
- );
- }, []);
-
// Handle voice transcript entries - add to chat in real-time
const handleVoiceTranscript = useCallback((role: 'user' | 'assistant', text: string) => {
if (!text.trim()) return;
@@ -820,12 +785,7 @@ export default function ChatScreen() {
J
)}
-
- {isVoice && (
-
- 🎤
-
- )}
+
{item.content}
@@ -1060,16 +1020,6 @@ export default function ChatScreen() {
)}
- {/* Audio output button - only during active call */}
- {isCallActive && (
-
-
-
- )}
-
{
console.warn('[AudioSession] Could not set speaker output:', outputErr);
}
} else if (Platform.OS === 'android') {
- // Android-specific configuration - FORCE SPEAKER OUTPUT
- // CRITICAL: Use 'inCommunication' mode + 'music' stream for speaker
- // Many Android devices default to earpiece for voice calls
- console.log('[AudioSession] Configuring Android audio for SPEAKER...');
+ // ============================================================
+ // HYPOTHESIS 2: audioStreamType = 'music' (instead of 'voiceCall')
+ // Theory: STREAM_VOICE_CALL routes to earpiece, STREAM_MUSIC to speaker
+ // ============================================================
+ console.log('[AudioSession] === HYPOTHESIS 2: audioStreamType = music ===');
await AudioSession.configureAudio({
android: {
- // Use inCommunication mode but with music stream for speaker
audioTypeOptions: {
manageAudioFocus: true,
- // inCommunication gives us more control over audio routing
- audioMode: 'inCommunication',
+ audioMode: 'inCommunication', // DEFAULT
audioFocusMode: 'gain',
- // Use 'music' stream - goes to speaker by default!
- audioStreamType: 'music',
- audioAttributesUsageType: 'media',
- audioAttributesContentType: 'music',
+ audioStreamType: 'music', // <-- CHANGED from 'voiceCall'
+ audioAttributesUsageType: 'voiceCommunication', // DEFAULT
+ audioAttributesContentType: 'speech', // DEFAULT
},
- // Force speaker as output
preferredOutputList: ['speaker'],
- // Allow us to control audio routing
- forceHandleAudioRouting: true,
},
});
console.log('[AudioSession] Starting Android audio session...');
await AudioSession.startAudioSession();
-
- // After starting, explicitly set speaker output
- console.log('[AudioSession] Forcing speaker output...');
- try {
- await AudioSession.showAudioRoutePicker?.();
- } catch {
- // showAudioRoutePicker may not be available, that's ok
- }
-
- console.log('[AudioSession] Android speaker mode configured!');
+ console.log('[AudioSession] Android audio session STARTED');
}
console.log('[AudioSession] Configuration complete!');
@@ -217,7 +203,7 @@ export async function reconfigureAudioForPlayback(): Promise {
android: {
audioTypeOptions: {
manageAudioFocus: true,
- audioMode: 'inCommunication',
+ audioMode: 'normal', // Use normal mode to keep speaker
audioFocusMode: 'gain',
audioStreamType: 'music',
audioAttributesUsageType: 'media',
@@ -227,7 +213,16 @@ export async function reconfigureAudioForPlayback(): Promise {
forceHandleAudioRouting: true,
},
});
- console.log('[AudioSession] Android reconfigured for speaker playback');
+
+ // CRITICAL: Force speaker via selectAudioOutput
+ try {
+ await AudioSession.selectAudioOutput('speaker');
+ console.log('[AudioSession] Android selectAudioOutput(speaker) SUCCESS!');
+ } catch (e) {
+ console.warn('[AudioSession] selectAudioOutput failed:', e);
+ }
+
+ console.log('[AudioSession] Android reconfigured for SPEAKER playback');
}
console.log('[AudioSession] Reconfigured successfully');
@@ -329,7 +324,15 @@ export async function setAudioOutput(useSpeaker: boolean): Promise {
}
if (Platform.OS === 'ios') {
- // iOS: Use videoChat mode + defaultToSpeaker for speaker, voiceChat for earpiece
+ // iOS: Use selectAudioOutput with force_speaker
+ try {
+ await AudioSession.selectAudioOutput(useSpeaker ? 'force_speaker' : 'default');
+ console.log(`[AudioSession] iOS selectAudioOutput: ${useSpeaker ? 'force_speaker' : 'default'}`);
+ } catch (e) {
+ console.warn('[AudioSession] selectAudioOutput failed, using fallback config');
+ }
+
+ // Also configure audio mode
await AudioSession.setAppleAudioConfiguration({
audioCategory: 'playAndRecord',
audioCategoryOptions: useSpeaker
@@ -345,21 +348,26 @@ export async function setAudioOutput(useSpeaker: boolean): Promise {
},
});
} 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
+ // Android: Use selectAudioOutput DIRECTLY - this calls setSpeakerphoneOn()
+ // This is the MOST RELIABLE way to force speaker on Android!
+ try {
+ await AudioSession.selectAudioOutput(useSpeaker ? 'speaker' : 'earpiece');
+ console.log(`[AudioSession] Android selectAudioOutput: ${useSpeaker ? 'speaker' : 'earpiece'}`);
+ } catch (e) {
+ console.warn('[AudioSession] selectAudioOutput failed:', e);
+ }
+
+ // Also reconfigure audio settings as backup
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,
},