Skip to main content

Overview

whatsapp-rust supports end-to-end encrypted 1:1 voice calls that interoperate with the official WhatsApp app. The full media path is implemented in pure Rust: mic capture, encoding, E2E-SRTP encryption, relay transport, decryption, decoding, and playout.
Voice calling is behind the optional voip feature flag. The default build is entirely unaffected — none of the codec or relay dependencies are compiled unless you opt in.

Enabling the Feature

The voip feature landed on main with PR #918 and will be included in the next published release. Until then, depend on the git source:
Once a release that includes voip is published, you can switch to the version form:

Answering an Incoming Call

Event::IncomingCall fires for all call-control stanzas (offer, preaccept, accept, reject, terminate, transport, relaylatency). Guard on CallAction::Offer before calling accept() — otherwise non-offer events return CallError::NotAnOffer:
accept(...).start() drives the media plane only (callKey decrypt → relay connect → engine). Sending <preaccept> and <accept> signaling stanzas to the peer is the caller’s responsibility and must happen before or alongside start(). See examples/voip-cli/src/main.rs in the repository for a complete incoming-call handler that includes signaling.

Placing an Outgoing Call

If the callee has a stored trusted-contact token, it’s attached to the offer automatically, and a fresh token is issued to them in the background afterward if the sender-side bucket has rolled over — matching WhatsApp Web’s sendTcToken in StartCall.js. This prevents 463 nacks on calls to privacy-restricted contacts and needs no action from the caller. Group-call initiation doesn’t implement this yet.

Audio I/O

You supply the audio I/O by implementing the AudioSource and AudioSink traits. Both traits are channel-based — the library reads from a Receiver and writes decoded PCM to a Sender. The bundled examples/voip-cli/src/main.rs wires up cpal/PipeWire as a reference. The CLI exposes three subcommands: To test the audio stack locally without a WhatsApp session:
The AudioSource / AudioSink implementation from the example:
If you already have bare async_channel endpoints, you can pass them directly — Receiver<Vec<i16>> implements AudioSource and Sender<Vec<i16>> implements AudioSink out of the box.
The library owns the call key, relay handshake, codec, and crypto. You only provide mic input and speaker output.

Call Handle

start() returns a CallHandle for controlling an active call:

Multi-Device Behavior

The library handles multi-device call scenarios automatically:
  • Companion answering — if another linked device picks up, the library performs a recv-key rekey to that device.
  • Sibling dismiss — if a sibling device declines or answers elsewhere, the call tears down cleanly on this device.
  • Offline missed calls — missed-call surfacing for devices that were offline when the call arrived.

Architecture: CallEngine

The core CallEngine lives in wacore and is sans-IO — it owns no socket, clock, or thread. You feed it relay packets, mic frames, and timer ticks; it emits transmit packets, playout PCM, call events, and the next deadline.
Platform support depends on which crate you use:
  • wacore with features = ["voip"] — pure Rust (MLow codec, SRTP crypto, CallEngine). No FFI. Compiles to WASM and embedded targets (esp32).
  • whatsapp-rust with features = ["voip"] — adds the Tokio async driver, webrtc-rs (DTLS/SCTP), and libopus FFI. This will not compile on wasm32 or espidf — a compile_error! enforces this at build time.

MLow Codec

WhatsApp’s voice codec (“MLow”) is a heavily modified Opus variant. whatsapp-rust includes a pure Rust port — no FFI, no C library. It compiles to WASM and embedded targets alongside the rest of the crate and is pinned by a byte-exact golden roundtrip test. The decoder operates at a single fixed point:

Encryption

Call audio is end-to-end encrypted the WhatsApp way:
  1. The call key arrives over the peer’s Signal session.
  2. E2E-SRTP keys are derived with HKDF + the libsrtp AES-CM KDF.
  3. Audio frames are protected with AES-128-CTR and authenticated with a 4-byte WARP MESSAGE-INTEGRITY tag (HMAC-SHA1).
  4. The SFrame layer wraps the SRTP payload.
On receive, the ROC is estimated per-packet via RFC 3711’s guess-index, then the WARP tag is verified in constant time against that estimate — before the ROC is advanced. Committing an unauthenticated packet’s index would let an on-path relay desync the receiver’s keystream with just a couple of forged packets. A packet that fails authentication is rejected outright and never advances the ROC state, so it can’t cause a persistent decode failure for subsequent legitimate frames. The relay never sees plaintext audio.

Validation

The implementation is tested at multiple levels:
  • Byte-exact golden roundtrip for the MLow codec
  • Known-answer-test vectors for the E2E-SRTP crypto
  • In-tree loopback DTLS/SCTP transport E2E test
  • Live tested end-to-end against the real WhatsApp app over 3G/WiFi/5G

Roadmap

The 1:1 audio path is the foundation. Natural follow-ups tracked upstream:
  • Group calls — the signaling and SFrame/SRTP key management generalize to multi-party; the engine is already participant-aware.
  • Video calls — the DTLS/SCTP/SRTP transport is codec-agnostic; video is a second media stream.
  • Deeper codec coverage — inband FEC, PLC, CNG, and low-bitrate operating points.
  • Embedded demo — the sans-IO core already builds for esp32.

Next Steps