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:
@@ -47,7 +47,7 @@ const isFailed = computed(() =>
|
||||
|
||||
// Call source and metadata — all camelCase due to deep transform
|
||||
const isWhatsappCall = computed(() => data.value?.callSource === 'whatsapp');
|
||||
const waCallId = computed(() => data.value?.callId);
|
||||
const callId = computed(() => data.value?.callId);
|
||||
const acceptedBy = computed(() => data.value?.acceptedBy);
|
||||
const durationSeconds = computed(() => data.value?.durationSeconds);
|
||||
const recordingUrl = computed(() => data.value?.recordingUrl);
|
||||
@@ -64,10 +64,19 @@ const formattedDuration = computed(() => {
|
||||
});
|
||||
|
||||
// Show join/accept button logic
|
||||
// WhatsApp: only ringing (peer-to-peer WebRTC — cannot rejoin after accept)
|
||||
// WhatsApp with media server: ringing + in_progress (server-relay supports rejoin)
|
||||
// WhatsApp without media server: only ringing (peer-to-peer WebRTC — cannot rejoin after accept)
|
||||
// Twilio: ringing + in-progress (conference model supports rejoin)
|
||||
const showJoinButton = computed(() => {
|
||||
if (isWhatsappCall.value) {
|
||||
// Server-relay mode enables rejoining in-progress calls
|
||||
const isMediaServerMode = data.value?.mediaServerEnabled;
|
||||
if (isMediaServerMode) {
|
||||
return [
|
||||
VOICE_CALL_STATUS.RINGING,
|
||||
VOICE_CALL_STATUS.IN_PROGRESS,
|
||||
].includes(status.value);
|
||||
}
|
||||
return status.value === VOICE_CALL_STATUS.RINGING;
|
||||
}
|
||||
return [VOICE_CALL_STATUS.RINGING, VOICE_CALL_STATUS.IN_PROGRESS].includes(
|
||||
@@ -133,7 +142,7 @@ const handleJoinCall = async () => {
|
||||
|
||||
try {
|
||||
if (isWhatsappCall.value) {
|
||||
const result = await acceptWhatsappCallById(waCallId.value);
|
||||
const result = await acceptWhatsappCallById(callId.value);
|
||||
if (result?.success && result.call) {
|
||||
router.push({
|
||||
name: 'inbox_conversation',
|
||||
|
||||
Reference in New Issue
Block a user