feat: add JsSIP SIP client and useSipPhone hook for Ozonetel softphone integration

This commit is contained in:
2026-03-17 18:03:31 +05:30
parent 4f9bdc7312
commit d5feaab75a
5 changed files with 425 additions and 0 deletions

157
src/hooks/use-sip-phone.ts Normal file
View File

@@ -0,0 +1,157 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { SIPClient } from '@/lib/sip-client';
import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
const DEFAULT_CONFIG: SIPConfig = {
displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent',
uri: import.meta.env.VITE_SIP_URI ?? '',
password: import.meta.env.VITE_SIP_PASSWORD ?? '',
wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '',
stunServers: 'stun:stun.l.google.com:19302',
};
export const useSipPhone = (config?: Partial<SIPConfig>) => {
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
const [callState, setCallState] = useState<CallState>('idle');
const [callerNumber, setCallerNumber] = useState<string | null>(null);
const [isMuted, setIsMuted] = useState(false);
const [isOnHold, setIsOnHold] = useState(false);
const [callDuration, setCallDuration] = useState(0);
const [callStartTime, setCallStartTime] = useState<Date | null>(null);
const sipClientRef = useRef<SIPClient | null>(null);
const durationIntervalRef = useRef<number | null>(null);
// Call duration timer
useEffect(() => {
if (callState === 'active' && !callStartTime) {
setCallStartTime(new Date());
}
if (callState === 'active') {
durationIntervalRef.current = window.setInterval(() => {
if (callStartTime) {
setCallDuration(Math.floor((Date.now() - callStartTime.getTime()) / 1000));
}
}, 1000);
} else if (callState === 'idle' || callState === 'ended' || callState === 'failed') {
if (durationIntervalRef.current) {
clearInterval(durationIntervalRef.current);
durationIntervalRef.current = null;
}
setCallDuration(0);
setCallStartTime(null);
}
return () => {
if (durationIntervalRef.current) {
clearInterval(durationIntervalRef.current);
}
};
}, [callState, callStartTime]);
// Auto-reset to idle after ended/failed
useEffect(() => {
if (callState === 'ended' || callState === 'failed') {
const timer = setTimeout(() => {
setCallState('idle');
setCallerNumber(null);
setIsMuted(false);
setIsOnHold(false);
}, 2000);
return () => clearTimeout(timer);
}
}, [callState]);
const connect = useCallback(() => {
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
if (!mergedConfig.wsServer || !mergedConfig.uri) {
console.warn('SIP config incomplete — wsServer and uri required');
return;
}
if (sipClientRef.current) {
sipClientRef.current.disconnect();
}
setConnectionStatus('connecting');
const client = new SIPClient(
mergedConfig,
(status) => setConnectionStatus(status),
(state, number) => {
setCallState(state);
if (number) setCallerNumber(number);
},
);
sipClientRef.current = client;
client.connect();
}, [config]);
const disconnect = useCallback(() => {
sipClientRef.current?.disconnect();
sipClientRef.current = null;
setConnectionStatus('disconnected');
}, []);
const makeCall = useCallback((phoneNumber: string) => {
sipClientRef.current?.call(phoneNumber);
setCallerNumber(phoneNumber);
}, []);
const answer = useCallback(() => {
sipClientRef.current?.answer();
}, []);
const hangup = useCallback(() => {
sipClientRef.current?.hangup();
}, []);
const toggleMute = useCallback(() => {
if (isMuted) {
sipClientRef.current?.unmute();
} else {
sipClientRef.current?.mute();
}
setIsMuted(!isMuted);
}, [isMuted]);
const toggleHold = useCallback(() => {
if (isOnHold) {
sipClientRef.current?.unhold();
} else {
sipClientRef.current?.hold();
}
setIsOnHold(!isOnHold);
}, [isOnHold]);
// Cleanup on unmount
useEffect(() => {
return () => {
sipClientRef.current?.disconnect();
};
}, []);
return {
// State
connectionStatus,
callState,
callerNumber,
isMuted,
isOnHold,
callDuration,
isRegistered: connectionStatus === 'registered',
isInCall: ['ringing-in', 'ringing-out', 'active'].includes(callState),
// Actions
connect,
disconnect,
makeCall,
answer,
hangup,
toggleMute,
toggleHold,
};
};