UNPKG

@revrag-ai/embed-react-native

Version:

A powerful React Native library for integrating AI-powered voice agents into mobile applications. Features real-time voice communication, intelligent speech processing, customizable UI components, and comprehensive event handling for building conversation

324 lines (294 loc) 10.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useVoiceAgent = void 0; var _livekitClient = require("livekit-client"); var _react = require("react"); var _embedEvent = require("../events/embed.event.js"); var _api = require("../api/api.js"); var _storeKey = require("../store/store.key.js"); // Create a singleton instance of Room that persists across hook instances // This ensures we don't create multiple Room instances that could conflict let roomInstance = null; let isConnecting = false; let isDisconnecting = false; let hasListeners = false; let connectionAttempts = 0; let stableConnectionTimerId = null; const MAX_CONNECTION_ATTEMPTS = 3; // Create getters for the room instance const getRoomInstance = () => { if (!roomInstance) { // Configure the room with the right options at creation time roomInstance = new _livekitClient.Room({ adaptiveStream: true, dynacast: true, // Using the most stable configuration for React Native publishDefaults: { simulcast: false // Disable simulcast to reduce SDP complexity } }); } return roomInstance; }; // Reset connection attempt counter const resetConnectionAttempts = () => { connectionAttempts = 0; // Clear any pending stable connection timer if (stableConnectionTimerId) { clearTimeout(stableConnectionTimerId); stableConnectionTimerId = null; } }; // Cleanup function to reset the room state const resetRoomState = () => { if (roomInstance) { try { // Only disconnect if currently connected if (roomInstance.state !== _livekitClient.ConnectionState.Disconnected && !isDisconnecting) { isDisconnecting = true; roomInstance.disconnect().finally(() => { isDisconnecting = false; }); } // Don't destroy the room instance, just reset its state hasListeners = false; resetConnectionAttempts(); } catch (e) { // todo: handle error resetting room state } } }; const useVoiceAgent = () => { const [isLoading, setIsLoading] = (0, _react.useState)(false); const [error, setError] = (0, _react.useState)(null); const [tokenDetails, setTokenDetails] = (0, _react.useState)(null); const [isMicMuted, setIsMicMuted] = (0, _react.useState)(false); const [connectionState, setConnectionState] = (0, _react.useState)(() => { // Initialize with the current room state if it exists const room = getRoomInstance(); return room ? room.state : _livekitClient.ConnectionState.Disconnected; }); // Setup event listeners for the room const setupRoomListeners = () => { if (hasListeners || !roomInstance) return; const handleConnectionChange = state => { // Use a function to ensure we're setting with the latest state setConnectionState(() => state); // Handle connection state changes if (state === _livekitClient.ConnectionState.Connected) { // Reset connection attempts when connected successfully resetConnectionAttempts(); // Set up a timer to mark the connection as stable after a few seconds // This prevents immediate disconnections from being treated as stable if (stableConnectionTimerId) { clearTimeout(stableConnectionTimerId); } } else if (state === _livekitClient.ConnectionState.Disconnected) { // Mark connection as unstable // Clear any pending stable connection timer if (stableConnectionTimerId) { clearTimeout(stableConnectionTimerId); stableConnectionTimerId = null; } // If disconnected unexpectedly and we have token details, don't automatically reconnect // This prevents connection loops if (tokenDetails && !isDisconnecting) { // todo: handle room disconnected } } }; const handleTrackSubscribed = track => { if (track.kind === 'audio') { track.setVolume(1.0); } }; // Handle participant disconnected - this could be helpful to detect server-side kicks roomInstance.on('participantDisconnected', () => { // todo: handle participant disconnected }); roomInstance.on('connectionStateChanged', handleConnectionChange); roomInstance.on('trackSubscribed', handleTrackSubscribed); // Listen for SDP negotiation errors to handle them better roomInstance.on('mediaDevicesError', e => { throw new Error(`Media devices error: ${e.message}`); // todo: handle media devices error }); hasListeners = true; }; // Initialize room and listeners (0, _react.useEffect)(() => { const room = getRoomInstance(); setupRoomListeners(); // Sync local state with room state setConnectionState(room.state); return () => { // Do NOT disconnect or destroy the room on component unmount // This ensures the singleton persists across component lifecycle // But we do want to update local state if the component is unmounted if (!tokenDetails) { setTokenDetails(null); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Connect to LiveKit when token is set (0, _react.useEffect)(() => { const room = getRoomInstance(); if (!tokenDetails || isConnecting) return; // Always sync our state with the room's current state setConnectionState(room.state); const connectToRoom = async () => { // Prevent multiple connection attempts if (isConnecting) return; // Limit connection attempts to prevent infinite loops if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) { setError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`); return; } connectionAttempts++; try { isConnecting = true; // Only attempt to connect if we're disconnected if (room.state === _livekitClient.ConnectionState.Disconnected) { // Update state before connection attempt setConnectionState(_livekitClient.ConnectionState.Connecting); await room.connect(tokenDetails.server_url, tokenDetails.token, { autoSubscribe: true // Ensure we subscribe to tracks automatically }); // Explicitly set to connected if connection was successful setConnectionState(room.state); } else if (room.state === _livekitClient.ConnectionState.Connected) { // Ensure our state matches setConnectionState(_livekitClient.ConnectionState.Connected); } else { // Sync our state with the room's current state setConnectionState(room.state); } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to connect to room'; setError(message); // Sync state to disconnected if we failed to connect setConnectionState(room.state); // Don't keep retrying if we hit an SDP error if (message.includes('SDP') || message.includes('sdp')) {} } finally { isConnecting = false; } }; connectToRoom(); // No cleanup here - we don't want to disconnect when token changes or component unmounts }, [tokenDetails]); // Generate token and connect const generateVoiceToken = async () => { // Don't generate a new token if we're already connecting/connected if (isConnecting || isLoading) { // todo: handle already connecting or loading return; } // Reset connection attempts when starting fresh resetConnectionAttempts(); const userData = await (0, _storeKey.getAgentData)(_embedEvent.EventKeys.USER_DATA); setIsLoading(true); setError(null); try { const apiService = _api.APIService.getInstance(); const response = await apiService.getTokenDetails({ app_user_id: userData?.app_user_id, call_type: 'EMBEDDED' }); if (!response.data) throw new Error('No voice token found'); // Only set token details if we're not already connected const room = getRoomInstance(); if (room.state !== _livekitClient.ConnectionState.Connected) { setTokenDetails(response.data); } else { // todo: handle room already connected } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to generate voice token'; setError(message); throw new Error(message); } finally { setIsLoading(false); } }; // End call const endCall = async () => { if (isDisconnecting) { // todo: handle already disconnecting return; } try { isDisconnecting = true; setTokenDetails(null); setIsMicMuted(false); resetConnectionAttempts(); const room = getRoomInstance(); if (room.state !== _livekitClient.ConnectionState.Disconnected) { // Update state before disconnection setConnectionState(_livekitClient.ConnectionState.Connecting); await room.disconnect(); // Update state after disconnection setConnectionState(_livekitClient.ConnectionState.Disconnected); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to end call'); // Make sure state is correctly reflected even if there's an error const room = getRoomInstance(); setConnectionState(room.state); } finally { isDisconnecting = false; } }; // Mute microphone const muteMic = () => { const room = getRoomInstance(); if (room.localParticipant) { room.localParticipant.setMicrophoneEnabled(false); setIsMicMuted(true); } }; // Unmute microphone const unmuteMic = () => { const room = getRoomInstance(); if (room.localParticipant) { room.localParticipant.setMicrophoneEnabled(true); setIsMicMuted(false); } }; // Clean up everything (use only when the app is shutting down) const cleanup = () => { endCall(); resetRoomState(); }; const getPopupDescription = async () => { const userData = await (0, _storeKey.getAgentData)(_embedEvent.EventKeys.USER_DATA); const apiService = _api.APIService.getInstance(); const response = await apiService.getPopupDescription({ app_user_id: userData?.app_user_id }); if (!response.data) throw new Error('No popup description found'); return response.data; }; return { initializeVoiceAgent: generateVoiceToken, endCall, muteMic, unmuteMic, isMicMuted, connectionState, room: getRoomInstance(), tokenDetails, isLoading, error, roomRef: { current: getRoomInstance() }, cleanup, getPopupDescription }; }; exports.useVoiceAgent = useVoiceAgent; //# sourceMappingURL=voiceagent.js.map