@zapo-js/voip is the official voice-calling plugin for zapo-js. It rides on the plugin system, exposes a WaVoipCoordinator at client.voip, and emits voip_* events on the host client.
Install
@roamhq/wrtc(>= 0.10.0) — provides SCTP for the relay transport.libmlow-wasm(^0.1.1) — WhatsApp’s Opus profile, compiled to WebAssembly. No native build step.zapo-js(^1.0.0) — the host client.
ffmpeg binary on PATH:
loadAudiodecodes the audio file viaffmpeg.- Streaming audio with
feedLiveAudiofrom a file likewise needsffmpegto produce 16 kHz monoFloat32Arraychunks.
Wire the plugin
AddvoipPlugin() to WaClientOptions.plugins. The coordinator appears at client.voip and the voip_* events become available on client.on.
Plugin options
Maximum simultaneous non-ended calls (ringing, connecting, or active). Increase to enable parallel multi-call. Outgoing
startCall rejects, and incoming calls arrive with acceptBlocked: true, when the limit is hit.Minimum log level for the (chatty) VoIP plugin. Defaults to the host client’s level; set it to cap diagnostics independently — for example
'warn' to keep VoIP noise out of a trace host logger.The coordinator surface
Placing a call
Bare or device JID to call.
Flag the call as video in the signaling stanzas. Video media encode/transport is not implemented; only audio media flows.
Audio file to preload and play once the call connects (needs
ffmpeg). Equivalent to calling loadAudio after the call goes active.Explicit peer device JIDs to ring; omit to resolve them automatically.
startCall resolves with the new call id once the offer is sent. Progress then arrives via voip_call_state. It rejects when you’re at the maxConcurrentCalls limit or if the offer fails to send.
Answering and ending
acceptCall(callId)— accept a ringing incoming call. Throws ifcallIdis unknown or not in an acceptable state.rejectCall(callId, reason?)— reject a ringing incoming call (default reasonDeclined). Sends the reject stanza, then tears the call down.endCall(callId, reason?)— end an active or connecting call (default reasonUserEnded). No-op if the call is unknown or already ended.
Preloaded outbound audio
audioPath via ffmpeg and queues it as the outbound audio, played once the call is active. Throws if the file is missing or ffmpeg is unavailable.
For an unbounded or live source (TTS stream, ongoing capture) use external audio mode instead — see below.
Mute
Live outbound audio (external mode)
For sources you can’t preload — streaming TTS, real-time capture — switch the call into external audio mode and pump samples in as they arrive. The plugin buffers them through a bounded jitter buffer and watermarks the buffer so a respectful producer never loses audio.Snapshots
CallInfo captures everything you need to render UI: callId, peerJid, direction, mediaType, createdAt, callerPn, plus a stateData object (state, audioMuted, videoOff, connectedAt, endReason, durationSecs, acceptBlocked, …) and derived getters isInitiator, isActive, isRinging, isEnded, canAccept, canReject, isAcceptBlocked.
Events
AddvoipPlugin() to plugins and these events become available on client.on. Without the plugin they don’t exist on the type — see Plugins.
Enums
Allenum values are lowercase strings — use the enum constants to avoid typos.
CallState
CallDirection
CallMediaType
EndCallReason
Audio format
- Sample rate: 16 kHz.
- Channels: mono.
- Sample format:
Float32Arrayin[-1.0, 1.0]. - Codec on the wire: WhatsApp’s Opus profile, encoded via
libmlow-wasm. The RTP payload type is120(PayloadType.WhatsAppOpus).
voip_call_inbound_audio) and outbound (feedLiveAudio) are this same Float32Array shape — keep your buffers in this format and you can route them straight through.
Worked example
A minimal incoming auto-accept that records inbound PCM into an in-memory buffer:examples/voip-example.ts in the zapo source tree.
See also
Plugin system
How
voipPlugin() plugs into WaClient, and how to author your own.Events
The host client’s event surface;
voip_* events sit alongside the built-ins.Credits
The VoIP plugin was built by:- @vinikjkkj
- @edgardmessias — Edgard Lorraine Messias
- @w3nder — Wender Teixeira
