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

Turns negotiated Session Description Protocol (SDP) text into a transport
config map that `XGW.Peer` can use.

## What SDP is (for readers new to WebRTC)

Before two endpoints send audio or video, they exchange short text documents
called **SDP offers and answers**. Those documents list:

- which codecs and media lines are in use
- **ICE** usernames/passwords used to authenticate connectivity probes
- the **DTLS** certificate fingerprint used to trust the encryption handshake
- optional **candidates** (IP/port addresses to try)
- whether a **data channel** (SCTP) is negotiated

Signaling (HTTP, WebSocket, SIP) carries those documents. XGW does not invent
SDP; it consumes the pair your app already negotiated (for example via
WebSocket/JSEP or SIP + [xsdp](https://github.com/Lazarus404/xsdp)) and
extracts only the fields needed to run ICE/DTLS/SRTP.

## Behavior notes

Uses `XMediaLib.SDP` only. ICE-lite v1 picks the first bundled DTLS-SRTP
m-line for fingerprint and ICE credentials. Session-level `a=fingerprint` /
ICE / setup attributes (common in Firefox) are inherited onto media lines
when those fields are absent at media level.

## RFCs

* RFC 8866 -- Session Description Protocol (SDP)
* RFC 8829 -- JSEP (offer/answer usage in WebRTC)
* RFC 8445 -- ICE credentials and candidates in SDP
* RFC 8122 -- `a=fingerprint`
* RFC 8841 -- SDP for SCTP data channels (when an application m-line is present)

# `config`

```elixir
@type config() :: %{
  remote_fingerprint: XMediaLib.SDP.Fingerprint.t(),
  local_fingerprint: XMediaLib.SDP.Fingerprint.t(),
  setup: XMediaLib.SDP.Media.setup(),
  bundle_mids: [String.t()],
  media_by_mid: %{optional(String.t()) =&gt; media_info()},
  remote_candidates: [XMediaLib.SDP.Candidate.t()],
  local_candidates: [XMediaLib.SDP.Candidate.t()],
  ssrc_to_mid: %{optional(non_neg_integer()) =&gt; String.t()},
  sctp: sctp_info() | nil,
  ice: %{
    local_ufrag: String.t(),
    local_pwd: String.t(),
    remote_ufrag: String.t(),
    remote_pwd: String.t(),
    local_setup: XMediaLib.SDP.Media.setup(),
    remote_setup: XMediaLib.SDP.Media.setup()
  }
}
```

Parsed transport and media config for one peer.

## Fields

* `:remote_fingerprint` / `:local_fingerprint` - DTLS cert digests from SDP
* `:setup` - local DTLS setup (`:passive` / `:active` / ...)
* `:bundle_mids` - BUNDLE group mids from the offer
* `:media_by_mid` - per-mid codec/SSRC summary from the offer
* `:remote_candidates` / `:local_candidates` - ICE candidates from offer/answer
* `:ssrc_to_mid` - offer SSRC map used to tag inbound RTP meta
* `:sctp` - negotiated data-channel ports, or `nil`
* `:ice` - local/remote ufrag, pwd, and setup atoms

# `media_info`

```elixir
@type media_info() :: %{
  mid: String.t() | nil,
  type: XMediaLib.SDP.Media.type() | nil,
  protocol: XMediaLib.SDP.Media.protocol() | nil,
  rtcp_mux: boolean(),
  codecs: [XMediaLib.SDP.Codec.t()],
  ssrcs: [XMediaLib.SDP.Ssrc.t()]
}
```

Per-m-line media summary from the remote offer.

## Fields

* `:mid` - media identification tag
* `:type` / `:protocol` - SDP media type and proto
* `:rtcp_mux` - whether RTCP shares the RTP port
* `:codecs` / `:ssrcs` - negotiated codecs and SSRCs

# `sctp_info`

```elixir
@type sctp_info() :: %{
  remote_port: non_neg_integer(),
  local_port: non_neg_integer(),
  max_message_size: non_neg_integer() | nil
}
```

Negotiated SCTP data-channel ports when an application m-line is present.

## Fields

* `:remote_port` / `:local_port` - SCTP port numbers from offer/answer
* `:max_message_size` - optional max message size from SDP

# `sdp_input`

```elixir
@type sdp_input() :: binary() | XMediaLib.SDP.t()
```

SDP offer or answer as binary or parsed struct.

# `build`

```elixir
@spec build(sdp_input(), sdp_input(), keyword()) :: {:ok, config()} | {:error, term()}
```

Parses offer and answer into peer config.

Returns `{:error, :invalid_sdp}`, `{:error, :missing_fingerprint}`,
`{:error, :missing_ice_credentials}`, or `{:error, :invalid_setup}` when
`ice_lite: true` and local setup is not `:passive`.

# `fingerprints_match?`

```elixir
@spec fingerprints_match?(
  XMediaLib.SDP.Fingerprint.t(),
  XMediaLib.SDP.Fingerprint.t()
) :: boolean()
```

Compares two SDP fingerprints (case-insensitive value).

## Examples

    iex> a = %XMediaLib.SDP.Fingerprint{algorithm: "sha-256", value: "aa:bb:cc"}
    iex> b = %XMediaLib.SDP.Fingerprint{algorithm: "SHA-256", value: "AA:BB:CC"}
    iex> XGW.Descriptions.fingerprints_match?(a, b)
    true

    iex> a = %XMediaLib.SDP.Fingerprint{algorithm: "sha-256", value: "aa:bb:cc"}
    iex> b = %XMediaLib.SDP.Fingerprint{algorithm: "sha-256", value: "dd:ee:ff"}
    iex> XGW.Descriptions.fingerprints_match?(a, b)
    false

# `normalize_fingerprint`

```elixir
@spec normalize_fingerprint(String.t()) :: String.t()
```

Normalizes an SDP fingerprint value to uppercase trimmed hex.

## Examples

    iex> XGW.Descriptions.normalize_fingerprint("aa:bb:cc")
    "AA:BB:CC"

    iex> XGW.Descriptions.normalize_fingerprint("  aa:bb  ")
    "AA:BB"

# `parse_sdp`

```elixir
@spec parse_sdp(sdp_input()) :: {:ok, XMediaLib.SDP.t()} | {:error, :invalid_sdp}
```

Parses SDP input into `%XMediaLib.SDP{}`.

Accepts a parsed struct or binary. Returns `{:error, :invalid_sdp}` for
non-string, non-struct input.

# `restart_remote`

```elixir
@spec restart_remote(sdp_input()) ::
  {:ok,
   %{
     remote_ice_ufrag: String.t(),
     remote_ice_pwd: String.t(),
     remote_candidates: [XMediaLib.SDP.Candidate.t()]
   }}
  | {:error, term()}
```

Parses a restart offer for new remote ICE credentials and candidates.

Local ufrag/pwd are unchanged in v1; only remote side updates.

---

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