One remote WebRTC client: runs ICE, DTLS, and SRTP, then gives your app plaintext media and data-channel events.
Mental model
Think of a Peer as the server-side half of a browser RTCPeerConnection.
Your application (SFU, MCU, or signaling server) still owns signaling: it
negotiates SDP offer/answer, then calls apply_descriptions/4 here. After
that, XGW owns the wire protocols until the Peer closes.
Lifecycle in short:
apply SDP -> ICE connects -> DTLS connects -> SRTP keys ready -> media I/OOutbound media (after {:xgw, :dtls_connected, peer_ref})
{:ok, handle} = XGW.Peer.outbound_handle(peer_ref)thenXGW.Peer.SendHandle.send_rtp(handle, ssrc, packet)(hot path; no GenServer):ok = XGW.Peer.send_rtp(peer_ref, ssrc, %XMediaLib.Rtp{} | binary()):ok = XGW.Peer.send_rtp_async(peer_ref, ssrc, packet)(cast; still shares ETS encrypt):ok = XGW.Peer.send_rtcp(peer_ref, %XMediaLib.Rtcp{} | binary())
The ssrc argument to send_rtp/3 overrides %Rtp{}.ssrc when they differ
(Echo v1 passes the inbound %Rtp{} unchanged so both match).
Handler callbacks
{:xgw, :ice_connected, peer_ref}{:xgw, :local_candidate, peer_ref, %{candidate: line, mid: mid}}(full mode srflx trickle){:xgw, :local_candidates_complete, peer_ref}(full mode; post-apply gather finished){:xgw, :dtls_connected, peer_ref}{:xgw, :sctp_connected, peer_ref}(when offer/answer include SCTP){:xgw, :sctp_disconnected, peer_ref}(SCTP association went down){:xgw, :datachannel, peer_ref, %{ref: ref, label: label, id: id}}(optional:reliability,:order,:paramfrom DCEP when present){:xgw, :datachannel_closed, peer_ref, channel_ref}{:xgw, :datachannel_message, peer_ref, channel_ref, binary()}{:xgw, :rtp, peer_ref, %XMediaLib.Rtp{}, meta}{:xgw, :rtcp, peer_ref, rtcp, meta}{:xgw, :closed, peer_ref, reason}(:normal,:fingerprint_mismatch,:consent_failed)
After DTLS, reply on a data channel with XGW.Peer.send_datachannel/3.
Production SCTP and DTLS app-data run on a linked XGW.Peer.Datachannel
child so ICE/RTP on this GenServer are not head-of-line blocked by DC egress.
ICE modes and gather
Embed host (and optionally srflx) a=candidate: lines from
XGW.Listener.gather_candidates/2 or host_candidate_lines/1 in the local
answer before apply_descriptions/4. Post-answer srflx arrives via
{:xgw, :local_candidate, ...} when stun_servers is configured.
Set ice_lite: false for the full ICE agent (XGW.ICE.Full). Full mode
parses local host candidates from the answer, stays in :gathering until
srflx, then :checking / :connected after nomination. Query locals with
local_candidates/1 and phase with ice_phase/1 (:completed for lite).
Gather options (:advertised_ip, :advertised_ips, :include_loopback,
:relay_only) belong on XGW.Listener.start_link/1, not the Peer.
Internal helpers (GenServer stays here):
Peer.Gather -- local candidate verify + post-apply srflx/relay gather
Peer.Turn -- TURN allocations, unwrap, channel bind, relay encode
Peer.Sctp -- SCTP start + event fan-outMedia callback meta map:
:from-{ip, port}of the remote UDP endpoint (ICE-selected pair):mid- BUNDLE m-line id from offer SSRC map, ornilwhen unknown:arrival_us- monotonic microseconds when the Listener demuxed the packet (wire side).Peer.deliver/4fills this only when missing (tests / direct inject). Conference TWCC uses it so Peer and Forwarder mailbox delay are not reported as network delay.
RFCs
End-to-end peer stack: RFC 8445 (ICE), RFC 8489 (STUN), RFC 8656 (TURN), RFC 9147 / RFC 6347 (DTLS), RFC 5764 (DTLS-SRTP), RFC 3711 (SRTP), RFC 8261 / RFC 8831 (SCTP data channels over DTLS), RFC 7675 (consent).
Summary
Types
Trickle ICE candidate: %XMediaLib.SDP.Candidate{} or %{candidate: line} map.
Classified ingress packet kind from XGW.Demux.
Peer GenServer pid.
Connection phase: :ice_wait, :dtls_wait, :media, or :closed.
Remote offer or local answer as SDP binary or %XMediaLib.SDP{}.
Functions
Adds a remote ICE candidate (Trickle ICE, RFC 8838).
Applies negotiated remote offer and local answer (produced upstream by XSDP).
Returns a specification to start this module under a supervisor.
Tears down ICE/DTLS and stops the peer process.
Test helper: install SRTP keys and enter media phase (skips live DTLS).
Test helper: force ICE connected with optional selected from endpoint.
Delivers a demuxed packet from the Listener (STUN, DTLS, RTP, RTCP, ChannelData).
Returns the ICE agent phase.
Signals ICE restart with a new remote offer (RFC 8445 section 9.1.1.1).
Returns local ICE candidates stored in the full ICE agent.
Opens a local data channel (DCEP) after the SCTP association is connected.
Returns a hot-path send handle when UDP media is published in ETS.
Returns the Peer's media phase (:ice_wait, :dtls_wait, :media, :closed).
Allocates TURN relay candidates once and keeps the allocations on the peer.
Sends a DataChannel user message after SCTP is up.
Queues a DataChannel user message without waiting for SCTP/DTLS flush.
Sends plaintext RTCP toward the peer (SRTCP-encrypted on the wire).
Sends plaintext RTP toward the peer (SRTP-encrypted on the wire).
Queues plaintext RTP toward the peer without waiting for encrypt/send.
Replaces the process that receives Peer notifications (:rtp, lifecycle, ...).
Starts a peer under XGW.PeerSupervisor.
Starts a peer as a child of XGW.PeerSupervisor.
Types
@type candidate() :: map()
Trickle ICE candidate: %XMediaLib.SDP.Candidate{} or %{candidate: line} map.
@type packet_kind() :: XGW.Demux.kind()
Classified ingress packet kind from XGW.Demux.
@type peer_ref() :: pid()
Peer GenServer pid.
@type phase() :: :ice_wait | :dtls_wait | :media | :closed
Connection phase: :ice_wait, :dtls_wait, :media, or :closed.
Remote offer or local answer as SDP binary or %XMediaLib.SDP{}.
Functions
@spec add_remote_candidate(peer_ref(), candidate() | XMediaLib.SDP.Candidate.t()) :: :ok | {:error, term()}
Adds a remote ICE candidate (Trickle ICE, RFC 8838).
Accepts %Candidate{} or a map with :candidate (line or struct), optional
:mid, :mline_index, and :end_of_candidates.
Applies negotiated remote offer and local answer (produced upstream by XSDP).
Initializes ICE-lite credentials and transport config from %XMediaLib.SDP{}
or SDP binaries. Validates local fingerprint against the listener certificate
when a listener is attached.
Returns a specification to start this module under a supervisor.
See Supervisor.
Tears down ICE/DTLS and stops the peer process.
@spec connect_dtls( peer_ref(), XGW.SRTPSession.keys(), {:inet.ip_address(), :inet.port_number()} | nil ) :: :ok
Test helper: install SRTP keys and enter media phase (skips live DTLS).
@spec connect_ice(peer_ref(), {:inet.ip_address(), :inet.port_number()} | nil) :: :ok
Test helper: force ICE connected with optional selected from endpoint.
@spec deliver(peer_ref(), packet_kind(), binary(), map()) :: :ok
Delivers a demuxed packet from the Listener (STUN, DTLS, RTP, RTCP, ChannelData).
Fills :arrival_us on meta when missing. The Listener stamps this at demux
time; this fallback covers tests and any inject path that skips the Listener.
@spec ice_phase(peer_ref()) :: XGW.ICE.ice_phase()
Returns the ICE agent phase.
Lite peers report :completed (binding-only path). Full peers report the
ICE state machine phase from XGW.ICE.Full (for example :gathering).
Signals ICE restart with a new remote offer (RFC 8445 section 9.1.1.1).
Full ufrag/pwd rotation and checklist reset: Full.ice_restart/1 resets state;
remote credentials come from the restart offer. Local ufrag/pwd unchanged in v1.
@spec local_candidates(peer_ref()) :: [XMediaLib.SDP.Candidate.t()]
Returns local ICE candidates stored in the full ICE agent.
Lite peers return []; use XGW.Listener.host_candidate/1 for lite host lines.
Opens a local data channel (DCEP) after the SCTP association is connected.
Synthesizes {:xgw, :datachannel, peer, info} to the handler so Session /
Backend register the label the same way as a remote open. Returns
{:error, :not_connected} when SCTP is missing or not connected.
@spec outbound_handle(peer_ref()) :: {:ok, XGW.Peer.SendHandle.t()} | {:error, :not_ready | :use_genserver}
Returns a hot-path send handle when UDP media is published in ETS.
Prefer XGW.Peer.SendHandle.send_rtp/3 for Echo/SFU forwarding. Returns
{:error, :not_ready} before media, or {:error, :use_genserver} for ICE-TCP.
Returns the Peer's media phase (:ice_wait, :dtls_wait, :media, :closed).
@spec prefetch_relay_candidates(peer_ref()) :: {:ok, [String.t()]} | {:error, :no_relay_candidates | term()}
Allocates TURN relay candidates once and keeps the allocations on the peer.
Used by relay-only answer builders so apply_descriptions/3 can seed those
relay locals without a second Allocate on the same 5-tuple.
@spec send_datachannel(peer_ref(), reference(), binary(), :string | :binary) :: :ok | {:error, term()}
Sends a DataChannel user message after SCTP is up.
Routes to XGW.Peer.Datachannel when that child is published; otherwise the
Peer GenServer (tests / pre-attach). type is :string (PPI 51, default) or
:binary (PPI 53).
Queues a DataChannel user message without waiting for SCTP/DTLS flush.
Prefer XGW.Peer.SendHandle.send_datachannel/3 when a :dc_passthrough row
is published (benches). Production Forwarder fallback casts the per-Peer
XGW.Peer.Datachannel process (batched drain) when published; otherwise the
Peer GenServer. type defaults to :string (PPI 51).
@spec send_rtcp(peer_ref(), binary() | XMediaLib.Rtcp.t()) :: :ok | {:error, term()}
Sends plaintext RTCP toward the peer (SRTCP-encrypted on the wire).
Same phase and error semantics as send_rtp/3.
@spec send_rtp(peer_ref(), non_neg_integer(), binary() | XMediaLib.Rtp.t()) :: :ok | {:error, term()}
Sends plaintext RTP toward the peer (SRTP-encrypted on the wire).
Requires :media phase. On UDP, uses the same ETS encrypt path as
SendHandle.send_rtp/3. Returns {:error, :not_connected},
{:error, :no_selected_pair}, {:error, :invalid_packet}, or an SRTP/UDP error.
@spec send_rtp_async(peer_ref(), non_neg_integer(), binary() | XMediaLib.Rtp.t()) :: :ok
Queues plaintext RTP toward the peer without waiting for encrypt/send.
Prefer outbound_handle/1 + SendHandle.send_rtp/3 when a handle is available;
this cast remains a fallback (ICE-TCP, missing handle).
Replaces the process that receives Peer notifications (:rtp, lifecycle, ...).
Used when a Session installs a media forwarder as handler: after start, or
when tests retarget delivery. handler must be a live pid.
Starts a peer under XGW.PeerSupervisor.
Required: :handler (pid). Optional: :listener, :ice_lite (default true;
false selects XGW.ICE.Full), :consent_interval_ms (default 5000), :consent_timeout_ms (default 30000),
:cert_dir (unused here; listener owns certs).
Starts a peer as a child of XGW.PeerSupervisor.