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
79 lines
2.4 KiB
JavaScript
79 lines
2.4 KiB
JavaScript
/* global axios */
|
|
import ApiClient from './ApiClient';
|
|
|
|
class WhatsappCallsAPI extends ApiClient {
|
|
constructor() {
|
|
super('whatsapp_calls', { accountScoped: true });
|
|
}
|
|
|
|
show(callId) {
|
|
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) {
|
|
const body = sdpAnswer ? { sdp_answer: sdpAnswer } : {};
|
|
return axios.post(`${this.url}/${callId}/accept`, body);
|
|
}
|
|
|
|
reject(callId) {
|
|
return axios.post(`${this.url}/${callId}/reject`);
|
|
}
|
|
|
|
terminate(callId) {
|
|
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) {
|
|
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`);
|
|
return axios.post(`${this.url}/${callId}/upload_recording`, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
}
|
|
}
|
|
|
|
export default new WhatsappCallsAPI();
|