import JsSIP from 'jssip'; // Lightweight SIP client for supervisor barge sessions. // Separate from the agent's sip-client.ts — different lifecycle. // Modeled on Ozonetel's kSip utility (CA-Admin/.../utils/ksip.tsx). // // DTMF mode mapping (from Ozonetel CA-Admin BargeinDrawerSip.tsx): // "4" → Listen (supervisor hears all, nobody hears supervisor) // "5" → Whisper/Training (agent hears supervisor, patient doesn't) // "6" → Barge (both hear supervisor) type EventCallback = (...args: any[]) => void; type SupervisorSipEvent = | 'registered' | 'registrationFailed' | 'callReceived' | 'callConnected' | 'callEnded' | 'callFailed'; type SupervisorSipConfig = { domain: string; port: string; number: string; password: string; }; class SupervisorSipClient { private ua: JsSIP.UA | null = null; private session: JsSIP.RTCSession | null = null; private config: SupervisorSipConfig | null = null; private listeners = new Map>(); private audioElement: HTMLAudioElement | null = null; init(config: SupervisorSipConfig): void { this.config = config; this.cleanup(); // Hidden audio element for remote call audio this.audioElement = document.createElement('audio'); this.audioElement.id = 'supervisor-remote-audio'; this.audioElement.autoplay = true; this.audioElement.setAttribute('playsinline', ''); document.body.appendChild(this.audioElement); const socketUrl = `wss://${config.domain}:${config.port}`; const socket = new JsSIP.WebSocketInterface(socketUrl); this.ua = new JsSIP.UA({ sockets: [socket], uri: `sip:${config.number}@${config.domain}`, password: config.password, registrar_server: `sip:${config.domain}`, authorization_user: config.number, session_timers: false, register: false, }); this.ua.on('registered', () => { console.log('[SupervisorSIP] Registered'); this.emit('registered'); }); this.ua.on('registrationFailed', (e: any) => { console.error('[SupervisorSIP] Registration failed:', e?.cause); this.emit('registrationFailed', e?.cause); }); this.ua.on('newRTCSession', (data: any) => { const rtcSession = data.session as JsSIP.RTCSession; if (rtcSession.direction !== 'incoming') return; console.log('[SupervisorSIP] Incoming call — auto-answering'); this.session = rtcSession; this.emit('callReceived'); rtcSession.on('accepted', () => { console.log('[SupervisorSIP] Call accepted'); this.emit('callConnected'); }); rtcSession.on('confirmed', () => { // Attach remote audio stream const connection = rtcSession.connection; if (connection && this.audioElement) { // Modern browsers: track event connection.addEventListener('track', (event: RTCTrackEvent) => { if (event.streams[0] && this.audioElement) { this.audioElement.srcObject = event.streams[0]; } }); // Fallback: getRemoteStreams (older browsers/JsSIP versions) const remoteStreams = (connection as any).getRemoteStreams?.(); if (remoteStreams?.[0] && this.audioElement) { this.audioElement.srcObject = remoteStreams[0]; } } }); rtcSession.on('ended', () => { console.log('[SupervisorSIP] Call ended'); this.session = null; this.emit('callEnded'); }); rtcSession.on('failed', (e: any) => { console.error('[SupervisorSIP] Call failed:', e?.cause); this.session = null; this.emit('callFailed', e?.cause); }); // Auto-answer with audio rtcSession.answer({ mediaConstraints: { audio: true, video: false }, }); }); this.ua.start(); } register(): void { this.ua?.register(); } isRegistered(): boolean { return this.ua?.isRegistered() ?? false; } isCallActive(): boolean { return this.session?.isEstablished() ?? false; } sendDTMF(digit: string): void { if (!this.session?.isEstablished()) { console.warn('[SupervisorSIP] Cannot send DTMF — no active session'); return; } console.log(`[SupervisorSIP] Sending DTMF: ${digit}`); this.session.sendDTMF(digit, { duration: 160, interToneGap: 1200, }); } hangup(): void { if (this.session) { try { this.session.terminate(); } catch { // Session may already be ended } this.session = null; } } close(): void { this.hangup(); if (this.ua) { try { this.ua.unregister({ all: true }); this.ua.stop(); } catch { // UA may already be stopped } this.ua = null; } this.cleanup(); this.listeners.clear(); } on(event: SupervisorSipEvent, callback: EventCallback): void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); } off(event: SupervisorSipEvent, callback: EventCallback): void { this.listeners.get(event)?.delete(callback); } private emit(event: string, ...args: any[]): void { this.listeners.get(event)?.forEach(cb => { try { cb(...args); } catch (e) { console.error(`[SupervisorSIP] Event error [${event}]:`, e); } }); } private cleanup(): void { if (this.audioElement) { this.audioElement.srcObject = null; this.audioElement.remove(); this.audioElement = null; } } } export const supervisorSip = new SupervisorSipClient();