Files
chatwoot/app/javascript/dashboard/api/whatsappCalls.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

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();