This system is closer to a B2B institutional device-fleet scenario than a consumer calling app. In healthcare, eldercare, and similar facilities, many shared Android devices may need to stay online for long periods, recover without local troubleshooting, and keep calls usable in noisy rooms where pickup distance and speech quality both matter.

The gateway integration is easier to maintain when the boundary is explicit: Go/gRPC owns signaling and control, WebRTC owns media, and the Android client owns lifecycle, permissions, capture, rendering, and UI state.

The sample ports, IDs, and paths in this article are placeholders. The reusable part is the cross-boundary contract between the Go service, RTC Gateway, Android client, and WebRTC PeerConnection.

Go gRPC signaling and Android WebRTC call lifecycle

Problem Context

If every call concern is pushed into an Android page, the code becomes hard to reason about. The page has to manage permissions, camera, microphone, remote views, buttons, registration, call control, hold, hangup, multi-party joins, and recovery. Once signaling and media state are mixed together, production debugging becomes slow.

I prefer splitting the system into three layers:

  • RTC Gateway: device registration, call control, callbacks, SDP/ICE forwarding.
  • Android client: gateway startup, callback handling, UI lifecycle, and local media resources.
  • WebRTC PeerConnection: offer/answer, ICE candidates, track events, and connection state.

This boundary makes failure triage more direct: registration failures are signaling problems; media failures need ICE and selected candidate pair; UI drift needs callback and resource lifecycle checks.

Gateway Startup

The Android client starts by building a gateway configuration and launching the gateway thread.

grpcSocket = GrpcSocket.Builder()
    .setIsMultil(false)
    .setDebug(BuildConfig.DEBUG)
    .setTest(false)
    .setEnableHttp(BuildConfig.DEBUG)
    .setFileDir(FileUtil.getContactFileDir())
    .build()

grpcSocket.startSocketThread()

The boundary is more important than the builder itself. The Android side passes runtime parameters. The gateway owns how those parameters become internal running state.

If the gateway listens on a local port such as 18086, treat it as a sample. Real deployments should move ports, file paths, HTTP switches, and debug flags into explicit environment configuration.

RTC Gateway startup example

Device Registration

After startup, the gateway registers the local device and other peers. A registration request usually includes server address, client address, peer id, display name, identity, and client type.

Gateway.instance().register(
    serverIp,
    clientIp,
    peerId,
    displayName,
    identity,
    clientType
);

Examples must not expose real IPs, phone numbers, device identifiers, or internal identity fields. Treat peerId as a sample id such as 1001.

Registration needs state consistency. A local success does not mean the gateway, signaling service, and other peers agree. At minimum, record:

  • peer id.
  • registration state.
  • registration timestamp.
  • server confirmation timestamp.
  • failure reason.
  • next retry time.

Those fields help debug “can call out but cannot receive calls”, “some devices look offline”, and “the page exited but the gateway still thinks the peer is online”.

Callback Design

The RTC Gateway and Android client need a small but explicit callback surface.

Runtime Callbacks

  • started.
  • error.
  • non-fatal warning.

These callbacks describe gateway availability. They should not be tied to one call.

Call Lifecycle Callbacks

  • open one-to-one call page.
  • open group call page.
  • answer.
  • hold.
  • hangup.

These callbacks are tied to Android UI and media resource lifecycle. The client must know when to request camera/microphone and when to release them.

WebRTC Negotiation Callbacks

  • received SDP.
  • received ICE candidate.
  • track added or removed.

These callbacks belong to the media negotiation path. They should be decoupled from UI state as much as possible, otherwise a page transition can drop SDP or candidate handling.

WebRTC Client Boundary

Android usually implements SdpObserver and PeerConnection.Observer. Debug logs should cover both interfaces.

SdpObserver answers whether offer/answer creation and setting succeeded. PeerConnection.Observer exposes ICE, connection state, remote tracks, and data channel events.

The first states I check are:

onIceConnectionChange
onConnectionChange
onIceGatheringChange
onAddTrack
onRemoveTrack

onIceConnectionChange and onConnectionChange are not the same. The former is closer to the ICE path; the latter is the overall PeerConnection state. A common mistake is staring at timeout logs while ignoring the WebRTC state machine. Media debugging should come back to ICE state, PeerConnection state, and selected candidate pair.

RTCConfiguration also deserves explicit review:

PeerConnection.RTCConfiguration rtcConfig =
    new PeerConnection.RTCConfiguration(iceServers);

rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
rtcConfig.keyType = PeerConnection.KeyType.ECDSA;

These settings affect candidate type, media bundling, security negotiation, and compatibility. They should be validated against the real network topology before release.

One-to-One Call Flow

A one-to-one call can be split into clear stages:

  1. Caller opens the call page.
  2. Android initializes WebRTC and requests camera/microphone.
  3. The client creates PeerConnection and adds local media.
  4. The client tells the gateway it is ready.
  5. The gateway or signaling service creates an offer.
  6. Both sides exchange SDP and ICE candidates.
  7. ICE enters connected.
  8. Remote track arrives and media is rendered.

There is often an async wait between opening the page and initializing WebRTC. If a lock or timeout is used, two boundaries matter:

  • The lock must release automatically if the page does not respond.
  • The client must explicitly notify the gateway after the page is ready.

Otherwise one UI failure can block the gateway state for later calls.

Group Call Flow

Group calls add resource reuse and state isolation.

The first participant may initialize global WebRTC and local media resources. Later participants should not reinitialize global media. They should add per-peer display state and wait for remote tracks.

Rules that prevent common defects:

  • each peer has its own call state.
  • each remote track maps to one UI container.
  • one participant leaving does not close every view.
  • onRemoveTrack removes only the matching participant.
  • global media resources are released only after all participants leave.

Without that separation, one hangup can close every remote view, or a departed participant can leave stale video on screen.

Hold, Hangup, and Broadcast

hold, hangup, and broadcast are control-plane events. They should not be mixed with media implementation details.

hold should make both sides enter a consistent UI and media state. It should be clear whether the local side, remote side, or server requested the hold.

hangup is about idempotent cleanup. Android must release camera, microphone, PeerConnection, local renderers, and remote renderers safely. Network failures and user actions may trigger hangup at the same time.

broadcast is useful for control messages, not sensitive data. Public examples should not expose internal fields.

Contract and Logs

For a Go + Android WebRTC boundary, logs need shared identifiers and stage names. At minimum, include:

  • call id.
  • peer id.
  • action.
  • local state.
  • remote state.
  • signaling state.
  • ICE state.
  • selected candidate pair.
  • timestamp with a consistent unit.

Failures should be separated by stage: request accepted, peer connection created, local SDP created, remote SDP applied, ICE connected, first media received, first media rendered. Stop at the first divergence instead of explaining later noise.

Takeaway

A WebRTC integration cannot be judged only by whether Android API calls return success. The boundary to verify is:

  • Go/gRPC signaling owns session control and event delivery.
  • Android owns lifecycle and resource management.
  • WebRTC owns media negotiation and transport.
  • logs and state machines connect the three layers.

When that boundary is unclear, every incident looks like a network issue, device issue, or SDK issue. Once the boundary is explicit, the debugging order is much shorter: confirm signaling state, then ICE/PeerConnection state, then UI and resource cleanup.

Open to Go gRPC signaling, Android WebRTC, RTC Gateway, PeerConnection state machine, and weak-network reliability discussions.