# `XGW.Peer`
[🔗](https://github.com/Lazarus404/xgw/blob/main/lib/xgw/peer.ex#L1)

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:

```text
apply SDP -> ICE connects -> DTLS connects -> SRTP keys ready -> media I/O
```

## Outbound media (after `{:xgw, :dtls_connected, peer_ref}`)

  * `{:ok, handle} = XGW.Peer.outbound_handle(peer_ref)` then
    `XGW.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`, `:param` from 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-out

Media callback `meta` map:

  * `:from` - `{ip, port}` of the remote UDP endpoint (ICE-selected pair)
  * `:mid` - BUNDLE m-line id from offer SSRC map, or `nil` when unknown
  * `:arrival_us` - monotonic microseconds when the Listener demuxed the
    packet (wire side). `Peer.deliver/4` fills 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).

# `candidate`

```elixir
@type candidate() :: map()
```

Trickle ICE candidate: `%XMediaLib.SDP.Candidate{}` or `%{candidate: line}` map.

# `packet_kind`

```elixir
@type packet_kind() :: XGW.Demux.kind()
```

Classified ingress packet kind from `XGW.Demux`.

# `peer_ref`

```elixir
@type peer_ref() :: pid()
```

Peer GenServer pid.

# `phase`

```elixir
@type phase() :: :ice_wait | :dtls_wait | :media | :closed
```

Connection phase: `:ice_wait`, `:dtls_wait`, `:media`, or `:closed`.

# `sdp`

```elixir
@type sdp() :: binary() | struct()
```

Remote offer or local answer as SDP binary or `%XMediaLib.SDP{}`.

# `add_remote_candidate`

```elixir
@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`.

# `apply_descriptions`

```elixir
@spec apply_descriptions(peer_ref(), sdp(), sdp(), keyword()) ::
  :ok | {:error, term()}
```

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.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `close`

```elixir
@spec close(peer_ref()) :: :ok | {:error, term()}
```

Tears down ICE/DTLS and stops the peer process.

# `connect_dtls`

```elixir
@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).

# `connect_ice`

```elixir
@spec connect_ice(peer_ref(), {:inet.ip_address(), :inet.port_number()} | nil) :: :ok
```

Test helper: force ICE connected with optional selected `from` endpoint.

# `deliver`

```elixir
@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.

# `ice_phase`

```elixir
@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`).

# `ice_restart`

```elixir
@spec ice_restart(peer_ref(), sdp()) :: :ok | {:error, term()}
```

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.

# `local_candidates`

```elixir
@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.

# `open_datachannel`

```elixir
@spec open_datachannel(peer_ref(), binary(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

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.

# `outbound_handle`

```elixir
@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.

# `phase`

```elixir
@spec phase(peer_ref()) :: phase()
```

Returns the Peer's media phase (`:ice_wait`, `:dtls_wait`, `:media`, `:closed`).

# `prefetch_relay_candidates`

```elixir
@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.

# `send_datachannel`

```elixir
@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).

# `send_datachannel_async`

```elixir
@spec send_datachannel_async(peer_ref(), reference(), binary(), :string | :binary) ::
  :ok
```

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).

# `send_rtcp`

```elixir
@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`.

# `send_rtp`

```elixir
@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.

# `send_rtp_async`

```elixir
@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).

# `set_handler`

```elixir
@spec set_handler(peer_ref(), pid()) :: :ok | {:error, term()}
```

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.

# `start_link`

```elixir
@spec start_link(keyword()) :: {:ok, peer_ref()} | {:error, term()}
```

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).

# `start_supervised`

```elixir
@spec start_supervised(keyword()) :: {:ok, peer_ref()} | {:error, term()}
```

Starts a peer as a child of `XGW.PeerSupervisor`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
