fix(whatsapp-call): stop browser from bypassing media server on inbound

Inbound calls were reaching the media server, DTLS was completing after
the DTLSRoleServer fix, and yet no media flowed — Meta still timed out
at 20s. Tracing the session 182412 showed why: the browser uploaded a
local .webm recording at t+22s. That only happens on the legacy
browser-direct-to-Meta path, which means the browser negotiated WebRTC
straight with Meta instead of waiting for the media server's agent
offer.

Root cause: GET /whatsapp_calls/:id returns Meta's sdp_offer whenever
the call is ringing, regardless of media-server mode. The FE calls
that endpoint from acceptWhatsappCallById when the call isn't in the
incoming-calls store yet (page reload, bubble click). It then sees a
truthy sdpOffer, isServerRelayCall() returns false, and the browser
dials Meta itself. Meta's DTLS was with *our* media server, so the
browser's peer connection never got media and Meta saw silence.

Fix:
- show omits sdp_offer / ice_servers when media_server_enabled and
  returns media_server_enabled as an explicit flag so the FE picks the
  right path.
- broadcast_incoming_call carries the same flag through ActionCable.
- FE isServerRelayCall prefers the explicit flag; only falls back to
  sdpOffer presence when the flag isn't provided.
- acceptWhatsappCallById and the ActionCable handler propagate the
  flag into the call object.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tanmay Deep Sharma
2026-04-21 18:31:20 +07:00
co-authored by Claude Opus 4.7
parent 0bf6a2cee8
commit c6c3ba054e
4 changed files with 19 additions and 4 deletions
@@ -143,9 +143,15 @@ function waitForIceGatheringComplete(pc) {
});
}
// ── Server-relay mode: detect by absence of sdpOffer in incoming call data ──
// ── Server-relay mode detection ──
// Prefer the explicit flag set by Rails whenever it's available (call object
// from GET /whatsapp_calls/:id, inbox serializer, or the incoming ActionCable
// broadcast). Fall back to the presence of sdpOffer so legacy deployments keep
// working when the field isn't set.
function isServerRelayCall(call) {
return !call.sdpOffer;
if (call?.mediaServerEnabled === true) return true;
if (call?.mediaServerEnabled === false) return false;
return !call?.sdpOffer;
}
/**
@@ -290,6 +296,7 @@ export async function acceptWhatsappCallById(callId) {
conversationId: data.conversation_id,
sdpOffer: data.sdp_offer,
iceServers: data.ice_servers,
mediaServerEnabled: data.media_server_enabled,
caller: data.caller,
};
callsStore.addIncomingCall(call);
@@ -226,6 +226,7 @@ class ActionCableConnector extends BaseActionCableConnector {
caller: data.caller,
sdpOffer: data.sdp_offer || null,
iceServers: data.ice_servers || null,
mediaServerEnabled: data.media_server_enabled,
});
};
@@ -14,8 +14,14 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
conversation_id: @call.conversation_id,
inbox_id: @call.inbox_id,
message_id: @call.message_id,
sdp_offer: @call.ringing? ? @call.sdp_offer : nil,
ice_servers: @call.ice_servers,
# In server-relay mode the browser must not talk WebRTC to Meta directly
# — the media server owns that peer connection. Omitting sdp_offer forces
# the FE's isServerRelayCall() check to return true so the accept flow
# waits for the whatsapp_call.agent_offer broadcast from Rails instead of
# negotiating straight with Meta.
sdp_offer: @call.ringing? && !Call.media_server_enabled? ? @call.sdp_offer : nil,
ice_servers: Call.media_server_enabled? ? [] : @call.ice_servers,
media_server_enabled: Call.media_server_enabled?,
caller: caller_info
}
end
@@ -157,6 +157,7 @@ class Whatsapp::IncomingCallService
direction: call.direction_label,
inbox_id: call.inbox_id,
conversation_id: call.conversation_id,
media_server_enabled: Call.media_server_enabled?,
caller: {
name: contact.name,
phone: contact.phone_number,