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
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user