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 { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
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