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:
41
package-lock.json
generated
41
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"@untitledui/file-icons": "^0.0.8",
|
"@untitledui/file-icons": "^0.0.8",
|
||||||
"@untitledui/icons": "^0.0.21",
|
"@untitledui/icons": "^0.0.21",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"jssip": "^3.13.6",
|
||||||
"motion": "^12.29.0",
|
"motion": "^12.29.0",
|
||||||
"qr-code-styling": "^1.9.2",
|
"qr-code-styling": "^1.9.2",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
"@types/jssip": "^3.5.3",
|
||||||
"@types/node": "^24.10.9",
|
"@types/node": "^24.10.9",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -3710,6 +3712,16 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jssip": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "http://localhost:4873/@types/jssip/-/jssip-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-Rvw7hJPEJ12dlinAyzGpt3wxyPFMJemKRU4jTGRlRATZIdaIZUmpehsdU0oq9jIhx9bNNqIgzR+eR5Xe/U0/2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jssip": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.12.0",
|
"version": "24.12.0",
|
||||||
"resolved": "http://localhost:4873/@types/node/-/node-24.12.0.tgz",
|
"resolved": "http://localhost:4873/@types/node/-/node-24.12.0.tgz",
|
||||||
@@ -4423,6 +4435,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "http://localhost:4873/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "http://localhost:4873/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "http://localhost:4873/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -4730,6 +4751,17 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/jssip": {
|
||||||
|
"version": "3.13.6",
|
||||||
|
"resolved": "http://localhost:4873/jssip/-/jssip-3.13.6.tgz",
|
||||||
|
"integrity": "sha512-Bf1ndrSuqpO87/AG56WACR7kKcCvKOzaIQROu7JUMh0qFaGOV4NuR+wsnaXa7f3/d6xhwVczczFyt1ywJmTjPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"sdp-transform": "^2.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "http://localhost:4873/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "http://localhost:4873/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -5638,6 +5670,15 @@
|
|||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sdp-transform": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "http://localhost:4873/sdp-transform/-/sdp-transform-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"sdp-verify": "checker.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "http://localhost:4873/semver/-/semver-7.7.4.tgz",
|
"resolved": "http://localhost:4873/semver/-/semver-7.7.4.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@untitledui/file-icons": "^0.0.8",
|
"@untitledui/file-icons": "^0.0.8",
|
||||||
"@untitledui/icons": "^0.0.21",
|
"@untitledui/icons": "^0.0.21",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"jssip": "^3.13.6",
|
||||||
"motion": "^12.29.0",
|
"motion": "^12.29.0",
|
||||||
"qr-code-styling": "^1.9.2",
|
"qr-code-styling": "^1.9.2",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
"@types/jssip": "^3.5.3",
|
||||||
"@types/node": "^24.10.9",
|
"@types/node": "^24.10.9",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
214
src/lib/sip-client.ts
Normal file
214
src/lib/sip-client.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import JsSIP from 'jssip';
|
||||||
|
import type { UAConfiguration, RTCSessionEvent, CallOptions } from 'jssip/lib/UA';
|
||||||
|
import type { RTCSession, PeerConnectionEvent, EndEvent, CallListener } from 'jssip/lib/RTCSession';
|
||||||
|
import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
|
||||||
|
|
||||||
|
export class SIPClient {
|
||||||
|
private ua: JsSIP.UA | null = null;
|
||||||
|
private currentSession: RTCSession | null = null;
|
||||||
|
private audioElement: HTMLAudioElement | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private config: SIPConfig,
|
||||||
|
private onConnectionChange: (status: ConnectionStatus) => void,
|
||||||
|
private onCallStateChange: (state: CallState, callerNumber?: string) => void,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
connect(): void {
|
||||||
|
const socket = new JsSIP.WebSocketInterface(this.config.wsServer);
|
||||||
|
|
||||||
|
const configuration: UAConfiguration = {
|
||||||
|
sockets: [socket],
|
||||||
|
uri: this.config.uri,
|
||||||
|
password: this.config.password,
|
||||||
|
display_name: this.config.displayName,
|
||||||
|
register: true,
|
||||||
|
register_expires: 120,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ua = new JsSIP.UA(configuration);
|
||||||
|
|
||||||
|
this.ua.on('connected', () => {
|
||||||
|
this.onConnectionChange('connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ua.on('disconnected', () => {
|
||||||
|
this.onConnectionChange('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ua.on('registered', () => {
|
||||||
|
this.onConnectionChange('registered');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ua.on('unregistered', () => {
|
||||||
|
this.onConnectionChange('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ua.on('registrationFailed', () => {
|
||||||
|
this.onConnectionChange('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ua.on('newRTCSession', (data: RTCSessionEvent) => {
|
||||||
|
const session = data.session;
|
||||||
|
this.currentSession = session;
|
||||||
|
|
||||||
|
// Extract caller number
|
||||||
|
const remoteUri = session.remote_identity?.uri?.toString() ?? '';
|
||||||
|
const callerNumber = remoteUri.replace('sip:', '').split('@')[0] || 'Unknown';
|
||||||
|
|
||||||
|
// Setup audio
|
||||||
|
session.on('peerconnection', (e: PeerConnectionEvent) => {
|
||||||
|
const pc = e.peerconnection;
|
||||||
|
pc.ontrack = (event: RTCTrackEvent) => {
|
||||||
|
if (!this.audioElement) {
|
||||||
|
this.audioElement = document.createElement('audio');
|
||||||
|
this.audioElement.autoplay = true;
|
||||||
|
document.body.appendChild(this.audioElement);
|
||||||
|
}
|
||||||
|
this.audioElement.srcObject = event.streams[0];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('accepted', (() => {
|
||||||
|
this.onCallStateChange('active', callerNumber);
|
||||||
|
}) as CallListener);
|
||||||
|
|
||||||
|
session.on('confirmed', () => {
|
||||||
|
this.onCallStateChange('active', callerNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('progress', (() => {
|
||||||
|
if (session.direction === 'outgoing') {
|
||||||
|
this.onCallStateChange('ringing-out', callerNumber);
|
||||||
|
}
|
||||||
|
}) as CallListener);
|
||||||
|
|
||||||
|
session.on('failed', (_e: EndEvent) => {
|
||||||
|
this.onCallStateChange('failed');
|
||||||
|
this.currentSession = null;
|
||||||
|
this.cleanupAudio();
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('ended', (_e: EndEvent) => {
|
||||||
|
this.onCallStateChange('ended');
|
||||||
|
this.currentSession = null;
|
||||||
|
this.cleanupAudio();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session.direction === 'incoming') {
|
||||||
|
this.onCallStateChange('ringing-in', callerNumber);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ua.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
this.hangup();
|
||||||
|
if (this.ua) {
|
||||||
|
this.ua.stop();
|
||||||
|
this.ua = null;
|
||||||
|
}
|
||||||
|
this.cleanupAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
call(phoneNumber: string): void {
|
||||||
|
if (!this.ua || !this.ua.isRegistered()) {
|
||||||
|
throw new Error('SIP not registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = this.config.uri.split('@')[1];
|
||||||
|
const target = `sip:${phoneNumber}@${host}`;
|
||||||
|
|
||||||
|
const options: CallOptions = {
|
||||||
|
mediaConstraints: { audio: true, video: false },
|
||||||
|
pcConfig: {
|
||||||
|
iceServers: this.parseStunServers(this.config.stunServers),
|
||||||
|
iceTransportPolicy: 'all',
|
||||||
|
},
|
||||||
|
rtcOfferConstraints: {
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ua.call(target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
answer(): void {
|
||||||
|
if (this.currentSession && this.currentSession.direction === 'incoming') {
|
||||||
|
this.currentSession.answer({
|
||||||
|
mediaConstraints: { audio: true, video: false },
|
||||||
|
pcConfig: {
|
||||||
|
iceServers: this.parseStunServers(this.config.stunServers),
|
||||||
|
iceTransportPolicy: 'all',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hangup(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.currentSession.terminate();
|
||||||
|
this.currentSession = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mute(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.currentSession.mute({ audio: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unmute(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.currentSession.unmute({ audio: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hold(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.currentSession.hold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unhold(): void {
|
||||||
|
if (this.currentSession) {
|
||||||
|
this.currentSession.unhold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.ua?.isConnected() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRegistered(): boolean {
|
||||||
|
return this.ua?.isRegistered() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupAudio(): void {
|
||||||
|
if (this.audioElement) {
|
||||||
|
this.audioElement.srcObject = null;
|
||||||
|
this.audioElement.remove();
|
||||||
|
this.audioElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseStunServers(stunConfig: string): RTCIceServer[] {
|
||||||
|
const servers: RTCIceServer[] = [];
|
||||||
|
const lines = stunConfig.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split(',');
|
||||||
|
const urls = parts[0].trim();
|
||||||
|
|
||||||
|
if (parts.length === 3) {
|
||||||
|
servers.push({ urls: [urls], username: parts[1].trim(), credential: parts[2].trim() });
|
||||||
|
} else {
|
||||||
|
servers.push({ urls: [urls] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/types/sip.ts
Normal file
11
src/types/sip.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export type SIPConfig = {
|
||||||
|
displayName: string;
|
||||||
|
uri: string;
|
||||||
|
password: string;
|
||||||
|
wsServer: string;
|
||||||
|
stunServers: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'registered' | 'error';
|
||||||
|
|
||||||
|
export type CallState = 'idle' | 'ringing-in' | 'ringing-out' | 'active' | 'ended' | 'failed';
|
||||||
Reference in New Issue
Block a user