Using Trickle ICE
Tip
PJSUA-LIB readers — symbol equivalents are listed at the bottom of this page.
Overview
ICE establishes a media path by gathering local candidates (host, STUN-derived server-reflexive, TURN-derived relayed) and probing them pairwise against the peer’s candidates. In the classic model (RFC 8445) each side must finish gathering before signalling begins, so the call setup blocks for the slowest STUN/TURN response.
Trickle ICE (RFC 8838) lets candidates be conveyed as soon as they
appear and connectivity checks start in parallel. The first
“initial offer” carries whatever is already known (typically just the
host candidates) and later candidates are trickled to the peer via
out-of-band signalling. In SIP that vehicle is SIP INFO messages with
application/trickle-ice-sdpfrag payloads, per RFC 8840.
The win is measurable for calls that include a relayed (TURN) candidate, where the allocation handshake otherwise dominates setup latency — call ringing can start before TURN has finished allocating.
For non-trickled ICE setup latency, see Using ICE in non-SIP Applications (the Negotiation time section).
Half trickle vs full trickle
Trickle ICE has two operating modes, defined in
pj_ice_sess_trickle:
PJ_ICE_SESS_TRICKLE_HALFInteroperable mode for when peer support is unknown at session start. As initiator, all local candidates are gathered before sending the initial offer (i.e. behaves like regular ICE on the wire) but the offer advertises trickle support so the answerer may trickle its side. As answerer, the agent trickles only when the offer indicates trickle support; otherwise it falls back to regular ICE.
PJ_ICE_SESS_TRICKLE_FULLUse only when peer trickle support is known in advance. The initiator sends an offer with whatever candidates are gathered so far (commonly just host) and trickles the rest. PJSIP does not probe peer capability — discovering it is the application’s responsibility.
When in doubt, use half. Both modes still satisfy the regular ICE state machine.
Aggressive nomination is disabled
Trickle ICE and aggressive nomination are mutually exclusive
(pj_ice_sess_options::aggressive doxygen). Enabling
trickle automatically disables aggressive nomination —
regular (controlled) nomination is used instead. The pjsua CLI does
this for you when --ice-trickle is set; for PJSUA / PJSUA2
applications setting iceTrickle is sufficient and no extra step
is required.
Enabling Trickle ICE
ICE itself must already be enabled — trickle is a mode of ICE, not a standalone feature. The relevant switch lives on the account NAT configuration.
PJSUA2
Set pj::AccountNatConfig::iceTrickle on the account
configuration before the account is created or modified:
AccountConfig acfg;
// ... existing config (id, server, credentials, etc.) ...
acfg.natConfig.iceEnabled = true;
acfg.natConfig.iceTrickle = PJ_ICE_SESS_TRICKLE_HALF;
try {
account.create(acfg); // or account.modify(acfg);
} catch(Error& err) {
}
The field defaults to PJ_ICE_SESS_TRICKLE_DISABLED.
PJSUA-LIB
Trickle can be configured globally (default for all accounts) or overridden per account.
Globally — set the trickle field of
pjsua_media_config::ice_opt (an instance of
pj_ice_sess_options) on the media configuration passed
to pjsua_init():
pjsua_media_config med_cfg;
pjsua_media_config_default(&med_cfg);
med_cfg.enable_ice = PJ_TRUE;
med_cfg.ice_opt.trickle = PJ_ICE_SESS_TRICKLE_HALF;
pjsua_init(NULL, NULL, &med_cfg);
Per account — opt out of the global ICE config and set the field explicitly:
pjsua_acc_config acc_cfg;
pjsua_acc_config_default(&acc_cfg);
// ... id, registration URI, credentials, etc. ...
acc_cfg.ice_cfg_use = PJSUA_ICE_CONFIG_USE_CUSTOM;
acc_cfg.ice_cfg.enable_ice = PJ_TRUE;
acc_cfg.ice_cfg.ice_opt.trickle = PJ_ICE_SESS_TRICKLE_FULL;
pjsua CLI
Add --ice-trickle=N where N is 0 (disabled), 1 (half),
or 2 (full):
$ ./pjsua --use-ice --ice-trickle=1 \
--stun-srv stun.example.org \
--turn-srv turn.example.org \
--turn-user [user] --turn-passwd ***
Aggressive nomination is automatically disabled when
--ice-trickle is non-zero.
How candidates are conveyed
The initial offer
When trickle is enabled, the SDP carried in the initial offer/answer contains:
a=ice-options:trickle— signals trickle support to the peera=ice-ufrag:/a=ice-pwd:— credentialswhatever
a=candidate:lines are already gathered (in full trickle mode this is often only the host candidates)a=end-of-candidates— present only when local gathering has already finished (half trickle initiator)a=mid:— media identifier; required by RFC 8840 because SDP fragments carried later in SIP INFO need to be associated with a specific m-line
PJSIP also adds trickle-ice to the Supported SIP header
(see pjsip/src/pjsua-lib/pjsua_call.c — the
str_trickle_ice literal).
Subsequent candidates: SIP INFO
Late-arriving candidates (STUN srflx after the STUN response, TURN relay after the allocation completes) are encoded as an SDP fragment and sent in a SIP INFO message:
INFO sip:peer@example.com SIP/2.0
Content-Type: application/trickle-ice-sdpfrag
...
<SDP fragment with new candidates, mid, ufrag, pwd>
The fragment contains only the candidates added since the last
conveyance (tracked internally via
pjmedia_ice_trickle_has_new_cand()) plus the a=mid,
a=ice-ufrag and a=ice-pwd lines and, when local gathering
completes, the a=end-of-candidates marker.
INFO messages are batched on a short timer
(PJSUA_TRICKLE_ICE_NEW_CAND_CHECK_INTERVAL, default
100 ms) so a burst of candidate-ready events produces one INFO, not
several.
Provisional responses (18x) before INFO
When the dialog is not yet established (i.e. before the final response) trickling on the answerer side rides 18x provisional responses with SDP. PJSIP retransmits these provisional responses (exponential backoff, capped at 6 attempts) until either the dialog is established (so SIP INFO becomes available) or trickling completes.
Detecting peer trickle support
PJSIP doesn’t actively probe peer capability before negotiation. Application-level discovery is done by inspecting the peer’s:
Supported: trickle-iceheader in initial signalling, ora=ice-options:trickleattribute in a received SDP
For greenfield endpoint-to-endpoint setups where peer support is guaranteed, full trickle is appropriate. For mixed fleets — or anything reaching PSTN, gateways, or legacy PBXs — start with half trickle.
Programmatic candidate notifications
Applications that want to observe candidate gathering progress (for
UI feedback, logging, or custom signalling outside the PJSIP-managed
SDP flow) can hook
pjmedia_ice_cb::on_new_candidate on the underlying
PJMEDIA ICE transport, which forwards
pj_ice_strans_cb::on_new_candidate from PJNATH. The
callback fires for each newly resolved srflx/relayed candidate and
once more with end_of_cand=PJ_TRUE when gathering is complete.
Most PJSIP applications don’t need this — the PJSIP-LIB / PJSUA2 SIP-side wiring delivers candidates automatically.
Debugging
A few markers help when call setup with trickle isn’t working as expected:
Initial SDP lacks
a=ice-options:trickle— trickle is disabled or the global setting wasn’t picked up. Verifyice_cfg_useisPJSUA_ICE_CONFIG_USE_CUSTOMif you set it per account.Peer ignores trickled INFO — check the peer accepts
application/trickle-ice-sdpfragand observes thetrickle-iceSupportedheader. Some intermediaries strip unknownSupportedvalues.No
a=end-of-candidatesever sent — gathering is hanging on a STUN or TURN server. Check the server is reachable; the ICE log at level 4 prints per-candidate progress (look forTrickleandice tricklelines in pjsip/src/pjsua-lib/pjsua_call.c).Aggressive nomination warnings — harmless once trickle is on, but they indicate the previously-set aggressive flag was overridden by trickle (expected behaviour).
Interaction with other ICE features
TURN TCP/TLS — orthogonal. Trickle controls when candidates are conveyed; the TURN connection type controls what relayed candidate is allocated. See TURN over TCP and TLS.
Manual host candidates — manual candidates are added during ICE initialisation alongside auto-detected ones, so they appear in the initial offer with the auto-detected hosts. Trickle only affects the srflx/relayed candidates that arrive later. See Manual ICE Host Candidates.
Negotiation timeout — even with trickle, ICE will report failure (with default settings) after roughly 7–8 seconds of no successful pair, the same as regular ICE.
PJSUA-LIB equivalents
PJSUA2 |
PJSUA-LIB |
|---|---|
|
Note: candidate trickling (SDP-fragment encoding, SIP INFO send/receive,
checklist updates) is handled by PJSUA-LIB itself in
pjsip/src/pjsua-lib/pjsua_call.c; neither PJSUA-LIB nor
PJSUA2 surfaces a per-candidate event. Applications needing that
signal use the PJMEDIA-level callback
pjmedia_ice_cb::on_new_candidate (which itself wraps
pj_ice_strans_cb::on_new_candidate from PJNATH).
The underlying trickle helpers —
pjmedia_ice_trickle_update(),
pjmedia_ice_trickle_encode_sdp(),
pjmedia_ice_trickle_decode_sdp(),
pj_ice_strans_update_check_list() — are PJMEDIA / PJNATH
APIs intended for the SIP integration layer, not for direct
application use.