Voice call improvements: single call limit, hide debug tab, remove speaker toggle
Changes: - Add CallManager singleton to ensure only 1 call per device at a time - Hide Debug tab from production (href: null) - Remove speaker/earpiece toggle button (always use speaker) - Agent uses voice_ask API (fast ~1 sec latency) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
57577b42c9
commit
e3192ead12
@ -72,14 +72,11 @@ export default function TabLayout() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Debug tab - for testing audio/voice */}
|
{/* Debug tab - hidden in production */}
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="debug"
|
name="debug"
|
||||||
options={{
|
options={{
|
||||||
title: 'Debug',
|
href: null,
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Feather name="terminal" size={22} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Hide explore tab */}
|
{/* Hide explore tab */}
|
||||||
|
|||||||
@ -288,19 +288,8 @@ export default function VoiceCallScreen() {
|
|||||||
<Ionicons name="call" size={32} color={AppColors.white} />
|
<Ionicons name="call" size={32} color={AppColors.white} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Speaker/Earpiece toggle */}
|
{/* Empty placeholder for layout balance */}
|
||||||
<TouchableOpacity
|
<View style={styles.controlButton} />
|
||||||
style={[styles.controlButton, isSpeakerOn && styles.controlButtonActive]}
|
|
||||||
onPress={handleToggleSpeaker}
|
|
||||||
disabled={!isActive}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={isSpeakerOn ? 'volume-high' : 'ear'}
|
|
||||||
size={28}
|
|
||||||
color={isSpeakerOn ? AppColors.success : AppColors.white}
|
|
||||||
/>
|
|
||||||
<Text style={styles.controlLabel}>{isSpeakerOn ? 'Speaker' : 'Earpiece'}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
stopAudioSession,
|
stopAudioSession,
|
||||||
reconfigureAudioForPlayback,
|
reconfigureAudioForPlayback,
|
||||||
} from '@/utils/audioSession';
|
} from '@/utils/audioSession';
|
||||||
|
import { callManager } from '@/services/callManager';
|
||||||
|
|
||||||
// Connection states
|
// Connection states
|
||||||
export type ConnectionState =
|
export type ConnectionState =
|
||||||
@ -103,6 +104,7 @@ export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomRe
|
|||||||
const connectionIdRef = useRef(0);
|
const connectionIdRef = useRef(0);
|
||||||
const isUnmountingRef = useRef(false);
|
const isUnmountingRef = useRef(false);
|
||||||
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||||
|
const callIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// ===================
|
// ===================
|
||||||
// LOGGING FUNCTIONS
|
// LOGGING FUNCTIONS
|
||||||
@ -158,10 +160,27 @@ export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomRe
|
|||||||
// Prevent multiple concurrent connection attempts
|
// Prevent multiple concurrent connection attempts
|
||||||
const currentConnectionId = ++connectionIdRef.current;
|
const currentConnectionId = ++connectionIdRef.current;
|
||||||
|
|
||||||
|
// Generate unique call ID for this session
|
||||||
|
const callId = `call-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
callIdRef.current = callId;
|
||||||
|
|
||||||
logInfo('========== STARTING VOICE CALL ==========');
|
logInfo('========== STARTING VOICE CALL ==========');
|
||||||
logInfo(`User ID: ${userId}`);
|
logInfo(`User ID: ${userId}`);
|
||||||
logInfo(`Platform: ${Platform.OS}`);
|
logInfo(`Platform: ${Platform.OS}`);
|
||||||
logInfo(`Connection ID: ${currentConnectionId}`);
|
logInfo(`Connection ID: ${currentConnectionId}`);
|
||||||
|
logInfo(`Call ID: ${callId}`);
|
||||||
|
|
||||||
|
// Register with CallManager - this will disconnect any existing call
|
||||||
|
logInfo('Registering call with CallManager...');
|
||||||
|
await callManager.registerCall(callId, async () => {
|
||||||
|
logInfo('CallManager requested disconnect (another call starting)');
|
||||||
|
if (roomRef.current) {
|
||||||
|
await roomRef.current.disconnect();
|
||||||
|
roomRef.current = null;
|
||||||
|
}
|
||||||
|
await stopAudioSession();
|
||||||
|
});
|
||||||
|
logSuccess('Call registered with CallManager');
|
||||||
|
|
||||||
// Check if already connected
|
// Check if already connected
|
||||||
if (roomRef.current) {
|
if (roomRef.current) {
|
||||||
@ -505,6 +524,13 @@ export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomRe
|
|||||||
logInfo('========== DISCONNECTING ==========');
|
logInfo('========== DISCONNECTING ==========');
|
||||||
setState('disconnecting');
|
setState('disconnecting');
|
||||||
|
|
||||||
|
// Unregister from CallManager
|
||||||
|
if (callIdRef.current) {
|
||||||
|
logInfo(`Unregistering call: ${callIdRef.current}`);
|
||||||
|
callManager.unregisterCall(callIdRef.current);
|
||||||
|
callIdRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (roomRef.current) {
|
if (roomRef.current) {
|
||||||
logInfo('Disconnecting from room...');
|
logInfo('Disconnecting from room...');
|
||||||
@ -603,6 +629,12 @@ export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomRe
|
|||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
|
// Unregister from CallManager
|
||||||
|
if (callIdRef.current) {
|
||||||
|
callManager.unregisterCall(callIdRef.current);
|
||||||
|
callIdRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (roomRef.current) {
|
if (roomRef.current) {
|
||||||
try {
|
try {
|
||||||
await roomRef.current.disconnect();
|
await roomRef.current.disconnect();
|
||||||
|
|||||||
@ -191,9 +191,10 @@ class WellNuoLLM(llm.LLM):
|
|||||||
token = await self._ensure_token()
|
token = await self._ensure_token()
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
# Using ask_wellnuo_ai instead of voice_ask (same params, same response)
|
# Using voice_ask - MUCH faster than ask_wellnuo_ai (1s vs 27s)
|
||||||
|
# ask_wellnuo_ai has 20x higher latency and causes timeouts
|
||||||
data = {
|
data = {
|
||||||
"function": "ask_wellnuo_ai",
|
"function": "voice_ask",
|
||||||
"clientId": "MA_001",
|
"clientId": "MA_001",
|
||||||
"user_name": WELLNUO_USER,
|
"user_name": WELLNUO_USER,
|
||||||
"token": token,
|
"token": token,
|
||||||
|
|||||||
111
services/callManager.ts
Normal file
111
services/callManager.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* CallManager - Singleton to manage active voice calls
|
||||||
|
*
|
||||||
|
* Ensures only ONE voice call can be active at a time per device.
|
||||||
|
* If a new call is started while another is active, the old one is disconnected first.
|
||||||
|
*
|
||||||
|
* This addresses the LiveKit concurrent agent jobs limit (5 per project).
|
||||||
|
*/
|
||||||
|
|
||||||
|
type DisconnectCallback = () => Promise<void>;
|
||||||
|
|
||||||
|
class CallManager {
|
||||||
|
private static instance: CallManager;
|
||||||
|
private activeCallId: string | null = null;
|
||||||
|
private disconnectCallback: DisconnectCallback | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// Singleton
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): CallManager {
|
||||||
|
if (!CallManager.instance) {
|
||||||
|
CallManager.instance = new CallManager();
|
||||||
|
}
|
||||||
|
return CallManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new call. If there's an existing call, disconnect it first.
|
||||||
|
* @param callId Unique ID for this call
|
||||||
|
* @param onDisconnect Callback to disconnect this call
|
||||||
|
* @returns true if this call can proceed
|
||||||
|
*/
|
||||||
|
async registerCall(
|
||||||
|
callId: string,
|
||||||
|
onDisconnect: DisconnectCallback
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log(`[CallManager] Registering call: ${callId}`);
|
||||||
|
|
||||||
|
// If there's an active call, disconnect it first
|
||||||
|
if (this.activeCallId && this.activeCallId !== callId) {
|
||||||
|
console.log(
|
||||||
|
`[CallManager] Active call exists (${this.activeCallId}), disconnecting...`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.disconnectCallback) {
|
||||||
|
try {
|
||||||
|
await this.disconnectCallback();
|
||||||
|
console.log(`[CallManager] Previous call disconnected`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[CallManager] Error disconnecting previous call:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the new call
|
||||||
|
this.activeCallId = callId;
|
||||||
|
this.disconnectCallback = onDisconnect;
|
||||||
|
console.log(`[CallManager] Call ${callId} is now active`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a call when it ends
|
||||||
|
* @param callId The call ID to unregister
|
||||||
|
*/
|
||||||
|
unregisterCall(callId: string): void {
|
||||||
|
if (this.activeCallId === callId) {
|
||||||
|
console.log(`[CallManager] Unregistering call: ${callId}`);
|
||||||
|
this.activeCallId = null;
|
||||||
|
this.disconnectCallback = null;
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`[CallManager] Call ${callId} is not active, ignoring unregister`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there's an active call
|
||||||
|
*/
|
||||||
|
hasActiveCall(): boolean {
|
||||||
|
return this.activeCallId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current active call ID
|
||||||
|
*/
|
||||||
|
getActiveCallId(): string | null {
|
||||||
|
return this.activeCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force disconnect the active call (if any)
|
||||||
|
*/
|
||||||
|
async forceDisconnect(): Promise<void> {
|
||||||
|
if (this.activeCallId && this.disconnectCallback) {
|
||||||
|
console.log(`[CallManager] Force disconnecting call: ${this.activeCallId}`);
|
||||||
|
try {
|
||||||
|
await this.disconnectCallback();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[CallManager] Error force disconnecting:`, err);
|
||||||
|
}
|
||||||
|
this.activeCallId = null;
|
||||||
|
this.disconnectCallback = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const callManager = CallManager.getInstance();
|
||||||
Loading…
x
Reference in New Issue
Block a user