mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: add JsSIP SIP client and useSipPhone hook for Ozonetel softphone integration
This commit is contained in:
157
src/hooks/use-sip-phone.ts
Normal file
157
src/hooks/use-sip-phone.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user