Android System Design Interview Questions
How do voice and video calls work?
Tier: Less commonDifficulty: Hard
Voice and video calling is built on WebRTC, an open standard that gives browsers and mobile apps peer-to-peer real-time media. WebRTC deliberately doesn't solve the whole problem on its own, so two pieces have to be built around it before any call connects, and on Android a third layer sits above it just to make the phone ring.
What I'd clarify first
- Is this one-to-one calling only, or does group calling need to be in scope, since group calls usually need a media server rather than pure peer-to-peer.
- Does a call have to survive the app being backgrounded, the screen locking, or the app not running at all when the call arrives, because that's a Telecom and foreground service question rather than a WebRTC one.
- Does the app need to work through restrictive corporate or carrier networks, which changes how much you have to lean on relay infrastructure versus direct peer connections.
- Is call quality adaptation to a poor network a requirement, or is a fixed quality level acceptable.
The two problems WebRTC doesn't solve
- Signaling. Before two devices can exchange media they have to exchange setup information, session descriptions, network candidates, who's calling whom. WebRTC intentionally leaves this out, so you build it, typically over the same WebSocket infrastructure a chat feature already has, since it's just another kind of real-time message passing.
- NAT traversal. Most phones sit behind a carrier or router NAT with no public address a peer could dial directly. STUN and TURN solve it. STUN asks a public server what your actual public address is, which is often enough for two peers to reach each other directly. When it isn't, common on strict corporate networks, a TURN server relays the media between the two peers, which costs latency and real bandwidth because the traffic is no longer taking the direct path.
Encryption is worth naming as the one thing WebRTC does solve for you. Once a path is picked, a DTLS handshake runs over that same path and derives the keys for SRTP, which encrypts every media packet. It's mandatory in the spec, there's no unencrypted mode to fall into by mistake. What that protects is the hop between the two endpoints, so on a group call the media server sits inside the boundary and can see the media unless you add end-to-end encryption on top of it.
How a call connects
- The caller creates an offer, a description of what it can send and receive, and pushes it over the signaling channel.
- The callee's device rings, and on accept it sends back an answer in the same form.
- Both sides gather ICE candidates at once, the local address, the public address STUN reports, and a TURN relay address, and trickle each one across the signaling channel as it's found rather than waiting for the whole set.
- Both sides run connectivity checks on the candidate pairs concurrently and settle on the best pair that works, preferring a direct path over a relayed one.
- DTLS handshakes over that path and hands SRTP its keys.
- Audio and video flow on the media path, and the signaling channel drops back to carrying call control only, mute state, hang up, and so on.
What Android adds
- Ringing when the app isn't running. The invite arrives as a high priority FCM data message, which is what gets delivered promptly even in Doze. Your handler wakes, posts the incoming call UI immediately, and only then starts any signaling. A normal priority message can be held until the next maintenance window, which for a call is the same as never.
- The incoming call UI.
Notification.CallStylegives you the ringing treatment with answer and decline actions the system understands. To take over a locked screen you attach a full screen intent, and from Android 14 theUSE_FULL_SCREEN_INTENTpermission is granted by default only to calling and alarm apps, so checkcanUseFullScreenIntentand degrade to a heads-up notification rather than assuming you have it. - Being a real call. Register with the Telecom framework, these days through
androidx.core.telecomand itsCallsManagerrather than writing aConnectionServiceyourself, and declareMANAGE_OWN_CALLS. That's what makes an incoming carrier call put yours on hold instead of both playing at once, and what makes the answer button on a Bluetooth headset, a watch or Android Auto do anything. - Staying alive for the duration. A foreground service typed
microphone, pluscameraon a video call, is what keeps the process running and keeps capture permitted once the app leaves the screen. Android 14 requires the type to be declared and the matching runtime permission actually granted. You also take audio focus so music pauses, and you let Telecom own the audio route, earpiece, speaker or headset, rather than drivingAudioManageryourself.
Tradeoffs I'd call out
- Peer-to-peer vs a media server. Direct peer-to-peer gives the lowest latency and costs the platform nothing in relay bandwidth, but every peer sending its stream to every other peer stops scaling past three or four participants. Past that you need a media server, and there are two kinds. An SFU takes one stream from each participant and forwards each one on unchanged, cheap on server CPU and heavier on the receiver's bandwidth and decode load. An MCU decodes everything and mixes it into a single stream per participant, the reverse trade, expensive on the server and easy on the client. SFU is the default answer now, MCU shows up where clients are weak or the output has to be one stream.
- Preferring the direct path vs going straight to TURN. This isn't a serial timeout, which is the thing people get wrong. ICE gathers host, STUN and TURN candidates at the same time and checks the pairs concurrently, so the cost of preferring direct is that the calls which end up needing the relay connect slightly later, not that every call waits for a direct attempt to fail first. Forcing TURN on everything is more predictable, at the price of paying relay bandwidth for calls that would have been free.
- Fixed call quality vs adaptive bitrate. A fixed resolution and bitrate is simpler to reason about, but on a degrading connection it freezes or drops. Adaptive bitrate, which WebRTC does natively, walks resolution and frame rate down smoothly as bandwidth falls. Opus on the audio side already adapts its own rate, and on video you're picking between VP8 and H.264, which every endpoint has, and VP9 or AV1, which cost more CPU for a better picture at the same bitrate.
What breaks at scale and on a poor connection
At scale the cost center isn't signaling, it's TURN relay bandwidth. Every call that can't connect peer-to-peer costs the platform real, ongoing bandwidth for its whole duration, which is why call quality and network diagnostics matter for capacity planning and not just for the person on the call. On a poor connection, adaptive bitrate carries you a long way, and when it isn't enough the right move is an automatic drop from video to audio-only. A call that degrades to audio and keeps going is a far better outcome than one that holds out for video quality and freezes.
Read more Build a calling app (opens in a new tab)Foreground service types (opens in a new tab)
Watch