Files
chatwoot/app/javascript/dashboard/composables/useCallReconnection.js
T
tds-1androot 5e5dc21f2f feat(whatsapp-call): add server-side WebRTC media server for call persistence
Implements a Pion Go B2BUA media server sidecar that sits between Meta's
WhatsApp Cloud API and the agent's browser, enabling call persistence
across page reloads, server-side recording, multi-participant support,
audio injection, and AI integration readiness.

Go Media Server (enterprise/media-server/):
- Pion WebRTC v4 B2BUA with Peer A (Meta) and Peer B (Agent) connections
- Real-time OGG/Opus recording with crash recovery
- Audio bridge with multi-peer fan-out and AudioConsumer plugin interface
- Audio injection from OGG files with loop support for hold music
- Session manager with graceful shutdown and orphaned recording recovery
- HTTP API with Bearer token auth, health checks, and metrics

Rails Integration:
- Whatsapp::MediaServerClient HTTP client for Go sidecar communication
- Dual-mode CallService: legacy browser-direct and server-relay paths
- Media server callback controller for agent disconnect/recording/terminate
- CallRecordingFetchJob: downloads OGG from Go server to ActiveStorage/S3
- CallCleanupJob: sweeps stale ringing and in-progress calls
- New endpoints: active, agent_answer, reconnect, join, play_audio
- DB migration: media_session_id column with indexes

Frontend:
- Dual-mode composable auto-detecting legacy vs server-relay
- handleAgentOffer() for receiving SDP from media server
- useCallReconnection composable for page reload recovery
- Removed terminateCallOnUnload in server-relay mode
- Simplified outbound call flow in ConversationHeader
- New ActionCable event: whatsapp_call.agent_offer

Documentation:
- Server-side WebRTC architecture spec (1505 lines)
- Implementation plan with 119 trackable checklist items
- Feature spec, PR breakdown, and relay architecture docs
2026-04-21 04:47:53 +00:00

64 lines
2.4 KiB
JavaScript

import { onMounted, computed } from 'vue';
import { useWhatsappCallsStore } from 'dashboard/stores/whatsappCalls';
import WhatsappCallsAPI from 'dashboard/api/whatsappCalls';
/**
* Checks for an active WhatsApp call on page load and reconnects if found.
* This handles the server-relay scenario where the call persists on the media
* server even after the agent's browser reloads.
*
* NOTE: This composable intentionally does NOT call useWhatsappCallSession()
* to avoid creating duplicate side effects (beforeunload handlers, cleanup
* callbacks, timers). The WhatsappCallWidget owns the useWhatsappCallSession
* instance. This composable only sets store state and triggers the reconnect
* API call — the actual WebRTC setup happens when the ActionCable agent_offer
* event arrives and is handled by handleAgentOffer.
*
* Usage: call `useCallReconnection()` in the app-level layout component that
* mounts once on page load.
*/
export function useCallReconnection() {
const callsStore = useWhatsappCallsStore();
const isReconnecting = computed(() => callsStore.isReconnecting);
onMounted(async () => {
// Skip if there's already an active or incoming call in the store
if (callsStore.hasActiveCall || callsStore.hasIncomingCall) return;
try {
const { data } = await WhatsappCallsAPI.active();
if (!data?.call) return;
const activeCallData = data.call;
callsStore.setReconnecting(true);
callsStore.setActiveCall({
id: activeCallData.id,
callId: activeCallData.call_id,
direction: activeCallData.direction,
conversationId: activeCallData.conversation_id,
status: 'reconnecting',
serverRelay: true,
caller: activeCallData.caller,
});
// Set timer offset so the timer resumes from the correct elapsed time
if (activeCallData.elapsed_seconds) {
callsStore.setTimerOffset(activeCallData.elapsed_seconds);
}
// Tell the server to create a new Peer B and send us a fresh SDP offer.
// The server will broadcast whatsapp_call.agent_offer via ActionCable,
// which is handled by handleAgentOffer in actionCable.js.
await WhatsappCallsAPI.reconnect(activeCallData.id);
} catch {
// No active call or API/reconnect error — clear state and silent fail.
// clearActiveCall() also resets isReconnecting and callTimerOffset.
callsStore.clearActiveCall();
}
});
return { isReconnecting };
}