# Transport objects: where this differs from a browser
`SctpTransport`, `DtlsTransport` and `IceTransport` implement the W3C interfaces of the same
names. This document records every place the Rust surface differs from
[WebRTC 1.0](https://www.w3.org/TR/webrtc/), and why — so a reader porting from JavaScript finds
the differences here rather than discovering them.
Everything below was checked against the **Recommendation** (REC-webrtc-20250313), not MDN and not
the editor's draft, which has already drifted from it.
## How you reach a transport
```rust
// Data: the only transport member the spec puts on RTCPeerConnection.
let sctp = pc.sctp().await.expect("SCTP negotiated");
let dtls = sctp.transport();
let ice = dtls.ice_transport();
// Media: a sender or receiver is the other way in — and the only way on a
// connection that carries no data channel.
let dtls = sender.transport().await?.expect("sender is associated");
```
There is deliberately **no** `pc.dtls_transport()` or `pc.ice_transport()`. The spec does not have
them: `RTCPeerConnection` exposes `sctp` and nothing else, and the rest of the graph is reached by
walking. An earlier draft of this API added lookups by id; they were removed as unfaithful.
---
## 1. Identity is `id()`, not reference equality
**Spec:** the graph is object references, so JavaScript asks `pc.sctp.transport === sender.transport`.
**Here:** handles are values over a shared core. `Arc::ptr_eq` would answer a different question —
whether two *handles* are the same object, not whether they name the same transport — so identity
is exposed explicitly:
```rust
assert_eq!(sctp.transport().id(), sender.transport().await?.unwrap().id());
```
`RTCTransportId` is opaque and cannot be constructed by an application; it exists only to be
compared. Its guarantees:
- Distinct transports have distinct ids, **including across peer connections** — comparing a
transport from one connection against another's correctly reports "different".
- Stable across reads: assigned when the transport is created, not derived when asked for.
And what it does not promise: the value is not reproducible across runs (it is seeded from a
per-connection random nonce, because cross-connection distinctness and reproducibility are mutually
exclusive), and it is unrelated to `RTCStatsId` — there is one `RTCTransportStats` entry describing
the bundled transport, not one per transport.
## 2. State is read, not delivered
**Spec:** `onstatechange`, `onerror`, `onselectedcandidatepairchange`.
**Here:** none of them. State is polled:
```rust
match dtls.state().await? { RTCDtlsTransportState::Connected => …, _ => … }
```
ICE state changes remain observable through the existing peer-connection events
(`on_ice_connection_state_change`, `on_ice_gathering_state_change`). DTLS and SCTP are poll-only.
## 3. `maxMessageSize` is always finite
**Spec:** typed `unrestricted double`, and §6.1.1.2 sets it to **positive infinity** when neither
endpoint imposes a limit.
**Here:** always a finite `u32`. The negotiated value also sizes a real receive buffer, so this
implementation always has a ceiling; §6.1.1.2 permits the "no limit" input only for an
implementation that "can handle messages of any size", which this one cannot. A configuration
naming no limit resolves to the implementation ceiling instead, so **the value reported is the
value enforced**. Reporting infinity while allocating 256 KiB would make the attribute a promise
the transport does not keep.
## 4. `gatheringState` reuses `RTCIceGatheringState`
**Spec:** two enums with identical value sets — `RTCIceGatheringState` for
`RTCPeerConnection.iceGatheringState`, `RTCIceGathererState` for `RTCIceTransport.gatheringState`.
**Here:** one type, `RTCIceGatheringState`, used for both. A second, structurally identical enum
would buy nothing and cost a conversion at every call site.
## 5. `component` is always `Rtp`
Not a deviation — conformance. RTCP multiplexing is required (`RTCRtcpMuxPolicy` has the single
value `"require"`), and for a muxed transport the spec itself specifies: "a single
`RTCIceTransport` transports both RTP and RTCP and `component` is set to `rtp`".
## 6. `getRemoteCertificates()` returns DER bytes
`Vec<Vec<u8>>`, the analogue of the browser's `sequence<ArrayBuffer>`. Empty until the DTLS
handshake completes.
## 7. `sender.transport()` / `receiver.transport()` nullability
**Spec:** null "prior to construction of the `RTCDtlsTransport` object", sourced from the
per-object `[[SenderTransport]]` / `[[ReceiverTransport]]` slots.
**Here:** `Ok(None)` until that sender's or receiver's **transceiver has been associated** — i.e.
has a mid — which is when those slots are filled while applying a local or remote description.
Note this is deliberately *not* keyed on the DTLS handshake having started: an offerer associates
its transceivers at `setLocalDescription`, before any answer exists, and a browser reports a
transport there too. The handle simply reports state `New` until the handshake begins.
`Err` is distinct from `Ok(None)`: it means the sender or receiver itself no longer exists.
## 8. `sctp()` does not return to `None` after a renegotiation that drops data
**Spec:** `[[SctpTransport]]` is set back to null when an answer initiates the closure of the SCTP
association.
**Here:** it is not. `sctp()` becomes `Some` when the association is negotiated and stays `Some`
for the connection's lifetime. Renegotiating a data channel away leaves a handle whose `state()`
reports `Closed` rather than a `None` from `sctp()`.
**Known deviation, not yet fixed.** Resetting it touches the renegotiation path, which is a
higher-risk change than the gap justifies. Check `state()` if this matters to you.
## 9. `RTCIceRole` never reports `"unknown"`
**Spec:** `RTCIceRole` is `"unknown" | "controlling" | "controlled"`, with `"unknown"` before a
role has been determined.
**Here:** the underlying agent stores the role as a boolean, so `role()` can only return
`Controlling` or `Controlled`. `RTCIceRole::Unspecified` exists in the enum but this accessor
never yields it.
## 10. `RTCIceParameters` carries a non-standard member
The spec dictionary has exactly `usernameFragment` and `password`. This one also carries
`ice_lite`.
---
## Why some methods are `async` and others are not
This follows the IDL's nullability rather than a house style:
| `id()` | stored on the handle |
| `SctpTransport::transport()`, `DtlsTransport::ice_transport()` | the spec types these **non-null**, so the next handle is built without consulting the core |
| `IceTransport::component()` | a constant |
Everything else reads live transport state, which means taking the core lock, which means `async`.
## Implementation note: routes
A borrowed view of the core cannot be held across an `await`, so each async accessor re-walks the
graph from an entry point and copies out owned data. Because the core is spec-shaped, there is more
than one entry point — a media-only connection has no `sctp()` — so each DTLS/ICE handle records
the route it was reached by (`Sctp`, `Sender(id)` or `Receiver(id)`) and re-walks that.
This is the cost of not adding `pc.dtls_transport()`. It is invisible to callers, and it is
covered by a test on a media-only connection, where the SCTP route does not exist and only the
sender and receiver routes resolve.