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

Shared UDP (and optional TCP) socket that receives all WebRTC traffic for
many peers and routes each packet to the right `XGW.Peer`.

## Why a shared listener

Opening one UDP port per browser would not scale and would break demux
assumptions. One Listener binds the media port; inbound bytes are classified
(`XGW.Demux`) and dispatched:

* **STUN Binding** -- look up the Peer by local ICE ufrag in USERNAME
  (`local:remote` at this host)
* **RTP / RTCP / DTLS** -- look up by remote `{ip, port}` after `bind_remote/3`
* **TURN ChannelData** -- look up by channel number from `bind_turn_channel/3`
* **TURN Data Indication** -- unwrap, then re-demux the inner packet

With a single registered Peer and no bind keys, all classified packets go to
that Peer (Echo / one-Peer-per-Listener back-compat).

Inbound media meta includes `:arrival_us` (monotonic microseconds) stamped in
`route_packet/4` before `Peer.deliver/4`, so TWCC arrivals exclude Peer
mailbox delay.

## Gather options (on `start_link/1`)

* Host: `:advertised_ip`, `:advertised_ips`, `:include_loopback` (default `true`)
* srflx: `:stun_servers` (default `[]`), `:gather_timeout_ms` (default `3000`)
* relay: `:turn_servers` plus TURN credential opts
* `:relay_only` -- skip host/srflx SDP validation when the answer is relay-only
* ICE-TCP: `:ice_tcp_enabled` / `:ice_tcp_port` for passive TCP candidates

Call `gather_candidates/2` before publishing answer SDP when you need host +
srflx + relay lines embedded up front.

## RFCs

* RFC 7983 -- demux of STUN / DTLS / TURN ChannelData / RTP/RTCP on one socket
* RFC 8445 -- ICE candidate gather and STUN Binding dispatch by ufrag
* RFC 8656 -- TURN Data Indication unwrap and ChannelData
* RFC 6544 -- optional ICE-TCP passive candidates and accept path

# `allocate_turn_channel`

```elixir
@spec allocate_turn_channel(pid(), pid()) :: {:ok, non_neg_integer()}
```

Allocates the next TURN channel number unique on this Listener socket.

# `bind_remote`

```elixir
@spec bind_remote(pid(), pid(), {:inet.ip_address(), :inet.port_number()}) :: :ok
```

Maps remote `{ip, port}` to `peer_pid` for RTP/RTCP/DTLS demux.

# `bind_turn_channel`

```elixir
@spec bind_turn_channel(pid(), pid(), non_neg_integer()) :: :ok
```

Maps TURN ChannelData `channel` to `peer_pid`.

# `bind_ufrag`

```elixir
@spec bind_ufrag(pid(), pid(), String.t()) :: :ok
```

Maps local ICE ufrag to `peer_pid` for STUN Binding demux.

# `cert_paths`

```elixir
@spec cert_paths(pid()) :: {String.t(), String.t()}
```

Returns `{certfile, keyfile}` paths for DTLS handshakes.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `gather_candidates`

```elixir
@spec gather_candidates(
  pid(),
  keyword()
) :: %{
  host: [XMediaLib.SDP.Candidate.t()],
  srflx: [XMediaLib.SDP.Candidate.t()],
  relay: [XMediaLib.SDP.Candidate.t()]
}
```

Gathers host, srflx, and relay candidates within `timeout_ms`.

Returns `%{host: [...], srflx: [...], relay: [...]}`.

# `gather_opts`

```elixir
@spec gather_opts(pid()) :: keyword()
```

Host gather options configured at listener start.

# `gather_timeout_ms`

```elixir
@spec gather_timeout_ms(pid()) :: non_neg_integer()
```

Pre-answer srflx gather timeout in milliseconds.

# `host_candidate`

```elixir
@spec host_candidate(pid()) :: XMediaLib.SDP.Candidate.t()
```

Host ICE candidate with highest priority for the listener UDP socket.

# `host_candidate_line`

```elixir
@spec host_candidate_line(pid()) :: String.t()
```

SDP `a=candidate:` attribute value (without prefix) for the highest-priority host.

# `host_candidate_lines`

```elixir
@spec host_candidate_lines(pid()) :: [String.t()]
```

SDP `a=candidate:` lines for all gathered host candidates (without prefix).

# `host_candidates`

```elixir
@spec host_candidates(pid()) :: [XMediaLib.SDP.Candidate.t()]
```

All host ICE candidates for the listener UDP socket (multi-interface).

# `ice_tcp_enabled?`

```elixir
@spec ice_tcp_enabled?(pid()) :: boolean()
```

Returns true when ICE-TCP passive candidates are enabled.

# `listen_sock`

```elixir
@spec listen_sock(pid()) :: :ssl.sslsocket()
```

Returns the SSL listen socket for DTLS accept (shared UDP via Transport).

# `local_fingerprint`

```elixir
@spec local_fingerprint(pid()) :: String.t()
```

SHA-256 fingerprint of the listener DTLS certificate (SDP format).

# `port`

```elixir
@spec port(pid()) :: :inet.port_number()
```

Bound port of the shared UDP socket.

# `register_peer`

```elixir
@spec register_peer(pid(), pid()) :: :ok
```

Registers `peer_pid` for demux (monitor only until bind_* keys are set).

# `relay_candidate_lines`

```elixir
@spec relay_candidate_lines(
  pid(),
  keyword()
) :: [String.t()]
```

SDP `a=candidate:` lines for relay candidates gathered within the timeout.

# `relay_only?`

```elixir
@spec relay_only?(pid()) :: boolean()
```

When true, answer SDP may omit host/srflx locals (relay-only / zero-public-port).

# `send`

```elixir
@spec send(pid(), :inet.ip_address(), :inet.port_number(), binary()) :: :ok
```

Sends a UDP datagram on the listener socket (STUN replies, etc.).

# `send_datagram`

```elixir
@spec send_datagram(pid(), :inet.ip_address(), :inet.port_number(), binary()) ::
  :ok | {:error, term()}
```

Sends a UDP datagram and waits for `:gen_udp.send/4` result (outbound media).

# `send_tcp_stun`

```elixir
@spec send_tcp_stun(pid(), XGW.ICE.endpoint(), binary()) :: :ok | {:error, term()}
```

Sends a framed STUN message over an established ICE-TCP connection to `peer_addr`.

# `sockname`

```elixir
@spec sockname(pid()) :: {:inet.ip_address(), :inet.port_number()}
```

Bound socket `{ip, port}`.

# `srflx_candidate_lines`

```elixir
@spec srflx_candidate_lines(
  pid(),
  keyword()
) :: [String.t()]
```

SDP `a=candidate:` lines for srflx candidates gathered within the timeout.

# `start_link`

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

Starts a UDP listener on `ip` / `port` (use `port: 0` for an ephemeral bind).

Optional gather opts: `:advertised_ip`, `:advertised_ips`, `:include_loopback`,
`:stun_servers`, `:gather_timeout_ms`, `:turn_servers`, `:relay_only`, TURN
credential opts, `:ice_tcp_enabled`, `:ice_tcp_port`.

# `stun_binding`

```elixir
@spec stun_binding(
  pid(),
  :inet.ip_address(),
  :inet.port_number(),
  non_neg_integer(),
  binary() | nil
) :: {:ok, binary()} | {:error, term()}
```

Sends a STUN Binding request on the listener socket and waits for a response.

Optional fifth argument is pre-encoded request wire (defaults to plain Binding).

# `stun_servers`

```elixir
@spec stun_servers(pid()) :: [String.t()]
```

Configured STUN server URIs for srflx gather.

# `tcp_candidate_lines`

```elixir
@spec tcp_candidate_lines(pid()) :: [String.t()]
```

SDP `a=candidate:` lines for passive TCP host candidates.

# `tcp_candidates`

```elixir
@spec tcp_candidates(pid()) :: [XMediaLib.SDP.Candidate.t()]
```

Passive ICE-TCP host candidates (empty when ICE-TCP is disabled).

# `tcp_connect_stun`

```elixir
@spec tcp_connect_stun(pid(), XGW.ICE.endpoint(), binary()) :: :ok | {:error, term()}
```

Connects to `peer_addr` over TCP if needed and sends a framed STUN message.

# `tcp_port`

```elixir
@spec tcp_port(pid()) :: :inet.port_number() | nil
```

Returns the bound ICE-TCP listen port when enabled, else `nil`.

# `turn_credentials`

```elixir
@spec turn_credentials(pid()) :: keyword()
```

TURN credential options for relay gather.

# `turn_request`

```elixir
@spec turn_request(
  pid(),
  :inet.ip_address(),
  :inet.port_number(),
  non_neg_integer(),
  binary()
) :: {:ok, binary()} | {:error, term()}
```

Sends a STUN/TURN request on the listener socket and waits for a response.

# `turn_servers`

```elixir
@spec turn_servers(pid()) :: [String.t()]
```

Configured TURN server URIs for relay gather.

# `udp_sock`

```elixir
@spec udp_sock(pid()) :: port()
```

Returns the shared UDP socket used for STUN/RTP and DTLS `cb_info`.

# `unregister_peer`

```elixir
@spec unregister_peer(pid(), pid()) :: :ok
```

Removes `peer_pid` and all demux keys that pointed at it.

---

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