# `XGW.ICE.Lite`
[🔗](https://github.com/Lazarus404/xgw/blob/main/lib/xgw/ice/lite.ex#L1)

ICE-lite agent: answer inbound STUN checks in the controlled role.

## Lite vs full

Many media servers sit on a public IP and never need to punch outbound NAT
holes themselves. **ICE-lite** (RFC 8445) is that mode: the agent publishes
host candidates, verifies MESSAGE-INTEGRITY with its local `ice-pwd`,
optionally checks USERNAME, merges trickle remote candidates, and responds
to Binding requests. Connectivity is declared when inbound checks include
ICE role attributes (and nomination when required).

Use `ice_lite: false` on the Peer for `XGW.ICE.Full` instead.

## RFCs

* RFC 8445 -- ICE-lite (controlled role, inbound checks only)
* RFC 8489 -- STUN Binding responses
* RFC 7675 -- consent freshness

# `from`

```elixir
@type from() :: XGW.ICE.endpoint()
```

Remote UDP endpoint `{ip, port}`.

# `t`

```elixir
@type t() :: %{
  ice_ufrag: String.t(),
  ice_pwd: String.t(),
  ice_tiebreaker: binary(),
  remote_ice_ufrag: String.t() | nil,
  remote_ice_pwd: String.t() | nil,
  bundle_mids: [String.t()],
  remote_candidates: [XMediaLib.SDP.Candidate.t()],
  remote_candidates_eoc?: boolean(),
  last_consent_at: integer() | nil,
  connected?: boolean(),
  selected: from() | nil
}
```

ICE-lite agent state.

* `ice_ufrag` / `ice_pwd` - local credentials from the answer
* `remote_ice_ufrag` - remote ufrag from the offer (username validation)
* `remote_ice_pwd` - remote pwd from the offer (outbound STUN integrity)
* `remote_candidates` - merged trickle + SDP remote candidates (priority order)
* `remote_candidates_eoc?` - true after end-of-candidates
* `last_consent_at` - monotonic ms of last consent refresh (RFC 7675)
* `connected?` - true after first ICE connectivity check
* `selected` - validated remote `{ip, port}` pair

# `add_remote_candidate`

```elixir
@spec add_remote_candidate(t(), map() | XMediaLib.SDP.Candidate.t()) ::
  {:ok, t()} | {:error, term()}
```

Adds a trickle remote candidate or records end-of-candidates.

# `endpoint`

```elixir
@spec endpoint(XMediaLib.SDP.Candidate.t()) :: from()
```

Returns `{ip, port}` for a parsed candidate (delegates to `XGW.ICE.endpoint/1`).

# `from_config`

```elixir
@spec from_config(map()) :: t()
```

Builds ICE-lite state from `XGW.Descriptions` config.

# `handle_binding`

```elixir
@spec handle_binding(binary(), from(), t()) ::
  {:reply, binary(), t()}
  | {:connected, binary(), t()}
  | {:consent, t()}
  | {:ignore, t()}
```

Handles one STUN datagram from `from`.

Returns `{:reply, wire, state}` for a binding response, optionally
`{:connected, wire, state}` when the first ICE connectivity check succeeds,
or `{:consent, state}` for an inbound Binding success (RFC 7675).

# `merge_remote_candidates`

```elixir
@spec merge_remote_candidates([XMediaLib.SDP.Candidate.t()], [
  XMediaLib.SDP.Candidate.t()
]) :: [
  XMediaLib.SDP.Candidate.t()
]
```

Merges remote candidates (delegates to `XGW.ICE.merge_remote/2`).

# `new`

```elixir
@spec new(keyword()) :: t()
```

Builds ICE-lite state with explicit credentials (tests and callers without SDP).

# `seed_remote_candidates`

```elixir
@spec seed_remote_candidates(t(), [XMediaLib.SDP.Candidate.t()]) :: t()
```

Seeds ICE-lite state with remote candidates from the initial offer SDP.

---

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