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:
@@ -10,10 +10,11 @@ class WhatsappCallsAPI extends ApiClient {
|
||||
return axios.get(`${this.url}/${callId}`);
|
||||
}
|
||||
|
||||
// Accept a ringing call. sdpAnswer is optional — omitted in server-relay mode
|
||||
// where the media server handles WebRTC negotiation.
|
||||
accept(callId, sdpAnswer) {
|
||||
return axios.post(`${this.url}/${callId}/accept`, {
|
||||
sdp_answer: sdpAnswer,
|
||||
});
|
||||
const body = sdpAnswer ? { sdp_answer: sdpAnswer } : {};
|
||||
return axios.post(`${this.url}/${callId}/accept`, body);
|
||||
}
|
||||
|
||||
reject(callId) {
|
||||
@@ -24,13 +25,47 @@ class WhatsappCallsAPI extends ApiClient {
|
||||
return axios.post(`${this.url}/${callId}/terminate`);
|
||||
}
|
||||
|
||||
// Initiate an outbound call. sdpOffer is optional — omitted in server-relay
|
||||
// mode where the media server generates the SDP offer for Meta.
|
||||
initiate(conversationId, sdpOffer) {
|
||||
return axios.post(`${this.url}/initiate`, {
|
||||
conversation_id: conversationId,
|
||||
sdp_offer: sdpOffer,
|
||||
const body = { conversation_id: conversationId };
|
||||
if (sdpOffer) body.sdp_offer = sdpOffer;
|
||||
return axios.post(`${this.url}/initiate`, body);
|
||||
}
|
||||
|
||||
// Send the agent's SDP answer for the Peer B connection (server-relay mode).
|
||||
agentAnswer(callId, sdpAnswer) {
|
||||
return axios.post(`${this.url}/${callId}/agent_answer`, {
|
||||
sdp_answer: sdpAnswer,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the current agent's active call (if any). Used for reconnection on page load.
|
||||
active() {
|
||||
return axios.get(`${this.url}/active`);
|
||||
}
|
||||
|
||||
// Reconnect to an active call after page reload. Server creates a new Peer B
|
||||
// and returns a fresh SDP offer via ActionCable.
|
||||
reconnect(callId) {
|
||||
return axios.post(`${this.url}/${callId}/reconnect`);
|
||||
}
|
||||
|
||||
// Join an existing call as a supervisor (listen-only by default).
|
||||
join(callId, role = 'listen_only') {
|
||||
return axios.post(`${this.url}/${callId}/join`, { role });
|
||||
}
|
||||
|
||||
// Play an audio file to the caller via the media server.
|
||||
playAudio(callId, { filePath, mode = 'replace', loop = false }) {
|
||||
return axios.post(`${this.url}/${callId}/play_audio`, {
|
||||
file_path: filePath,
|
||||
mode,
|
||||
loop,
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy: upload browser-side recording. Deprecated when media server is enabled.
|
||||
uploadRecording(callId, blob) {
|
||||
const formData = new FormData();
|
||||
formData.append('recording', blob, `call-${callId}.webm`);
|
||||
|
||||
Reference in New Issue
Block a user