Remove unused useLiveKitRoom hook
This LiveKit hook was no longer used after switching to speech recognition. Also removed the outdated comment referencing it in _layout.tsx. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
432964c4d0
commit
260a722cd9
@ -1,6 +1,3 @@
|
|||||||
// WebRTC globals are now registered in useLiveKitRoom hook
|
|
||||||
// before any LiveKit classes are loaded.
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
import { Stack, router, useSegments } from 'expo-router';
|
import { Stack, router, useSegments } from 'expo-router';
|
||||||
|
|||||||
@ -1,707 +0,0 @@
|
|||||||
/**
|
|
||||||
* useLiveKitRoom - Hook for LiveKit voice call with Julia AI
|
|
||||||
*
|
|
||||||
* IMPORTANT: This hook encapsulates ALL LiveKit logic.
|
|
||||||
* The UI component should only use the returned state and actions.
|
|
||||||
*
|
|
||||||
* LOGGING: Maximum transparency - every step is logged!
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
||||||
import { Platform, AppState, AppStateStatus, NativeModules } from 'react-native';
|
|
||||||
import type { Room as RoomType } from 'livekit-client';
|
|
||||||
|
|
||||||
// Helper to detect iOS Simulator
|
|
||||||
// Expo Go and production builds both work with this approach
|
|
||||||
const isIOSSimulator = (): boolean => {
|
|
||||||
if (Platform.OS !== 'ios') return false;
|
|
||||||
// Check via DeviceInfo module if available
|
|
||||||
const { PlatformConstants } = NativeModules;
|
|
||||||
return PlatformConstants?.interfaceIdiom === 'simulator' ||
|
|
||||||
PlatformConstants?.isSimulator === true;
|
|
||||||
};
|
|
||||||
import { getToken, VOICE_NAME, BeneficiaryData } from '@/services/livekitService';
|
|
||||||
import {
|
|
||||||
configureAudioForVoiceCall,
|
|
||||||
stopAudioSession,
|
|
||||||
reconfigureAudioForPlayback,
|
|
||||||
} from '@/utils/audioSession';
|
|
||||||
import { callManager } from '@/services/callManager';
|
|
||||||
|
|
||||||
// Connection states
|
|
||||||
export type ConnectionState =
|
|
||||||
| 'idle'
|
|
||||||
| 'initializing'
|
|
||||||
| 'configuring_audio'
|
|
||||||
| 'requesting_token'
|
|
||||||
| 'connecting'
|
|
||||||
| 'connected'
|
|
||||||
| 'reconnecting'
|
|
||||||
| 'disconnecting'
|
|
||||||
| 'disconnected'
|
|
||||||
| 'error';
|
|
||||||
|
|
||||||
// Log entry type
|
|
||||||
export interface LogEntry {
|
|
||||||
timestamp: string; // Formatted time string (HH:MM:SS.mmm)
|
|
||||||
level: 'info' | 'warn' | 'error' | 'success';
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook options
|
|
||||||
export interface UseLiveKitRoomOptions {
|
|
||||||
userId: string;
|
|
||||||
beneficiaryData?: BeneficiaryData;
|
|
||||||
onTranscript?: (role: 'user' | 'assistant', text: string) => void;
|
|
||||||
autoConnect?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook return type
|
|
||||||
export interface UseLiveKitRoomReturn {
|
|
||||||
// Connection state
|
|
||||||
state: ConnectionState;
|
|
||||||
error: string | null;
|
|
||||||
|
|
||||||
// Call info
|
|
||||||
roomName: string | null;
|
|
||||||
callDuration: number;
|
|
||||||
|
|
||||||
// Audio state
|
|
||||||
isMuted: boolean;
|
|
||||||
isAgentSpeaking: boolean;
|
|
||||||
canPlayAudio: boolean;
|
|
||||||
|
|
||||||
// Debug info
|
|
||||||
logs: LogEntry[];
|
|
||||||
participantCount: number;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
connect: () => Promise<void>;
|
|
||||||
disconnect: () => Promise<void>;
|
|
||||||
toggleMute: () => Promise<void>;
|
|
||||||
clearLogs: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main hook for LiveKit voice calls
|
|
||||||
*/
|
|
||||||
export function useLiveKitRoom(options: UseLiveKitRoomOptions): UseLiveKitRoomReturn {
|
|
||||||
const { userId, beneficiaryData, onTranscript, autoConnect = false } = options;
|
|
||||||
|
|
||||||
// State
|
|
||||||
const [state, setState] = useState<ConnectionState>('idle');
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [roomName, setRoomName] = useState<string | null>(null);
|
|
||||||
const [callDuration, setCallDuration] = useState(0);
|
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
|
||||||
const [isAgentSpeaking, setIsAgentSpeaking] = useState(false);
|
|
||||||
const [canPlayAudio, setCanPlayAudio] = useState(false);
|
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
||||||
const [participantCount, setParticipantCount] = useState(0);
|
|
||||||
|
|
||||||
// Refs
|
|
||||||
const roomRef = useRef<RoomType | null>(null);
|
|
||||||
const callStartTimeRef = useRef<number | null>(null);
|
|
||||||
const connectionIdRef = useRef(0);
|
|
||||||
const isUnmountingRef = useRef(false);
|
|
||||||
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
|
||||||
const callIdRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// LOGGING FUNCTIONS
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
const log = useCallback((level: LogEntry['level'], message: string) => {
|
|
||||||
const now = new Date();
|
|
||||||
const timestamp = now.toLocaleTimeString('en-US', {
|
|
||||||
hour12: false,
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
}) + '.' + now.getMilliseconds().toString().padStart(3, '0');
|
|
||||||
|
|
||||||
const entry: LogEntry = {
|
|
||||||
timestamp,
|
|
||||||
level,
|
|
||||||
message,
|
|
||||||
};
|
|
||||||
setLogs((prev) => [...prev, entry]);
|
|
||||||
|
|
||||||
// Also log to console with color
|
|
||||||
const prefix = `[LiveKit ${timestamp}]`;
|
|
||||||
switch (level) {
|
|
||||||
case 'error':
|
|
||||||
console.error(`${prefix} ERROR: ${message}`);
|
|
||||||
break;
|
|
||||||
case 'warn':
|
|
||||||
console.warn(`${prefix} WARN: ${message}`);
|
|
||||||
break;
|
|
||||||
case 'success':
|
|
||||||
console.log(`${prefix} SUCCESS: ${message}`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(`${prefix} INFO: ${message}`);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const logInfo = useCallback((msg: string) => log('info', msg), [log]);
|
|
||||||
const logWarn = useCallback((msg: string) => log('warn', msg), [log]);
|
|
||||||
const logError = useCallback((msg: string) => log('error', msg), [log]);
|
|
||||||
const logSuccess = useCallback((msg: string) => log('success', msg), [log]);
|
|
||||||
|
|
||||||
const clearLogs = useCallback(() => {
|
|
||||||
setLogs([]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// CONNECT FUNCTION
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
const connect = useCallback(async () => {
|
|
||||||
// Prevent multiple concurrent connection attempts
|
|
||||||
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(`User ID: ${userId}`);
|
|
||||||
logInfo(`Platform: ${Platform.OS}`);
|
|
||||||
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
|
|
||||||
if (roomRef.current) {
|
|
||||||
logWarn('Already connected to a room, disconnecting first...');
|
|
||||||
await roomRef.current.disconnect();
|
|
||||||
roomRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// ========== STEP 1: Initialize ==========
|
|
||||||
setState('initializing');
|
|
||||||
logInfo('STEP 1/6: Initializing...');
|
|
||||||
|
|
||||||
// Detect simulator vs real device
|
|
||||||
const isSimulator = isIOSSimulator();
|
|
||||||
logInfo(`Device type: ${isSimulator ? 'SIMULATOR' : 'REAL DEVICE'}`);
|
|
||||||
logInfo(`Device model: ${Platform.OS} ${Platform.Version}`);
|
|
||||||
|
|
||||||
if (isSimulator) {
|
|
||||||
logWarn('⚠️ SIMULATOR DETECTED - Microphone will NOT work!');
|
|
||||||
logWarn('Simulator can only test: connection, token, agent presence, TTS playback');
|
|
||||||
logWarn('For full STT test, use a real iPhone device');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if connection was cancelled
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Connection cancelled (component unmounting or new connection started)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== STEP 2: Register WebRTC Globals ==========
|
|
||||||
logInfo('STEP 2/6: Registering WebRTC globals...');
|
|
||||||
|
|
||||||
const { registerGlobals } = await import('@livekit/react-native');
|
|
||||||
|
|
||||||
if (typeof global.RTCPeerConnection === 'undefined') {
|
|
||||||
logInfo('RTCPeerConnection not found, calling registerGlobals()...');
|
|
||||||
registerGlobals();
|
|
||||||
logSuccess('WebRTC globals registered!');
|
|
||||||
} else {
|
|
||||||
logInfo('WebRTC globals already registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check again
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Connection cancelled after registerGlobals');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== STEP 3: Configure iOS Audio ==========
|
|
||||||
setState('configuring_audio');
|
|
||||||
logInfo('STEP 3/6: Configuring iOS AudioSession...');
|
|
||||||
|
|
||||||
await configureAudioForVoiceCall();
|
|
||||||
logSuccess('iOS AudioSession configured!');
|
|
||||||
|
|
||||||
// Check again
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Connection cancelled after audio config');
|
|
||||||
await stopAudioSession();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== STEP 4: Get Token ==========
|
|
||||||
setState('requesting_token');
|
|
||||||
logInfo('STEP 4/6: Requesting token from server...');
|
|
||||||
|
|
||||||
const tokenResult = await getToken(userId, beneficiaryData);
|
|
||||||
|
|
||||||
if (!tokenResult.success || !tokenResult.data) {
|
|
||||||
const errorMsg = tokenResult.error || 'Failed to get token';
|
|
||||||
logError(`Token request failed: ${errorMsg}`);
|
|
||||||
setError(errorMsg);
|
|
||||||
setState('error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token, wsUrl, roomName: room } = tokenResult.data;
|
|
||||||
setRoomName(room);
|
|
||||||
|
|
||||||
logSuccess(`Token received!`);
|
|
||||||
logInfo(` Room: ${room}`);
|
|
||||||
logInfo(` WebSocket URL: ${wsUrl}`);
|
|
||||||
logInfo(` Token length: ${token.length} chars`);
|
|
||||||
|
|
||||||
// Check again
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Connection cancelled after token');
|
|
||||||
await stopAudioSession();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== STEP 5: Import LiveKit and Create Room ==========
|
|
||||||
logInfo('STEP 5/6: Creating LiveKit Room...');
|
|
||||||
|
|
||||||
const { Room, RoomEvent, ConnectionState: LKConnectionState, Track } = await import(
|
|
||||||
'livekit-client'
|
|
||||||
);
|
|
||||||
|
|
||||||
logInfo(` Room class available: ${typeof Room === 'function'}`);
|
|
||||||
logInfo(` RoomEvent available: ${typeof RoomEvent === 'object'}`);
|
|
||||||
|
|
||||||
const lkRoom = new Room();
|
|
||||||
roomRef.current = lkRoom;
|
|
||||||
|
|
||||||
logSuccess('Room instance created!');
|
|
||||||
|
|
||||||
// ========== Setup Event Listeners ==========
|
|
||||||
logInfo('Setting up event listeners...');
|
|
||||||
|
|
||||||
// Connection state changes
|
|
||||||
lkRoom.on(RoomEvent.ConnectionStateChanged, (newState) => {
|
|
||||||
logInfo(`EVENT: ConnectionStateChanged -> ${newState}`);
|
|
||||||
|
|
||||||
switch (newState) {
|
|
||||||
case LKConnectionState.Connecting:
|
|
||||||
setState('connecting');
|
|
||||||
break;
|
|
||||||
case LKConnectionState.Connected:
|
|
||||||
setState('connected');
|
|
||||||
logSuccess('Connected to room!');
|
|
||||||
if (!callStartTimeRef.current) {
|
|
||||||
callStartTimeRef.current = Date.now();
|
|
||||||
logInfo('Call timer started');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LKConnectionState.Reconnecting:
|
|
||||||
setState('reconnecting');
|
|
||||||
logWarn('Reconnecting...');
|
|
||||||
break;
|
|
||||||
case LKConnectionState.Disconnected:
|
|
||||||
setState('disconnected');
|
|
||||||
logInfo('Disconnected from room');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track subscribed (audio from agent)
|
|
||||||
lkRoom.on(RoomEvent.TrackSubscribed, async (track, publication, participant) => {
|
|
||||||
logInfo(`EVENT: TrackSubscribed`);
|
|
||||||
logInfo(` Track kind: ${track.kind}`);
|
|
||||||
logInfo(` Track source: ${track.source}`);
|
|
||||||
logInfo(` Participant: ${participant.identity}`);
|
|
||||||
logInfo(` Publication SID: ${publication.trackSid}`);
|
|
||||||
|
|
||||||
if (track.kind === Track.Kind.Audio) {
|
|
||||||
logSuccess(`Audio track from ${participant.identity} - should hear voice now!`);
|
|
||||||
setIsAgentSpeaking(true);
|
|
||||||
|
|
||||||
// Reconfigure audio for playback
|
|
||||||
logInfo('Reconfiguring audio for playback...');
|
|
||||||
await reconfigureAudioForPlayback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track unsubscribed
|
|
||||||
lkRoom.on(RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
|
|
||||||
logInfo(`EVENT: TrackUnsubscribed`);
|
|
||||||
logInfo(` Track kind: ${track.kind}`);
|
|
||||||
logInfo(` Participant: ${participant.identity}`);
|
|
||||||
|
|
||||||
if (track.kind === Track.Kind.Audio) {
|
|
||||||
setIsAgentSpeaking(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track muted/unmuted
|
|
||||||
lkRoom.on(RoomEvent.TrackMuted, (publication, participant) => {
|
|
||||||
logInfo(`EVENT: TrackMuted - ${publication.trackSid} by ${participant.identity}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
lkRoom.on(RoomEvent.TrackUnmuted, (publication, participant) => {
|
|
||||||
logInfo(`EVENT: TrackUnmuted - ${publication.trackSid} by ${participant.identity}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Participants
|
|
||||||
lkRoom.on(RoomEvent.ParticipantConnected, (participant) => {
|
|
||||||
logSuccess(`EVENT: ParticipantConnected - ${participant.identity}`);
|
|
||||||
setParticipantCount((c) => c + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
lkRoom.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
||||||
logInfo(`EVENT: ParticipantDisconnected - ${participant.identity}`);
|
|
||||||
setParticipantCount((c) => Math.max(0, c - 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Active speakers (voice activity)
|
|
||||||
lkRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
||||||
if (speakers.length > 0) {
|
|
||||||
const speakerNames = speakers.map((s: any) => s.identity).join(', ');
|
|
||||||
logInfo(`EVENT: ActiveSpeakersChanged - ${speakerNames}`);
|
|
||||||
|
|
||||||
// Check if agent is speaking
|
|
||||||
const agentSpeaking = speakers.some((s: any) => s.identity.startsWith('agent'));
|
|
||||||
setIsAgentSpeaking(agentSpeaking);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Local track published (our mic)
|
|
||||||
lkRoom.on(RoomEvent.LocalTrackPublished, (publication, participant) => {
|
|
||||||
logSuccess(`EVENT: LocalTrackPublished`);
|
|
||||||
logInfo(` Track: ${publication.trackSid}`);
|
|
||||||
logInfo(` Kind: ${publication.kind}`);
|
|
||||||
logInfo(` Source: ${publication.source}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Audio playback status
|
|
||||||
lkRoom.on(RoomEvent.AudioPlaybackStatusChanged, () => {
|
|
||||||
const canPlay = lkRoom.canPlaybackAudio;
|
|
||||||
logInfo(`EVENT: AudioPlaybackStatusChanged - canPlaybackAudio: ${canPlay}`);
|
|
||||||
setCanPlayAudio(canPlay);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Data received (transcripts)
|
|
||||||
lkRoom.on(RoomEvent.DataReceived, (payload, participant) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(new TextDecoder().decode(payload));
|
|
||||||
logInfo(`EVENT: DataReceived from ${participant?.identity || 'unknown'}`);
|
|
||||||
logInfo(` Type: ${data.type}`);
|
|
||||||
|
|
||||||
if (data.type === 'transcript' && onTranscript) {
|
|
||||||
logInfo(` Role: ${data.role}, Text: ${data.text?.substring(0, 50)}...`);
|
|
||||||
onTranscript(data.role, data.text);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Non-JSON data, ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Errors
|
|
||||||
lkRoom.on(RoomEvent.Disconnected, (reason) => {
|
|
||||||
logWarn(`EVENT: Disconnected - Reason: ${reason}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check again before connect
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Connection cancelled before room.connect()');
|
|
||||||
await stopAudioSession();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== STEP 6: Connect to Room ==========
|
|
||||||
setState('connecting');
|
|
||||||
logInfo('STEP 6/6: Connecting to LiveKit room...');
|
|
||||||
logInfo(` URL: ${wsUrl}`);
|
|
||||||
logInfo(` Room: ${room}`);
|
|
||||||
|
|
||||||
await lkRoom.connect(wsUrl, token, {
|
|
||||||
autoSubscribe: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
logSuccess('Connected to room!');
|
|
||||||
|
|
||||||
// ========== CRITICAL: Start Audio Playback ==========
|
|
||||||
// This is REQUIRED for audio to play on iOS and Android!
|
|
||||||
// Without this call, remote audio tracks will NOT be heard.
|
|
||||||
logInfo('Starting audio playback (room.startAudio)...');
|
|
||||||
try {
|
|
||||||
await lkRoom.startAudio();
|
|
||||||
logSuccess(`Audio playback started! canPlaybackAudio: ${lkRoom.canPlaybackAudio}`);
|
|
||||||
setCanPlayAudio(lkRoom.canPlaybackAudio);
|
|
||||||
} catch (audioPlaybackErr: any) {
|
|
||||||
logError(`startAudio failed: ${audioPlaybackErr.message}`);
|
|
||||||
// Don't fail the whole call - audio might still work on some platforms
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if connection was cancelled after connect
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Connection cancelled after room.connect()');
|
|
||||||
await lkRoom.disconnect();
|
|
||||||
await stopAudioSession();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Enable Microphone ==========
|
|
||||||
logInfo('Enabling microphone...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await lkRoom.localParticipant.setMicrophoneEnabled(true);
|
|
||||||
logSuccess('Microphone enabled!');
|
|
||||||
logInfo(` Local participant: ${lkRoom.localParticipant.identity}`);
|
|
||||||
|
|
||||||
// Log track info - CRITICAL for debugging!
|
|
||||||
const audioTracks = lkRoom.localParticipant.getTrackPublications();
|
|
||||||
logInfo(` Published tracks: ${audioTracks.length}`);
|
|
||||||
|
|
||||||
let micTrackFound = false;
|
|
||||||
audioTracks.forEach((pub) => {
|
|
||||||
logInfo(` - ${pub.kind}: ${pub.trackSid} (${pub.source})`);
|
|
||||||
logInfo(` isMuted: ${pub.isMuted}, isSubscribed: ${pub.isSubscribed}`);
|
|
||||||
|
|
||||||
if (pub.kind === 'audio' && pub.source === 'microphone') {
|
|
||||||
micTrackFound = true;
|
|
||||||
const track = pub.track;
|
|
||||||
if (track) {
|
|
||||||
logInfo(` Track mediaStreamTrack: ${track.mediaStreamTrack?.readyState || 'N/A'}`);
|
|
||||||
logInfo(` Track enabled: ${track.mediaStreamTrack?.enabled || 'N/A'}`);
|
|
||||||
} else {
|
|
||||||
logWarn(` WARNING: No track object on publication!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!micTrackFound) {
|
|
||||||
// Check if simulator
|
|
||||||
const isSimulator = isIOSSimulator();
|
|
||||||
if (isSimulator) {
|
|
||||||
logWarn('No microphone track - EXPECTED on simulator');
|
|
||||||
logInfo('Simulator test: check if Agent joined and TTS works');
|
|
||||||
} else {
|
|
||||||
logError('CRITICAL: No microphone track published! STT will NOT work!');
|
|
||||||
logError('Possible causes: permissions denied, AudioSession not configured, hardware issue');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logSuccess('Microphone track found and published - STT should work');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (micError: any) {
|
|
||||||
logError(`Failed to enable microphone: ${micError.message}`);
|
|
||||||
logError(`Stack: ${micError.stack || 'N/A'}`);
|
|
||||||
// This is CRITICAL - user must know!
|
|
||||||
setError(`Microphone error: ${micError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial participant count
|
|
||||||
setParticipantCount(lkRoom.remoteParticipants.size);
|
|
||||||
logInfo(`Remote participants: ${lkRoom.remoteParticipants.size}`);
|
|
||||||
|
|
||||||
logSuccess('========== VOICE CALL STARTED ==========');
|
|
||||||
} catch (err: any) {
|
|
||||||
// Ignore errors if unmounting
|
|
||||||
if (isUnmountingRef.current || currentConnectionId !== connectionIdRef.current) {
|
|
||||||
logWarn('Error ignored (component unmounting)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMsg = err?.message || String(err);
|
|
||||||
logError(`Connection failed: ${errorMsg}`);
|
|
||||||
logError(`Stack: ${err?.stack || 'N/A'}`);
|
|
||||||
|
|
||||||
setError(errorMsg);
|
|
||||||
setState('error');
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await stopAudioSession();
|
|
||||||
}
|
|
||||||
}, [userId, beneficiaryData, onTranscript, logInfo, logWarn, logError, logSuccess]);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// DISCONNECT FUNCTION
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
const disconnect = useCallback(async () => {
|
|
||||||
logInfo('========== DISCONNECTING ==========');
|
|
||||||
setState('disconnecting');
|
|
||||||
|
|
||||||
// Unregister from CallManager
|
|
||||||
if (callIdRef.current) {
|
|
||||||
logInfo(`Unregistering call: ${callIdRef.current}`);
|
|
||||||
callManager.unregisterCall(callIdRef.current);
|
|
||||||
callIdRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (roomRef.current) {
|
|
||||||
logInfo('Disconnecting from room...');
|
|
||||||
await roomRef.current.disconnect();
|
|
||||||
roomRef.current = null;
|
|
||||||
logSuccess('Disconnected from room');
|
|
||||||
} else {
|
|
||||||
logInfo('No room to disconnect from');
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
logError(`Disconnect error: ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logInfo('Stopping audio session...');
|
|
||||||
await stopAudioSession();
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
setState('disconnected');
|
|
||||||
setRoomName(null);
|
|
||||||
setIsMuted(false);
|
|
||||||
setIsAgentSpeaking(false);
|
|
||||||
setParticipantCount(0);
|
|
||||||
callStartTimeRef.current = null;
|
|
||||||
|
|
||||||
logSuccess('========== DISCONNECTED ==========');
|
|
||||||
}, [logInfo, logError, logSuccess]);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// TOGGLE MUTE
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
const toggleMute = useCallback(async () => {
|
|
||||||
if (!roomRef.current) {
|
|
||||||
logWarn('Cannot toggle mute - not connected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMuted = !isMuted;
|
|
||||||
logInfo(`Toggling mute: ${isMuted} -> ${newMuted}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await roomRef.current.localParticipant.setMicrophoneEnabled(!newMuted);
|
|
||||||
setIsMuted(newMuted);
|
|
||||||
logSuccess(`Microphone ${newMuted ? 'muted' : 'unmuted'}`);
|
|
||||||
} catch (err: any) {
|
|
||||||
logError(`Failed to toggle mute: ${err.message}`);
|
|
||||||
}
|
|
||||||
}, [isMuted, logInfo, logWarn, logError, logSuccess]);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// CALL DURATION TIMER
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (state !== 'connected') return;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (callStartTimeRef.current) {
|
|
||||||
const elapsed = Math.floor((Date.now() - callStartTimeRef.current) / 1000);
|
|
||||||
setCallDuration(elapsed);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// APP STATE HANDLING
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
||||||
const prevState = appStateRef.current;
|
|
||||||
appStateRef.current = nextAppState;
|
|
||||||
|
|
||||||
if (prevState.match(/inactive|background/) && nextAppState === 'active') {
|
|
||||||
logInfo('App returned to foreground');
|
|
||||||
} else if (prevState === 'active' && nextAppState.match(/inactive|background/)) {
|
|
||||||
logInfo('App went to background - call continues in background');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
||||||
return () => subscription.remove();
|
|
||||||
}, [logInfo]);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// CLEANUP ON UNMOUNT
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isUnmountingRef.current = false;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isUnmountingRef.current = true;
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
const cleanup = async () => {
|
|
||||||
// Unregister from CallManager
|
|
||||||
if (callIdRef.current) {
|
|
||||||
callManager.unregisterCall(callIdRef.current);
|
|
||||||
callIdRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (roomRef.current) {
|
|
||||||
try {
|
|
||||||
await roomRef.current.disconnect();
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
roomRef.current = null;
|
|
||||||
}
|
|
||||||
await stopAudioSession();
|
|
||||||
};
|
|
||||||
|
|
||||||
cleanup();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// AUTO CONNECT
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (autoConnect && state === 'idle') {
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
}, [autoConnect, state, connect]);
|
|
||||||
|
|
||||||
// ===================
|
|
||||||
// RETURN
|
|
||||||
// ===================
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Connection state
|
|
||||||
state,
|
|
||||||
error,
|
|
||||||
|
|
||||||
// Call info
|
|
||||||
roomName,
|
|
||||||
callDuration,
|
|
||||||
|
|
||||||
// Audio state
|
|
||||||
isMuted,
|
|
||||||
isAgentSpeaking,
|
|
||||||
canPlayAudio,
|
|
||||||
|
|
||||||
// Debug
|
|
||||||
logs,
|
|
||||||
participantCount,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
toggleMute,
|
|
||||||
clearLogs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { VOICE_NAME };
|
|
||||||
Loading…
x
Reference in New Issue
Block a user