# Technical Documentation: webrtc-rs/webrtc

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/webrtc-rs/webrtc)

### Overview (`README.md`)

<h1 align="center">
 <a href="https://webrtc.rs"><img src="https://raw.githubusercontent.com/webrtc-rs/webrtc-rs.github.io/master/res/webrtc.rs.png" alt="WebRTC.rs"></a>
 <br>
</h1>
<p align="center">
 <a href="https://github.com/webrtc-rs/webrtc/actions">
  <img src="https://github.com/webrtc-rs/webrtc/workflows/cargo/badge.svg">
 </a>
 <a href="https://codecov.io/gh/webrtc-rs/webrtc">
  <img src="https://codecov.io/gh/webrtc-rs/webrtc/branch/master/graph/badge.svg">
 </a>
 <a href="https://deps.rs/repo/github/webrtc-rs/webrtc">
  <img src="https://deps.rs/repo/github/webrtc-rs/webrtc/status.svg">
 </a>
 <a href="https://crates.io/crates/webrtc">
  <img src="https://img.shields.io/crates/v/webrtc.svg">
 </a>
 <a href="https://docs.rs/webrtc">
  <img src="https://docs.rs/webrtc/badge.svg">
 </a>
 <a href="https://doc.rust-lang.org/1.6.0/complement-project-faq.html#why-dual-mitasl2-license">
  <img src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue" alt="License: MIT/Apache 2.0">
 </a>
 <a href="https://discord.gg/4Ju8UHdXMs">
  <img src="https://img.shields.io/discord/800204819540869120?logo=discord" alt="Discord">
 </a>
 <a href="https://twitter.com/WebRTCrs">
  <img src="https://img.shields.io/twitter/url/https/twitter.com/webrtcrs.svg?style=social&label=%40WebRTCrs" alt="Twitter">
 </a>
</p>
<p align="center">
 Async-friendly WebRTC implementation in Rust
</p>

<p align="center">
<strong>Sponsored with 💖 by</strong><br>
</p>
<p align="center">
<strong>Gold Sponsors:</strong><br>
<a href="https://www.recall.ai" target="_blank">
<img src="https://raw.githubusercontent.com/webrtc-rs/webrtc-rs.github.io/master/res/recall_md.svg"
alt="Recall.ai">
</a><br>
<p align="center">
<strong>Silver Sponsors:</strong><br>
<a href="https://getstream.io/video/voice-calling/?utm_source=https://github.com/webrtc-rs/webrtc&utm_medium=sponsorship&utm_content=&utm_campaign=webrtcRepo_July2023_video_klmh22" target="_blank">
<img src="https://raw.githubusercontent.com/webrtc-rs/webrtc-rs.github.io/master/res/stream-logo.png" height="50" alt="Stream Chat">
</a><br>
<a href="https://channel.io/" target="_blank">
<img src="https://raw.githubusercontent.com/webrtc-rs/webrtc-rs.github.io/master/res/ChannelTalk_logo_md.png" alt="ChannelTalk">
</a><br>
<strong>Bronze Sponsors:</strong><br>
<a href="https://github.com/AdrianEddy" target="_blank">AdrianEddy</a><br>
</p>

<details>
<summary><b>Table of Content</b></summary>

- [Overview](#overview)
- [Getting Started](#getting-started)
- [The architecture](#the-architecture)
- [Release lines](#release-lines)
- [What's in 0.21](#whats-in-021)
- [How to Provide Feedback](#how-to-provide-feedback)
- [Building and Testing](#building-and-testing)
- [Semantic Versioning](#semantic-versioning)
- [Open Source License](#open-source-license)
- [Contributing](#contributing)

</details>

## Overview

WebRTC.rs is an async-friendly WebRTC implementation in Rust, originally inspired by and largely rewriting the Pion
stack. The async `webrtc` crate is a clean, ergonomic, runtime-agnostic rewrite on top of a Sans-I/O core; it ships with
Tokio and smol runtime backends, and any other runtime can be plugged in by implementing one trait.

**Architecture:**

- **[rtc](https://github.com/webrtc-rs/rtc)**: Sans-I/O protocol core with complete WebRTC stack (95%+ W3C API
  compliance)
- **webrtc** (this crate): a thin async layer over `rtc`:
    - **`PeerConnection`** — the user-facing async API handle; all operations (create offers/answers, add tracks,
      create data channels) are `async`
    - **`PeerConnectionDriver`** — an internal background event loop, spawned automatically, that owns the sockets,
      drives the Sans-I/O `rtc` core, handles timeouts, and dispatches events
    - **`Runtime`** — a trait abstracting timers, task spawning, and sockets, so the crate is runtime-agnostic

**📖 Learn more:** Read
our [architecture blog post](https://webrtc.rs/blog/2026/01/31/async-friendly-webrtc-architecture.html) for design
details and roadmap.

## Getting Started

Add the crate:

```toml
[dependencies]
webrtc = "0.21.0"
```

**Feature flags:**

| Feature            | Default | Description                                                             |
|--------------------|---------|-------------------------------------------------------------------------|
| `runtime-tokio`    | ✅       | Timers, task spawning and sockets via Tokio                             |
| `runtime-smol`     |         | The same, via smol                                                      |
| `runtime-mock`     |         | `MockRuntime`, a deterministic virtual-clock runtime for tests (no I/O) |
| `crypto-ring`      | ✅       | The `ring`-based crypto provider                                        |
| `crypto-aws-lc-rs` |         | The `aws-lc-rs`-based crypto provider                                   |

The runtime features are **additive**: each one only makes a built-in runtime *available*, so enabling several is safe
and a single process can drive different connections on different runtimes.

The crypto features work the same way. Enabling both compiles both providers, and `ring` stays the default selection —
so a dependency that turns on `crypto-aws-lc-rs` cannot silently change which one your application runs. Building with
neither compiles no provider, and you supply your own.

**Bringing your own runtime.** The built-ins are not privileged — implement `webrtc::runtime::Runtime` and pass it per
connection with `with_runtime`, with no `#[cfg]` edits and no fork. See
the [custom-runtime example](examples/custom-runtime), which runs the full stack on `async-executor` + `async-io` with
`--no-default-features` (neither Tokio nor smol compiled in).

**Choosing a crypto provider.** Same story: pass one per connection through `SettingEngine`, which also means two
connections in one process can use different providers.

```rust,ignore
use std::sync::Arc;
use webrtc::peer_connection::SettingEngineBuilder;
use webrtc::peer_connection::crypto;

let setting_engine = SettingEngineBuilder::new()
    .with_crypto_provider(Arc::new(crypto::providers::AwsLcRsProvider::new()))
    .build();
```

Applications needing a FIPS-validated module, an HSM, or a platform backend implement
`crypto::RTCCryptoProvider` and pass it the same way; `rtc-crypto`'s conformance suite validates
an implementation against the same RFC vectors the built-ins pass. No cryptography happens in this
crate — it forwards the provider to `rtc`.

Build a peer connection and create an offer:

```
/* Detailed source-code truncated for AI context efficiency. */
```

`build()` returns an opaque `impl PeerConnection`. `PeerConnection` is an object-safe trait, so when you need to store
the connection in a struct or share it across tasks, wrap it:

```rust,ignore
let pc: Arc<dyn PeerConnection> = Arc::new(pc);
```

Either way no runtime or interceptor type parameters leak into your own types.

**Next steps:** browse the [API docs](https://docs.rs/webrtc) or the
[39 runnable examples](https://github.com/webrtc-rs/webrtc/tree/master/examples) — data channels, media playback,
simulcast, ICE restart, insertable streams, and more.

### The architecture

✅ **Runtime independence**

- Runtime-agnostic via a Quinn-style `Runtime` abstraction (timers, task spawning, sockets, DNS)
- Feature flags: **`runtime-tokio`** (default) and **`runtime-smol`**, additive rather than mutually exclusive
- Any third-party runtime works today: implement `Runtime`, inject it per connection with `with_runtime`.
  The [custom-runtime example](examples/custom-runtime) does exactly that on `async-executor` + `async-io`, with neither
  built-in runtime compiled in
- **`runtime-mock`** gives tests a deterministic virtual clock, so timing-dependent behaviour is testable instantly and
  without sockets

✅ **Clean event handling**

- One trait-based event handler (`PeerConnectionEventHandler`) for every event, with default no-op methods so you
  implement only what you need — no per-event callback registration
- Centralized state: the handler is one shared `Arc<MyHandler>`. Methods take `&self`, so mutable handler state
  goes behind a single lock rather than being captured per closure

✅ **Sans-I/O foundation**

- Protocol logic completely separate from I/O (via the [rtc](https://github.com/webrtc-rs/rtc) core)
- Deterministic testing without real network I/O
- A thin async driver (`PeerConnection` handle + background `PeerConnectionDriver`) over the core

### Release lines

- **`v0.21.x`** — currently `v0.21.0`, and the recommended choice today. The pre-1.0 line, working towards a
  stable public API; see [what's in 0.21](#whats-in-021) below.
- **`v0.20.x`** — the previous line.

While the version is `0.x`, a minor bump may carry breaking changes
(see [Semantic Versioning](#semantic-versioning)). Once `1.0` ships, that stops being true — which is the whole
point of the `0.21` work.

### What's in 0.21

`v0.21` is the run-up to `1.0`, whose goal is a **stable public API** — not every feature, but an API that will
not break under you. Work so far:

✅ **Extensibility, before the freeze** — public enums are `#[non_exhaustive]` and the traits this crate alone
implements are sealed, so variants and methods can be added later without a major bump. This cannot be done
*after* `1.0`, which is why it came first.

✅ **A provider-neutral cryptographic API** — every cryptographic operation in the stack now goes through the
new `rtc-crypto` crate rather than a hard-wired backend. `RTCCryptoProvider` bundles an operations trait and a
CSPRNG; `crypto-ring` (default) and `crypto-aws-lc-rs` ship built in and are **additive** (enabling both builds,
and `ring` stays the default selection). The provider is chosen **per peer connection** — there is no
process-global provider to install — so two connections in one process can use different ones. Build with
neither feature and supply your own for OpenSSL, a FIPS-validated module, an HSM or a platform backend;
`rtc-crypto`'s conformance suite validates it against the same RFC vectors the built-ins pass. No cryptography
happens in this crate; a CI check enforces that.

✅ **Faster per-packet crypto** — key schedules moved off the per-packet path. SRTP keys its MACs once per
context (~40% faster per RTCP packet), DTLS CBC keys its record MACs at epoch setup (~4-7% faster), AES-CTR is
batched (~9-10% faster on a 1200-byte payload), and the built-in CSPRNG no longer reads the OS per record —
DTLS GCM record encryption went from 1.015 µs to 262 ns. Every per-packet benchmark is at parity with or
faster than the pre-migration baseline.

✅ **Deterministic time** — the Sans-I/O core no longer reads a clock. Time is an input, threaded from
`Runtime::now()`, so `runtime-mock`'s virtual clock genuinely drives ICE timeouts, DTLS retransmits and SCTP
RTO. Advancing a mock clock by 30 s now produces real protocol transitions instead of nothing.

✅ **No silent drops on data channels** — a slow consumer used to lose messages on a *reliable* channel once
the internal hand-off queue filled. The driver now keeps them and stops pulling from the core, so back-pressure
reaches SCTP's receive window and the peer is throttled instead. Media is unaffected by a stalled data channel.

✅ **Faster and more correct SCTP setup** — the initial T1-init/T1-cookie RTO is now the 1 s that RFC 9260 §16
recommends, rather than the 3 s inherited from RFC 4960, so a lost first handshake packet is retried a second
in rather than three. And an inbound `DATA_CHANNEL_OPEN` that arrives before the local association reaches
`Established` is now held until it does, instead of announcing a channel as open over a transport that would
reject the first thing written to it.

⚠️ **One behaviour change to note** — `MediaEngine::register_default_codecs` no longer registers
`video/ulpfec`, because the receive path does not recover media from ULPFEC packets. This changes the default
offer's SDP: payload type 116 is gone. Register it explicitly with `MIME_TYPE_ULP_FEC` if you need it.

Track the remaining work in [The path to webrtc 1.0](https://github.com/webrtc-rs/webrtc/issues/836).

### How to Provide Feedback

We welcome your input as `v0.20.x` and `v0.21.x` grow:

- Review the [architecture blog post](https://webrtc.rs/blog/2026/01/31/async-friendly-webrtc-architecture.html)
- Join discussions on [GitHub Issues](https://github.com/webrtc-rs/webrtc/issues)
- Chat with us on [Discord](https://discord.gg/4Ju8UHdXMs)

**New projects:** start on `v0.21.0`.  
**Hit a gap?** Open an issue — reports of what is missing directly shape what we prioritise before `1.0`.

## Building and Testing

```bash
# Update rtc submodule first
git submodule update --init --recursive

# Build the library
cargo build

# Run tests
cargo test

# Build documentation
cargo doc --open

# Run examples
cargo run --example data-channels
```

## Semantic Versioning

This project follows [Semantic Versioning](https://semver.org/):

- **Patch** (`0.x.Y`): Bug fixes and internal improvements with no public API changes.
- **Minor** (`0.X.0`): Backwards-compatible additions or deprecations to the public API.
- **Major** (`X.0.0`): Breaking changes to the public API.

While the version is `0.x`, the minor version acts as the major — i.e., a minor bump may include breaking changes. Once
`1.0.0` is released, full semver stability guarantees apply.

Pre-release versions are published with the following suffixes, in order of increasing stability:

- **`-alpha.N`**: Early preview. API is unstable and may change significantly.
- **`-beta.N`**: Feature-complete for the release. API may still have minor changes.
- **`-rc.N`**: Release candidate. No further API changes are expected unless critical issues are found.

For example: `1.0.0-alpha.1` → `1.0.0-beta.1` → `1.0.0-rc.1` → `1.0.0`.

## Open Source License

Dual licensing under both MIT and Apache-2.0 is the currently accepted standard by the Rust language community and has
been used for both the compiler and many public libraries since (
see <https://doc.rust-lang.org/1.6.0/complement-project-faq.html#why-dual-mitasl2-license>). In order to match the
community standards, webrtc-rs is using the dual MIT+Apache-2.0 license.

## Contributing

Contributors or Pull Requests are Welcome!!!

---

### Overview (`examples/README.md`)

<h1 align="center">
  Examples
</h1>

All examples are ported from [Pion](https://github.com/pion/webrtc/tree/master/examples#readme). Please
check [Pion Examples](https://github.com/pion/webrtc/tree/master/examples#readme) for more details:

### Data Channel API

- ✅ [Data Channels](data-channels): The data-channels example shows how you can send/recv DataChannel messages from a
  web browser.
- ✅ [Data Channels Create](data-channels-create): Example data-channels-create shows how you can send/recv DataChannel
  messages from a web browser. The difference with the data-channels example is that the data channel is initialized
  from the server side in this example.
- ✅ [Data Channels Close](data-channels-close): Example data-channels-close is a variant of data-channels that allow
  playing with the life cycle of data channels.
- ✅ [Data Channels Flow Control](data-channels-flow-control): Example data-channels-flow-control shows how to use flow
  control.
- ✅ [Data Channels Offer Answer](data-channels-offer-answer): Example offer-answer is an example of two webrtc-rs
  instances communicating directly!
- ✅ [Data Channels Simple](data-channels-simple): Simple example of a WebRTC DataChannel using it as the signaling
  server.

### Media API

- ✅ [Reflect](reflect): The reflect example demonstrates how to have webrtc-rs send back to the user exactly what it
  receives using the same PeerConnection.
- ✅ [Play from Disk VPx](play-from-disk-vpx): The play-from-disk-vpx example demonstrates how to send VP8/VP9 video to
  your browser from a file saved to disk.
- ✅ [Play from Disk H26x](play-from-disk-h26x): The play-from-disk-h26x example demonstrates how to send H264/H265 video
  to your browser from a file saved to disk.
- ✅ [Play from Disk Renegotiation](play-from-disk-renegotiation): The play-from-disk-renegotiation example is an
  extension of the play-from-disk example, but demonstrates how you can add/remove video tracks from an already
  negotiated PeerConnection.
- ✅ [Save to Disk VPx](save-to-disk-vpx): The save-to-disk-vpx example shows how to record your webcam and save the
  footage (VP8/VP9 for video, Opus for audio) to disk on the server side.
- ✅ [Save to Disk H26x](save-to-disk-h26x): The save-to-disk-h26x example shows how to record your webcam and save the
  footage (H264/H265 for video, Opus for audio) to disk on the server side.
- ✅ [Insertable Streams](insertable-streams): The insertable-streams example demonstrates how webrtc-rs can be used to
  send E2E encrypted video and decrypt via insertable streams in the browser.
- ✅ [Broadcast](broadcast): The broadcast example demonstrates how to broadcast a video to multiple peers. A
  broadcaster uploads the video once and the server forwards it to all other peers.
- ✅ [RTP Forwarder](rtp-forwarder): The rtp-forwarder example demonstrates how to forward your audio/video streams
  using RTP.
- ✅ [RTP to WebRTC](rtp-to-webrtc): The rtp-to-webrtc example demonstrates how to take RTP packets sent to a webrtc-rs
  process into your browser.
- ✅ [Simulcast](simulcast): The simulcast example demonstrates how to accept and demux 1 Track that contains 3
  Simulcast streams. It then returns the media as 3 independent Tracks back to the sender.
- ✅ [Swap Tracks](swap-tracks): The swap-tracks demonstrates how to swap multiple incoming tracks on a single outgoing
  track.
- ✅ [RTCP Processing](rtcp-processing): The rtcp-processing example demonstrates how a custom RtcpForwarderInterceptor
  narrows inbound RTCP to keyframe requests and marks them with `Attribute::DeliverToApplication`, which is what gets
  them past the end of the chain to the application.
- ✅ [Save to Disk AV1](save-to-disk-av1): The save-to-disk-av1 is a simple application that shows how to save a video to
  disk using AV1.
- ✅ [Play from Disk Playlist Control](play-from-disk-playlist-control): Streams Opus pages from multi or single track
  Ogg containers, exposes the playlist over an SCTP DataChannel, and lets the browser hop between tracks while showing
  artist/title metadata parsed from OpusTags.
- ✅ [Play from Disk FEC](play-from-disk-fec): The play-from-disk-fec demonstrates how to use FlexFEC-03 while sending
  video to your Chrome-based browser from a file saved to disk. A custom interceptor stands at the wire and drops media
  packets after they have been protected, so the browser recovers them from the repair stream.
- ✅ [Save to Disk FEC](save-to-disk-fec): The save-to-disk-fec demonstrates how to use the FlexFEC-03 decoder while
  receiving video from the play-from-disk-fec example, rebuilding the packets that example dropped and saving the
  result to disk.
- ✅ [Bandwidth Estimation From Disk](bandwidth-estimation-from-disk): bandwidth-estimation-from-disk demonstrates how to
  use Bandwidth Estimation APIs, switching between three pre-encoded renditions as the estimate moves.

### Miscellaneous

- ✅ [mDNS Query and Gather](mdns-query-and-gather) Example mdns-query-and-gather demonstrates webrtc-rs hides local ip
  with mDNS.
- ✅ [ICE Restart](ice-restart): The ice-restart demonstrates webrtc-rs ICE Restart abilities.
- ✅ [Trickle ICE Host](trickle-ice-host) Example demonstrates WebRTC's Trickle ICE APIs to add Host type local
  candidate.
- ✅ [Trickle ICE ServerReflexive](trickle-ice-srflx): Example demonstrates how to add ServerReflexive (STUN)
  type local candidate.
- ✅ [Trickle ICE Relay](trickle-ice-relay): Example demonstrates how to add Relay (TURN)
  type local candidate.
- ✅ [Trickle ICE](trickle-ice) Example trickle-ice demonstrates the comprehensive Trickle ICE APIs with all three types
  of ICE candidates. This is important to use since it allows ICE Gathering and Connecting to happen concurrently.
- ✅ [ICE TCP](ice-tcp) Example ice-tcp demonstrates how a WebRTC connection can be made over TCP instead of UDP. By
  default, webrtc-rs only does UDP. webrtc-rs can be configured to use a TCP port with passive mode.
- ✅ [ICE TCP Active-Passive](ice-tcp-active-passive) Example ice-tcp-active-passive demonstrates RTC's ICE TCP active
  mode abilities.
- ✅ [Stats](stats): Stats demonstrates how to use the webrtc-stats implementation provided by WebRTC-rs.
- ✅ [Custom Runtime](custom-runtime): Example custom-runtime demonstrates that webrtc-rs is runtime-agnostic by
  implementing the `Runtime` trait over `async-executor` + `async-io` — neither Tokio nor smol — and injecting it with
  `with_runtime`. Run it with `--no-default-features` to prove nothing in the library is hard-wired to a built-in
  runtime.

### Not Applicable

- [x] [ICE Single Port](N/A) This example doesn't apply to sansio RTC because sansio RTC is I/O-free by design: The
  library never creates or manages sockets. Your application creates UDP sockets and feeds data to handle_read(). Port
  multiplexing is already the application's responsibility.
- [x] [ICE Proxy](N/A) This example doesn't apply to sansio RTC too, since Sansio RTC is I/O-free - it never creates
  network connections. The application is responsible for all I/O.
- [x] [Data Channels WHIP WHEP](N/A): This example doesn't apply to sansio RTC, since it demonstrates a WHIP/WHEP-like
  implementation.
- [x] [WHIP WHEP](N/A): This example doesn't apply to sansio RTC, since it demonstrates using WHIP and WHEP.

---

### Overview (`examples/bandwidth-estimation-from-disk/README.md`)

# bandwidth-estimation-from-disk

bandwidth-estimation-from-disk demonstrates how to use RTC's Bandwidth Estimation APIs.

Send-side congestion control produces one number — how many bits per second the path looks willing to carry — and it is
the sender's job to meet it. This example meets it the crudest way that works: three IVF files encoded at 300 kbps,
1 Mbps and 2.5 Mbps, and a switch to whichever one fits.

## Instructions

### Create IVF files named `low.ivf`, `med.ivf` and `high.ivf`

```shell
ffmpeg -i $INPUT_FILE -g 30 -b:v .3M  -s 320x240   low.ivf
ffmpeg -i $INPUT_FILE -g 30 -b:v 1M   -s 858x480   med.ivf
ffmpeg -i $INPUT_FILE -g 30 -b:v 2.5M -s 1280x720  high.ivf
```

The bitrates matter: they are the numbers the estimate is compared against, and they are declared in
`QUALITY_LEVELS` in the source. Encoding at different rates than those without updating the table gives you a
demo that switches at the wrong moments.

`-g 30` matters too. A switch can only take effect at a keyframe, so the keyframe interval is the floor on how
quickly this example can react to the estimate.

### Open the bandwidth-estimation-from-disk example page

Open [jsfiddle.net](https://jsfiddle.net/a1cz42op/) in your browser. You should see two text-areas, a 'Start Session'
button and 'Copy browser Session Description to clipboard'.

### Run bandwidth-estimation-from-disk with your browser's Session Description as stdin

In the jsfiddle press 'Copy browser Session Description to clipboard', or copy the base64 string manually. Then:

```shell
echo $BROWSER_SDP | cargo run --example bandwidth-estimation-from-disk
```

The three `.ivf` files are read from the current directory by default; `-v/--video-dir` points it elsewhere. All three
must exist before the call starts — a missing file is rejected up front rather than at the moment the estimate says to
switch to it.

On Windows, paste the Session Description into a file and pass it with `-i`:

```shell
cargo run --example bandwidth-estimation-from-disk -- -i my_file
```

### Input bandwidth-estimation-from-disk's Session Description into your browser

Copy the text that `bandwidth-estimation-from-disk` just emitted and paste it into the second text area in the
jsfiddle.

### Hit 'Start Session' in jsfiddle, enjoy your video!

A video should start playing in your browser above the input boxes. When the example switches quality levels it prints
the old and new file:

```text
Switching from low.ivf to med.ivf
Switching from med.ivf to high.ivf
Switching from high.ivf to med.ivf
```

It starts at `low.ivf` and climbs from there. Beginning at the highest rendition instead would open the call by
congesting the very path it is still trying to measure.

To see it move, constrain the path — throttle the interface, or run over a congested link. On an unloaded loopback
connection the estimate will climb to `high.ivf` and stay there, which is correct behaviour and an uninteresting demo.

## How the estimate reaches the application

`configure_congestion_control` takes the estimator by value, and it ends up boxed inside the interceptor chain where
the application cannot reach it. So the number is pushed rather than pulled:

```rust
struct ReportingEstimator<E: BandwidthEstimator> {
    inner: E,
    target: watch::Sender<f64>,
}
```

`ReportingEstimator` wraps the real estimator, forwards every method, and publishes the target on a `watch` channel
after each call that can move it. The streaming task reads `*target_rx.borrow()` on every frame.

Publishing happens after `on_reports` and `handle_timeout` — not in `target_bitrate`, which takes `&self` and could not
publish even if it wanted to. Those two are the points the interceptor's contract names as the ones where the estimate
can change.

That wrapper is the whole of the integration, and it is worth noticing what it is *not*: no callback registration, no
event variant, no new peer-connection API. A `BandwidthEstimator` is a function from acknowledgements to a number, so
anything that wants to watch that number can sit exactly where the algorithm does.

## What makes a switch decodable

`switch_quality_level` reopens the new rendition and then skips forward until it finds a frame satisfying two
conditions. Dropping either one produces a switch that still looks like it worked:

- **It must be a keyframe** (VP8 frame type bit, RFC 6386 §9.1). An interframe references reference frames from a file
  the receiver was never sent, and decodes to a smear until the next keyframe happens along.
- **Its timestamp must be at or after the last one sent.** Otherwise the stream jumps backwards in time and the
  receiver discards everything until it catches up to where it already was.

One packetizer spans every rendition, so sequence numbers and timestamps stay continuous across a switch. To the
receiver this is one stream whose content changes resolution — restarting either would look like the stream had been
replaced.

Congrats, you have used WebRTC-rs!

---

### Overview (`examples/broadcast/README.md`)

# broadcast

broadcast is a WebRTC.rs application that demonstrates how to broadcast a video to many peers, while only requiring the broadcaster to upload once.

This could serve as the building block to building conferencing software, and other applications where publishers are bandwidth constrained.

## Instructions

### Build broadcast

```shell
cargo build --example broadcast
```

### Open broadcast example page

[jsfiddle.net](https://jsfiddle.net/1jc4go7v/) You should see two buttons 'Publish a Broadcast' and 'Join a Broadcast'

### Run Broadcast

#### Linux/macOS

Run `broadcast`

### Start a publisher

* Click `Publish a Broadcast`
* Copy the string in the first input labelled `Browser base64 Session Description`
* Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step.
* The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser.
* Press `Start Session`
* The connection state will be printed in the terminal and under `logs` in the browser.

### Join the broadcast

* Click `Join a Broadcast`
* Copy the string in the first input labelled `Browser base64 Session Description`
* Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step.
* The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser.
* Press `Start Session`
* The connection state will be printed in the terminal and under `logs` in the browser.

You can change the listening port using `-port 8011`

You can `Join the broadcast` as many times as you want. The `broadcast` application is relaying all traffic, so your browser only has to upload once.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/custom-runtime/README.md`)

# custom-runtime

custom-runtime demonstrates that WebRTC.rs is **genuinely runtime-agnostic**: it implements the `webrtc::runtime::Runtime` trait over [`async-executor`](https://crates.io/crates/async-executor) and [`async-io`](https://crates.io/crates/async-io) — neither Tokio nor smol — and drives every runtime capability the WebRTC stack needs through it.

It doubles as the acceptance test for the runtime abstraction. Building it with **neither** `runtime-tokio` nor `runtime-smol` proves nothing in the library is hard-wired to a built-in runtime.

## Instructions

### Build custom-runtime

```shell
cargo build --example custom-runtime
```

### Run custom-runtime

The interesting invocation disables both built-in runtimes, so the only `Runtime` in the process is the one the example defines:

```shell
cargo run --no-default-features --example custom-runtime
```

Expected output:

```text
running on a custom runtime: my-runtime
sleep(50ms) took 50.304959ms
tick 0
tick 1
tick 2
timeout on a pending future => Err(Elapsed)
hello from a spawned task
udp: received "ping" from 127.0.0.1:54321
resolved localhost:3478 -> [[::1]:3478, 127.0.0.1:3478]
```

Socket binding may be denied in restricted environments (sandboxes, some CI); the UDP step then reports `udp: skipped` rather than failing.

## What it implements

Eight required `Runtime` methods, plus the three socket traits:

| Method                                        | Backed by                                    |
|-----------------------------------------------|----------------------------------------------|
| `spawn`                                       | `async_executor::Executor` on a 2-thread pool |
| `sleep`, `interval`                           | `async_io::Timer`                            |
| `wrap_udp_socket`, `wrap_tcp_listener`, `connect_tcp` | `async_io::Async<T>`                 |
| `resolve_host`                                | `std`'s resolver, offloaded to a thread      |
| `block_on`                                    | `futures_lite::future::block_on`             |

`spawn_reactor`, `yield_now` and `name` have working defaults, so they are optional. `timeout` needs no implementation at all — it is derived generically from `sleep`.

The socket wrapper sends and receives one datagram per syscall: it ignores `Transmit::segment_size` and leaves `max_gso_segments` / `max_gro_segments` at their default of 1, so the driver never asks it to batch. A production runtime would enable UDP GSO/GRO the way the built-in ones do — construct a `webrtc::runtime::UdpSocketState` at wrap time, report its `max_gso_segments()` / `gro_segments()`, and pass the whole `Transmit` to its `send`. Its `recv` already takes the same `bufs` / `meta` slices as `poll_recv` and returns the same message count, so the receive path forwards them rather than adapting anything; what it does need is to translate a count of `0` into the host runtime's not-ready protocol, and to ensure every filled message reports a `stride` of at least 1. See `src/runtime/tokio.rs` or `smol.rs`.

Reporting `max_gro_segments() > 1` is the one place a wrapper can quietly hurt itself. It tells the driver how large a receive buffer to allocate, and a coalesced segment is bounded by the **path MTU**, not by the largest datagram your application sends — size against the latter and the kernel drops the tail of each coalesced read, which shows up as unexplained packet loss rather than an error. The driver handles this for buffers it allocates; a runtime that reads into buffers of its own, such as one demultiplexing a shared socket across several connections, owns the same bound in its own loop. `AsyncUdpSocket::max_gro_segments` documents the rule.

## Using it for a real connection

The runtime is injected **per connection**, so a single process can run some connections on a custom runtime and others on a built-in one:

```rust
let runtime: Arc<dyn Runtime> = Arc::new(MyRuntime::new());

let pc = PeerConnectionBuilder::new()
    .with_runtime(runtime.clone())
    .with_handler(Arc::new(MyHandler))
    .with_udp_addrs(vec!["0.0.0.0:0"])
    .build()
    .await?;
```

There is no global registry to install into and nothing to override — which is what keeps per-connection runtime choice possible.

## Writing your own

1. Implement `Runtime` (the 8 required methods above).
2. Implement `AsyncUdpSocket`, `AsyncTcpListener` and `AsyncTcpStream` for your socket types. `AsyncUdpSocket` is poll-based: supply `poll_send` and `poll_recv`, and the four `async` convenience methods come from defaults.
3. Return your task handle from `spawn` via `JoinHandle::new(Box::new(..))`, implementing `JoinHandleInner` for it.
4. Pass it to `with_runtime`.

No `#[cfg]` edits, no fork, and no changes to library internals.

One thing to get right, and the reason this example is worth reading: **`poll_send`/`poll_recv` must be ordered to match your reactor's readiness semantics.** Event- or ticket-based reactors (async-io, and this example) must attempt the syscall *first* and consult readiness only on `WouldBlock` — otherwise datagrams already queued in the socket buffer generate no fresh readiness event and a burst stalls. Reactors with cached, level-triggered readiness (Tokio) instead need the operation to report `WouldBlock` back through their own wrapper (`try_io`) so the cache is invalidated.

---

### Overview (`examples/data-channels-close/README.md`)

# data-channels-close
data-channels-close is a variant of the data-channels example that allow playing with the life cycle of data channels.

---

### Overview (`examples/data-channels-create/README.md`)

# data-channels-create

data-channels-create is a WebRTC.rs application that shows how you can send/recv DataChannel messages from a web browser. The difference with the data-channels example is that the datachannel is initialized from the WebRTC.rs side in this example.

## Instructions

### Build data-channels-create

```shell
cargo build --example data-channels-create
```

### Open data-channels-create example page

[jsfiddle.net](https://jsfiddle.net/swgxrp94/20/)

### Run data-channels-create

Just run `data-channels-create`.

### Input data-channels-create's SessionDescription into your browser

Copy the text that `data-channels-create` just emitted and copy into first text area of the jsfiddle.

### Hit 'Start Session' in jsfiddle

Hit the 'Start Session' button in the browser. You should see `have-remote-offer` below the `Send Message` button.

### Input browser's SessionDescription into data-channels-create

Meanwhile text has appeared in the second text area of the jsfiddle. Copy the text and paste it into `data-channels-create` and hit ENTER.
In the browser you'll now see `connected` as the connection is created. If everything worked you should see `New DataChannel data`.

Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal!

WebRTC.rs will send random messages every 5 seconds that will appear in your browser.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/data-channels-flow-control/README.md`)

# data-channels-flow-control

This example demonstrates how to use the following property / methods.

* pub async fn buffered_amount(&self) -> usize
* pub async fn set_buffered_amount_low_threshold(&self, th: usize)
* pub async fn buffered_amount_low_threshold(&self) -> usize
* pub async fn on_buffered_amount_low(&self, f: OnBufferedAmountLowFn)

These methods are equivalent to that of JavaScript WebRTC API.
See <https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel> for more details.

## When do we need it?

Send or SendText methods are called on DataChannel to send data to the connected peer.
The methods return immediately, but it does not mean the data was actually sent onto
the wire. Instead, it is queued in a buffer until it actually gets sent out to the wire.

When you have a large amount of data to send, it is an application's responsibility to
control the buffered amount in order not to indefinitely grow the buffer size to eventually
exhaust the memory.

The rate you wish to send data might be much higher than the rate the data channel can
actually send to the peer over the Internet. The above properties/methods help your
application to pace the amount of data to be pushed into the data channel.

## How to run the example code

The demo code implements two endpoints (requester and responder) in it.

```plain
                        signaling messages
           +----------------------------------------+
           |                                        |
           v                                        v
   +---------------+                        +---------------+
   |               |          data          |               |
   |   requester   |----------------------->|   responder   |
   |:PeerConnection|                        |:PeerConnection|
   +---------------+                        +---------------+
```

First requester and responder will exchange signaling message to establish a peer-to-peer
connection, and data channel (label: "data").

Once the data channel is successfully opened, requester will start sending a series of
1024-byte packets to responder, until you kill the process by Ctrl+С.

Here's how to run the code:

```shell
$ cargo run --release --example data-channels-flow-control
    Finished release [optimized] target(s) in 0.36s
     Running `target\release\examples\data-channels-flow-control.exe`

Throughput is about 127.060 Mbps
Throughput is about 122.091 Mbps
Throughput is about 120.630 Mbps
Throughput is about 120.105 Mbps
Throughput is about 119.873 Mbps
Throughput is about 118.890 Mbps
Throughput is about 118.525 Mbps
Throughput is about 118.614 Mbps
```

---

### Overview (`examples/data-channels-offer-answer/README.md`)

# data-channels-offer-answer

data-channels-offer-answer is an example of two webrtc-rs instances communicating directly!

The SDP offer and answer are exchanged automatically over HTTP.
The `data-channels-answer` side acts like a HTTP server and should therefore be ran first.

`hyper` v0.14 requires a Tokio runtime (it uses `tokio::net::TcpListener` internally via `Server::bind`). When we run
under `runtime-smol`, there's no Tokio
reactor, so `hyper` panics. Therefore, this example must be ran in default runtime-tokio.

## Instructions

First run `data-channels-answer`:

```shell
cargo run --example data-channels-answer
```

Next, run `data-channels-offer`:

```shell
cargo run --example data-channels-offer
```

You should see them connect and start to exchange messages.

---

### Overview (`examples/data-channels-simple/README.md`)

# WebRTC DataChannel Example in Rust

This is a minimal example of a **WebRTC DataChannel** using **Rust (sansio RTC)** as the signaling server.

`hyper` v0.14 requires a Tokio runtime (it uses `tokio::net::TcpListener` internally via `Server::bind`). When we run
under `runtime-smol`, there's no Tokio
reactor, so `hyper` panics. Therefore, this example must be ran in default runtime-tokio.

## Features

- Rust HTTP server for signaling (using hyper)
- Browser-based DataChannel
- ICE candidate exchange
- Real-time messaging between browser and Rust server

## Usage

1. Run the server:

```bash
cargo run --example data-channels-simple
```

2. Open browser at http://localhost:8080

3. Send messages via DataChannel and see them in terminal & browser logs.

## How It Works

1. The browser creates a DataChannel and generates an SDP offer
2. The offer is sent to the Rust server via HTTP POST to `/offer`
3. The server creates a PeerConnection, processes the offer, and returns an answer
4. The browser sets the answer as remote description
5. ICE candidates are exchanged via HTTP POST to `/candidate`
6. Once connected, messages can be sent bidirectionally through the DataChannel

## Architecture

```text
┌─────────────┐                     ┌──────────────────┐
│   Browser   │                     │   Rust Server    │
│             │                     │                  │
│ DataChannel │◄──── WebRTC ───────►│ RTCPeerConnection|
│             │      (UDP)          │                  │
│  HTTP POST  │────────────────────►│  /offer          │
│  HTTP POST  │────────────────────►│  /candidate      │
└─────────────┘                     └──────────────────┘
```

## API Endpoints

| Endpoint     | Method | Description                      |
|--------------|--------|----------------------------------|
| `/`          | GET    | Serve demo HTML page             |
| `/offer`     | POST   | Receive SDP offer, return answer |
| `/candidate` | POST   | Receive ICE candidate            |

---

### Overview (`examples/data-channels/README.md`)

# data-channels

data-channels is a WebRTC.rs application that shows how you can send/recv DataChannel messages from a web browser

## Instructions

### Build data-channels

```shell
cargo build --example data-channels
```

### Open data-channels example page

[jsfiddle.net](https://jsfiddle.net/9tsx15mg/90/)

### Run data-channels, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser's session description, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/data-channels`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/data-channels < my_file`

### Input data-channels's SessionDescription into your browser

Copy the text that `data-channels` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle

Under Start Session you should see 'Checking' as it starts connecting. If everything worked you should see `New DataChannel foo 1`

Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal!

WebRTC.rs will send random messages every 5 seconds that will appear in your browser.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/ice-restart/README.md`)

# ice-restart

ice-restart demonstrates WebRTC.rs ICE Restart abilities.

`hyper` v0.14 requires a Tokio runtime (it uses `tokio::net::TcpListener` internally via `Server::bind`). When we run
under `runtime-smol`, there's no Tokio
reactor, so `hyper` panics. Therefore, this example must be ran in default runtime-tokio.

## Instructions

### Build ice-restart

```shell
cargo build --example ice-restart
```

### Run ice-restart

```shell
cargo run --example ice-restart
```

### Open the Web UI

Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now
prints stats about the PeerConnection
and allow you to do an ICE Restart at anytime.

* `ICE Restart` is the button that causes a new offer to be made with `iceRestart: true`.
* `ICE Connection States` will contain all the connection states the PeerConnection moves through.
* `ICE Selected Pairs` will print the selected pair every 3 seconds. Note how the uFrag/uPwd/Port change everytime you
  start the Restart process.
* `Inbound DataChannel Messages` containing the current time sent by the Pion process every 3 seconds.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/ice-tcp-active-passive/README.md`)

# ICE TCP Active/Passive Example

This example demonstrates Rust-to-Rust WebRTC connectivity over TCP (RFC 4571) using the async `webrtc` API. It showcases the difference between active and passive candidate pairing.

## About ICE TCP Types

ICE over TCP defines three connection types (RFC 6544):

- **Passive**: Listens for incoming TCP connections (like a server)
- **Active**: Initiates outgoing TCP connections (like a client)
- **Simultaneous-Open (S-O)**: Both sides attempt connections simultaneously

This example demonstrates **Active** mode on the offering side and **Passive** mode on the answering side.

## How It Works

1. **Answer side** (TCP passive):
   - Configures the `PeerConnection` with a passive candidate listening on a TCP port (default: `8443`).
   - Serves an HTTP signaling server on port `60000` to receive the SDP offer.
   - Sets the remote description, generates an answer, and returns it.

2. **Offer side** (TCP active):
   - Configures the `PeerConnection` with an active candidate (bound to an ephemeral port).
   - The active candidate generates a candidate with port `9` (discard port) for SDP signaling.
   - Generates an SDP offer and POSTs it to the answerer's HTTP signaling server.
   - Sets the answer returned by the passive side.
   - Initiates an outbound TCP connection to the answerer's passive port (`8443`).

3. **Data Channel Exchange**:
   - Once connected, both sides establish the data channel and exchange periodic timestamp messages every 3 seconds.

## Running the Example

### 1. Run the Answer Side (Passive)
From the root of the project, run:

```bash
cargo run --example ice-tcp-passive-answer
```

### 2. Run the Offer Side (Active)
In a separate terminal, run:

```bash
cargo run --example ice-tcp-active-offer
```

For detailed logging, append the `--debug` flag:

```bash
cargo run --example ice-tcp-passive-answer -- --debug
cargo run --example ice-tcp-active-offer -- --debug
```

---

### Overview (`examples/ice-tcp/README.md`)

# ICE TCP Example

This example demonstrates how to establish a WebRTC connection exclusively over TCP (RFC 4571) using the async `webrtc`
API.

## About ICE TCP

ICE TCP is useful when UDP traffic is blocked by restrictive firewalls. WebRTC normally uses UDP for media and data
transport, but can fall back to TCP when necessary.

This example showcases:

- Configuring the `PeerConnection` to only gather and listen on TCP candidates using `.with_tcp_addrs()` and leaving UDP
  addresses empty.
- Customizing the DTLS role via `SettingEngineBuilder::with_answering_dtls_role(RTCDtlsRole::Client)` to act as the DTLS
  client when answering the connection.
- Serving a local web interface using `hyper` to serve static pages and handle SDP signaling.
- Establishing a `DataChannel` over pure TCP and exchanging periodic messages.

## How It Works

1. The server starts an HTTP server on port `8080` and configures a WebRTC TCP listener on port `8443`.
2. When a browser opens `http://localhost:8080`, it generates an SDP offer with both TCP and UDP candidates.
3. The browser sends this SDP offer to the server via an HTTP `POST` to `/doSignaling`.
4. The server configures the `PeerConnection`, sets the remote offer, and generates an answer.
5. The server waits for local ICE gathering to complete (disabling trickle ICE) so that the answer includes the server's
   TCP passive candidate.
6. The server responds with the local SDP answer.
7. The browser sets the answer and initiates an active TCP connection to the server's listener on port `8443`.
8. Once connected, a `DataChannel` is established, and the server sends timestamp messages every 3 seconds.

## Running the Example

### 1. Run the Server

From the root of the project, run:

```bash
cargo run --example ice-tcp
```

For detailed trace logging, use:

```bash
cargo run --example ice-tcp -- --debug
```

### 2. Access the Web UI

Open your browser and navigate to:

```
http://localhost:8080
```

You will see:

- Real-time updates of the ICE connection state.
- Inbound messages received from the server over the established TCP WebRTC data channel.

---

### Overview (`examples/insertable-streams/README.md`)

# insertable-streams

insertable-streams demonstrates how to use insertable streams with WebRTC.rs.
This example modifies the video with a single-byte XOR cipher before sending, and then
decrypts in Javascript.

insertable-streams allows the browser to process encoded video. You could implement
E2E encryption, add metadata or insert a completely different video feed!

## Instructions

### Create IVF named `output.ivf` that contains a VP8 track

```shell
ffmpeg -i $INPUT_FILE -g 30 output.ivf
```

### Build insertable-streams

```shell
cargo build --example insertable-streams
```

### Open insertable-streams example page

[jsfiddle.net](https://jsfiddle.net/uqr80Lak/) you should see two text-areas and a 'Start Session' button. You will also have a 'Decrypt' checkbox.
When unchecked the browser will not decrypt the incoming video stream, so it will stop playing or display certificates.

### Run insertable-streams with your browsers SessionDescription as stdin

The `output.ivf` you created should be in the same directory as `insertable-streams`. In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/insertable-streams`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/insertable-streams < my_file`

### Input insertable-streams's SessionDescription into your browser

Copy the text that `insertable-streams` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

A video should start playing in your browser above the input boxes. `insertable-streams` will exit when the file reaches the end.

To stop decrypting the stream uncheck the box and the video will not be viewable.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/mdns-query-and-gather/README.md`)

# mdns-query-and-gather

`mdns-query-and-gather` is an async `webrtc` example that shows how to hide a
local host ICE candidate behind an mDNS name.

## Instructions

### Open the mdns-query-and-gather example page

[jsfiddle.net](https://jsfiddle.net/e41tgovp/)

### Run mdns-query-and-gather with your browser's SessionDescription as stdin

In the jsfiddle the top textarea is your browser's session description, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | cargo run --example mdns-query-and-gather`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `cargo run --example mdns-query-and-gather < my_file`

### Input mdns-query-and-gather's SessionDescription into your browser

Copy the text that `mdns-query-and-gather` just emitted and copy it into the second text area.

### Hit `Start Session` in jsfiddle

Under Start Session you should see `Checking` as it starts connecting. If everything worked you should see
`Data channel 'foo'-'1' open`.

Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal and echo back.

---

### Overview (`examples/play-from-disk-fec/README.md`)

# play-from-disk-fec

play-from-disk-fec demonstrates how to use forward error correction (FlexFEC-03) while sending video to your
Chrome-based browser from a file saved to disk. The example deliberately drops media packets on the way out, *after*
they have been protected, so you can watch the browser rebuild them from the repair stream.

## Instructions

### Create an IVF named `output.ivf` that contains a VP8 track

```shell
ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf
```

**Note**: `-b:v 2M` sets the video bitrate to 2 megabits per second. That default gives decent quality, but if you see
problems (dropped frames and so on) you can lower it. See the [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#Options)
for the format of the value.

The repository already ships a usable file at `rtc/examples/test-data/output_vp8.ivf`, which the commands below use.

### Open the play-from-disk-fec example page

Open the `jsfiddle/` page in this directory (or [jsfiddle.net](https://jsfiddle.net/hgzwr9cm/)) in Chrome. You should
see two text-areas and a 'Start Session' button.

### Run play-from-disk-fec to generate an offer

Unlike most examples here, **play-from-disk-fec offers and the browser answers.** That is not a stylistic choice:
Chrome accepts `video/flexfec-03` when it is offered to it, but does not put it in its own offers. If the browser
offered, there would be no FEC payload type to select and the example would show dropped video and no recovery.

```shell
cargo run --example play-from-disk-fec -- -v rtc/examples/test-data/output_vp8.ivf
```

It prints the offer in base64 to stdout.

### Paste the offer into your browser

Copy the base64 offer and paste it into the first text area in the jsfiddle (labeled "Remote Session Description").

### Hit 'Start Session' in the jsfiddle to generate an answer

The browser processes the offer and puts its answer in the second text area.

### Feed the answer back

Copy the base64 answer from the second text area and paste it into the terminal where `play-from-disk-fec` is waiting,
then press Enter.

Or run it with `-i` and save the answer to that file instead. The file is not read until you press Enter, so you can
start the example first and write the file afterwards:

```shell
cargo run --example play-from-disk-fec -- -v rtc/examples/test-data/output_vp8.ivf -i answer.txt
```

### Enjoy your video

A video should start playing in the browser above the input boxes. `play-from-disk-fec` exits when the file reaches
the end.

While it runs, it prints a line like this every 100 media packets:

```text
Stats: Media: 500, FEC: 50, Dropped: 100, Drop ratio: 20.0000%
```

## What the example actually does

Two interceptors do the work, and their order is the whole point:

| Slot | Interceptor | Role |
|---|---|---|
| 5_000 | `FlexFec03Send` (`Slot::FecEncoder`) | builds the repair block |
| … | everything `register_default_interceptors` adds, plus congestion control | |
| 500 | `DropFilter` (in this example) | the "network", discarding media packets |

The write walk runs from the application down to the wire, so the *higher* slot is reached first. The encoder sees the
complete media stream and computes repair over it. `DropFilter` then runs last of all — below every built-in
interceptor — and throws some of that media away. The repair packets still go out, so the browser can reconstruct what
was lost.

Standing at the wire is what makes it a path simulator rather than a sender bug. Every slot above it changes what is
being demonstrated:

- **Above the FEC encoder**, nothing is ever protected. The encoder would compute repair for a stream the network never
  carried, the browser would see the same holes with nothing to fill them from, and the example would look like it was
  working while demonstrating nothing.
- **Above the NACK responder**, dropped packets never enter the retransmission buffer, so the sender cannot answer a
  NACK for a packet the network lost.
- **Above congestion control**, the estimator undercounts what was actually put on the wire.

This is where pion's `packetDropInterceptorFactory` ends up too: pion registers it first, and in pion's chain the
first-registered interceptor is the one closest to the wire.

Repair packets are never dropped: `DropFilter` learns the FEC SSRC from `StreamInfo::ssrc_fec` when the stream binds
and lets that SSRC through untouched.

### Tuning the loss

`--drop-one-in N` drops one media packet in every N (default 5, so 20%); `--drop-one-in 0` disables dropping entirely,
which is the way to see what the stream looks like without the loss this example exists to survive.

The repair rate is one FEC packet per ten media packets, which recovers a *single* loss anywhere in a block of ten. At
the default one-in-five the browser will still see gaps — that is the honest picture, not a bug. FEC narrows the loss
it is sized for and no more, and a block that loses two of ten is not recoverable however the repair is arranged. Try
`--drop-one-in 20` to see the recovery keep up.

Congrats, you have used WebRTC-rs!

---

### Overview (`examples/play-from-disk-h26x/README.md`)

# play-from-disk-h26x

play-from-disk-h26x demonstrates how to send h264/h265 video and/or audio to your browser from files saved to disk.

## Instructions

### Create video file named `output.264` or `output.265` that contains a H264 or H265 track and

`output.ogg` that contains a Opus track

```shell
ffmpeg -i $INPUT_FILE -an -c:v libx264 -bsf:v h264_mp4toannexb -b:v 2M -max_delay 0 -bf 0 output.264
ffmpeg -i $INPUT_FILE -an -c:v libx265 -bsf:v hevc_mp4toannexb -b:v 2M -max_delay 0 -bf 0 output.265
ffmpeg -i $INPUT_FILE -c:a libopus -page_duration 20000 -vn output.ogg
```

### Open play-from-disk-h26x example page

[jsfiddle.net](https://jsfiddle.net/9s10amwL/) you should see two text-areas and a 'Start Session' button

### Run play-from-disk-h26x with your browsers SessionDescription as stdin

The `output.264` or `output.265` you created should be in the same directory as `play-from-disk-h26x`. In the jsfiddle
the top textarea is your browser, copy that and:

#### Linux/macOS

1. Run
   `echo $BROWSER_SDP | cargo run --example play-from-disk-h26x -- -v rtc/examples/test-data/output.264 -a rtc/examples/test-data/output.ogg`

2. Run
   `echo $BROWSER_SDP | cargo run --example play-from-disk-h26x -- -v rtc/examples/test-data/output.265 -a rtc/examples/test-data/output.ogg --hevc`

#### Windows

1. Paste the SessionDescription into a file.
2. Run
   `cargo run --example play-from-disk-h26x -v rtc/examples/test-data/output.264 -a rtc/examples/test-data/output.ogg < my_file`
3. Run
   `cargo run --example play-from-disk-h26x -v rtc/examples/test-data/output.265 -a rtc/examples/test-data/output.ogg --hevc < my_file`

### Input play-from-disk-h26x's SessionDescription into your browser

Copy the text that `play-from-disk-h26x` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

A video should start playing in your browser above the input boxes. `play-from-disk-h26x` will exit when the file
reaches the end

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/play-from-disk-playlist-control/README.md`)

# play-from-disk-playlist-control

Streams Opus pages from multi or single-track Ogg containers, exposes the playlist over an SCTP DataChannel, and lets
the browser hop between tracks while showing artist/title metadata parsed from OpusTags.

`hyper` v0.14 requires a Tokio runtime. This example is intended to run with the default `runtime-tokio` feature.

## What this showcases

- Reads multi-stream Ogg containers with `OggReader` and keeps per-track playback state.
- Publishes playlist + now-playing metadata over a DataChannel.
- Browser can send `next`, `prev`, or a 1-based track number to jump around.
- Audio is sent as Opus over RTP while metadata/control ride over SCTP.

## Prepare a demo playlist

By default, the example looks for `playlist.ogg` in the working directory. You can specify a different file with
`--playlist-file`.

**Fake two-track Ogg with metadata**

```sh
ffmpeg \
  -f lavfi -t 8 -i "sine=frequency=330" \
  -f lavfi -t 8 -i "sine=frequency=660" \
  -map 0:a -map 1:a \
  -c:a libopus -page_duration 20000 \
  -metadata:s:a:0 artist="WebRTC-rs Artist" -metadata:s:a:0 title="Fake Intro" \
  -metadata:s:a:1 artist="Open-Source Friend" -metadata:s:a:1 title="Fake Outro" \
  playlist.ogg
```

## Run it

```sh
cargo run --example play-from-disk-playlist-control
```

Or with a custom address and playlist file:

```sh
cargo run --example play-from-disk-playlist-control -- --addr 127.0.0.1:8080 --playlist-file my_music.ogg
```

Then open:

```text
http://127.0.0.1:8080
```

## Command-line options

```text
-a, --addr <ADDR>                Server address [default: 127.0.0.1:8080]
-d, --debug                      Enable debug logging
-l, --log-level <LOG_LEVEL>      Log level [default: INFO]
-o, --output-log-file <FILE>     Output log to file
-p, --playlist-file <FILE>       Playlist OGG file [default: playlist.ogg]
```

---

### Overview (`examples/play-from-disk-renegotiation/README.md`)

# play-from-disk-renegotiation

play-from-disk-renegotiation demonstrates WebRTC.rs's renegotiation abilities.

For a simpler example of playing a file from disk we also have [examples/play-from-disk](/examples/play-from-disk)

`hyper` v0.14 requires a Tokio runtime (it uses `tokio::net::TcpListener` internally via `Server::bind`). When we run
under `runtime-smol`, there's no Tokio
reactor, so `hyper` panics. Therefore, this example must be ran in default runtime-tokio.

## Instructions

### Build play-from-disk-renegotiation

```shell
cargo build --example play-from-disk-renegotiation
```

### Create IVF named `output.ivf` that contains a VP8 track

```shell
ffmpeg -i $INPUT_FILE -g 30 output.ivf
```

### Run play-from-disk-renegotiation

The `output.ivf` you created should be in the same directory as `play-from-disk-renegotiation`.

### Open the Web UI

Open [http://localhost:8080](http://localhost:8080) and you should have a `Add Track` and `Remove Track` button. Press
these to add as many tracks as you want, or to remove as many as you wish.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/play-from-disk-vpx/README.md`)

# play-from-disk-vpx

play-from-disk-vpx demonstrates how to send vp8/vp8 video and/or audio to your browser from files saved to disk.

## Instructions

### Create IVF named `output_vp8.ivf` or `output_vp9.ivf` that contains a VP8/VP9 track and/or

`output.ogg` that contains a Opus track

```shell
ffmpeg -i $INPUT_FILE -g 30 output_vp8.ivf
ffmpeg -i $INPUT_FILE -g 30 -c libvpx-vp9 output_vp9.ivf
ffmpeg -i $INPUT_FILE -map 0:a -c:a dca -ac 2 -c:a libopus -page_duration 20000 -vn output.ogg
```

### Build play-from-disk-vpx

```shell
cargo build --example play-from-disk-vpx
```

### Open play-from-disk-vpx example page

[jsfiddle.net](https://jsfiddle.net/9s10amwL/) you should see two text-areas and a 'Start Session' button

### Run play-from-disk-vpx with your browsers SessionDescription as stdin

The `output_vp8.ivf`/`output_vp9.ivf` you created should be in the same directory as `play-from-disk-vpx`. In the
jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

1. Run
   `echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp8.ivf -a rtc/examples/test-data/output.ogg`
2. Run
   `echo $BROWSER_SDP | ./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp9.ivf -a rtc/examples/test-data/output.ogg --vp9`

#### Windows

1. Paste the SessionDescription into a file.
2. Run
   `./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp8.ivf -a rtc/examples/test-data/output.ogg < my_file`
3. Run
   `./target/debug/examples/play-from-disk-vpx -v rtc/examples/test-data/output_vp9.ivf -a rtc/examples/test-data/output.ogg --vp9 < my_file`

### Input play-from-disk-vpx's SessionDescription into your browser

Copy the text that `play-from-disk-vpx` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

A video should start playing in your browser above the input boxes. `play-from-disk-vpx` will exit when the file reaches
the end

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/reflect/README.md`)

# reflect

reflect demonstrates how with one PeerConnection you can send video to webrtc-rs and have the packets sent back. This example could be easily extended to do server side processing.

## Instructions

### Build reflect

```shell
cargo build --example reflect
```

### Open reflect example page

[jsfiddle.net](https://jsfiddle.net/9jgukzt1/) you should see two text-areas and a 'Start Session' button.

### Run reflect, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/reflect -a -v`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/reflect -a -v < my_file`

### Input reflect's SessionDescription into your browser

Copy the text that `reflect` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

Your browser should send video to webrtc-rs, and then it will be relayed right back to you.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/rtcp-processing/README.md`)

# rtcp-processing

`rtcp-processing` demonstrates the async WebRTC API for processing RTCP packets.

## What it shows

1. Building a `PeerConnection` with the async `PeerConnectionBuilder` pattern used by the other async examples.
2. Adding a custom `RtcpForwarderInterceptor` that narrows inbound RTCP to keyframe requests — PLI and FIR — and marks
   those with `Attribute::DeliverToApplication` so they reach the application instead of stopping at the end of the chain.
4. Receiving those requests on the async side through `TrackRemoteEvent::OnRtcpPacket`.
5. Printing their headers and human-readable bodies as media flows.

## Why a packet has to be marked

By default, inbound RTCP is consumed inside the interceptor chain for reports, NACK handling, congestion control, and
similar logic — it is control traffic the interceptors act on, not media the application asked for. An interceptor that
attaches `Attribute::DeliverToApplication` to a packet vouches for it, and that packet arrives alongside the media.

Marking is the mechanism because a copy cannot outrun the end of the chain: a chain is a flat list, and what an
interceptor emits rejoins that list *behind* itself, where the stage that ends the inbound RTCP path is still ahead of
it. A marked packet finishes the walk normally and that stage reads the mark.

Marking also keeps the judgement per-packet, which a chain-wide switch could not: this example vouches for keyframe
requests and lets the receiver reports its own chain is acting on stop where they always did. This example mirrors the
sansio `rtc/examples/rtcp-processing` example.

### Open rtcp-processing example page

[jsfiddle.net](https://jsfiddle.net/zurq6j7x/) you should see two text-areas, 'Start Session' button and 'Copy browser
SessionDescription to clipboard'

## Build

```shell
cargo build --example rtcp-processing
```

## Run

```shell
cargo run --example rtcp-processing
```

## With debug logging

```shell
cargo run --example rtcp-processing -- --debug
```

## Read SDP from a file

```shell
cargo run --example rtcp-processing -- --input-sdp-file offer.txt
```

## Signaling flow

1. Paste a base64-encoded SDP offer from a browser.
2. Copy the printed base64 answer back into the browser.
3. Start sending audio/video from the browser.
4. Watch incoming RTCP packets printed per remote track.

## Async RTCP delivery

With the custom forwarder registered, RTCP arrives through the remote track event loop:

```rust
while let Some(evt) = track.poll().await {
if let TrackRemoteEvent::OnRtcpPacket(rtcp_packets) = evt {
for packet in rtcp_packets {
let header = packet.header();
println! ("Type: {:?}", header.packet_type);
println! ("{packet}");
}
}
}
```

Without an interceptor marking packets, `TrackRemoteEvent::OnRtcpPacket` is not emitted at all: the chain ends the
inbound RTCP path before the application. With the `RtcpForwarderInterceptor`, only PLI and FIR arrive — this peer is
receive-only in the jsfiddle above, so it is the one *sending* keyframe requests and will print nothing until something
downstream asks it for one.

To see every inbound RTCP packet instead, widen what the interceptor vouches for rather than removing it: mark each
`Packet::Rtcp` unconditionally in its `handle_read`. Removing the line altogether leaves nothing to mark them, and the
application sees no RTCP at all.

---

### Overview (`examples/rtp-forwarder/README.md`)

# rtp-forwarder

rtp-forwarder is a simple application that shows how to forward your webcam/microphone via RTP using WebRTC.rs.

## Instructions

### Build rtp-forwarder

```shell
cargo build --example rtp-forwarder
```

### Open rtp-forwarder example page

[jsfiddle.net](https://jsfiddle.net/1qva2zd8/) you should see your Webcam, two text-areas and a 'Start Session' button

### Run rtp-forwarder, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/rtp-forwarder`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/rtp-forwarder < my_file`

### Input rtp-forwarder's SessionDescription into your browser

Copy the text that `rtp-forwarder` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle and enjoy your RTP forwarded stream!

You can run any of these commands at anytime. The media is live/stateless, you can switch commands without restarting Pion.

#### VLC

Open `rtp-forwarder.sdp` with VLC and enjoy your live video!

#### ffmpeg/ffprobe

Run `ffprobe -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to get more details about your streams

Run `ffplay -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to play your streams

You can add `-fflags nobuffer` to lower the latency. You will have worse playback in networks with jitter.

#### Twitch/RTMP

`ffmpeg -protocol_whitelist file,udp,rtp -i rtp-forwarder.sdp -c:v libx264 -preset veryfast -b:v 3000k -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 -c:a aac -b:a 160k -ac 2 -ar 44100 -f flv rtmp://live.twitch.tv/app/$STREAM_KEY` Make sure to replace `$STREAM_KEY` at the end of the URL first.

---

### Overview (`examples/rtp-to-webrtc/README.md`)

# rtp-to-webrtc

rtp-to-webrtc demonstrates how to consume a RTP stream video UDP, and then send to a WebRTC client.

With this example we have pre-made GStreamer and ffmpeg pipelines, but you can use any tool you like!

## Instructions

### Build rtp-to-webrtc

```shell
cargo build --example rtp-to-webrtc
```

### Open jsfiddle example page

[jsfiddle.net](https://jsfiddle.net/z7ms3u5r/) you should see two text-areas and a 'Start Session' button

### Run rtp-to-webrtc with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser's SessionDescription, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/rtp-to-webrtc`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/rtp-to-webrtc < my_file`

### Send RTP to listening socket

You can use any software to send VP8 packets to port 5004. We also have the pre made examples below

#### GStreamer

```shell
gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480,format=I420 ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! rtpvp8pay ! udpsink host=127.0.0.1 port=5004
```

#### ffmpeg

```shell
ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -vcodec libvpx -cpu-used 5 -deadline 1 -g 10 -error-resilient 1 -auto-alt-ref 1 -f rtp "rtp://127.0.0.1:5004?pkt_size=1200"
```

### Input rtp-to-webrtc's SessionDescription into your browser

Copy the text that `rtp-to-webrtc` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

A video should start playing in your browser above the input boxes.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/save-to-disk-av1/README.md`)

# save-to-disk-av1

`save-to-disk-av1` records incoming AV1 video from a browser and saves it to an IVF file.

## Instructions

### Build save-to-disk-av1

```shell
cargo build --example save-to-disk-av1
```

### Open save-to-disk-av1 example page

[jsfiddle.net](https://jsfiddle.net/8jv91r25/) you should see your webcam, two text-areas and two buttons:
`Copy browser SDP to clipboard`, `Start Session`.

### Run save-to-disk-av1 with your browser's SessionDescription as stdin

In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the
base64 string manually.

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/save-to-disk-av1`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/save-to-disk-av1 < my_file`

### Input save-to-disk-av1's SessionDescription into your browser

Copy the text that `save-to-disk-av1` emits and paste it into the second text area.

### Hit `Start Session`, wait, close the page, and inspect `output.ivf`

The example writes AV1 RTP into an IVF container and saves it in the file given by `--video` (default: `output.ivf`).

## Notes

- `--client` keeps the sansio example's DTLS answering-role override.
- `--host` and `--port` control the UDP bind address used by the async peer connection driver.

---

### Overview (`examples/save-to-disk-fec/README.md`)

# save-to-disk-fec

save-to-disk-fec is the receiving half of [play-from-disk-fec](../play-from-disk-fec). That example protects a VP8
stream with FlexFEC-03 and then deliberately drops media packets at the wire; this one accepts the offer, rebuilds
what was dropped from the repair stream, and writes the result to an IVF file.

There is no pion counterpart — pion has no FlexFEC *receiver*.

## Instructions

Two terminals. `play-from-disk-fec` offers, `save-to-disk-fec` answers.

### 1. Start play-from-disk-fec to generate an offer

```shell
cargo run --example play-from-disk-fec -- -v rtc/examples/test-data/output_vp8.ivf
```

It prints a base64 offer and waits.

### 2. Feed that offer to save-to-disk-fec

Save the base64 blob to a file and pass it with `-i`, or paste it on stdin:

```shell
cargo run --example save-to-disk-fec -- -i offer.txt -v output.ivf
```

It prints a base64 answer.

### 3. Feed the answer back

Save the answer to `answer.txt`, then press Enter in the `play-from-disk-fec` terminal (start it with
`-i answer.txt` if you want it read from a file). The two connect and the stream starts.

### 4. Watch the recovery

The sender reports what it discarded:

```text
dropping 1 media packet in 5 at the wire; repair is 1 per 10
Stats: Media: 400, FEC: 34, Dropped: 80, Drop ratio: 20.0000%
```

and the receiver reports what it rebuilt:

```text
FEC negotiated: media SSRC 4246467035 protected by repair SSRC 2229544491
Stats: Media: 300 (arrived: 277, recovered: 23), Unrouted FEC: 0, Recovered: 7.6667%
```

Ctrl-C either side to stop; `output.ivf` is closed on the way out and plays in any IVF-capable player
(`ffplay output.ivf`).

## Reading the numbers

**Not all of the loss comes back, and that is correct.** The repair rate is one FEC packet per ten media packets,
which recovers a *single* loss anywhere in a block of ten. The sender drops one in five. Most blocks therefore lose
two packets and are not recoverable however the repair is arranged — in the run above, 23 of roughly 80 losses were
rebuilt. FEC narrows the loss it is sized for and no more.

To see it keep up, run the sender with `--drop-one-in 20`. To see the difference it makes, run with
`--drop-one-in 0` and compare the file sizes.

**`Unrouted FEC` should read 0.** The decoder sits wire-ward of the counter and consumes the repair packets it has a
decoder for, so none reach the counter. A non-zero value means repair packets arrived for a stream that never bound —
FEC negotiated but not usable — which from the outside looks identical to a path that simply lost nothing.

## What makes it work

Two things, and neither is much code:

- **`video/flexfec-03` registered in the `MediaEngine`.** An answerer can only select payload types the offer listed,
  so the repair codec has to be registered here for the offer's `a=rtpmap:49 flexfec-03/90000` to be answered. Leave
  it out and everything still runs: the connection comes up, the file fills, and the sender's induced loss goes
  straight to disk. That silent no-op is why the example prints `FEC negotiated: …` on bind, and says so loudly when
  a media stream binds without a repair SSRC.
- **`FlexFec03Receive` at `Slot::FecDecoder`** (6_000), wire-ward of everything that inspects sequence numbers. A
  rebuilt packet has to be indistinguishable from one that arrived, so it must rejoin the stream before the NACK
  generator — which would otherwise ask the sender for a packet already being rebuilt here — and before the jitter
  buffer, which has to order it along with the rest.

`RecoveryCounter` at slot 6_500 adds nothing to that. It only reports, and it sits immediately application-ward of
the decoder because that is the earliest point at which `Attribute::RecoveredByFec` exists to be counted. Wire-ward
of the decoder it would count nothing and report a recovery rate of zero on a connection recovering perfectly well.

## Why the counter reports for itself

`RTCMessage::RtpPacket` carries the packet and nothing else — attributes do not cross into `poll_read`. So an
application that wants to know which packets were rebuilt either counts them inside the chain, as this example does,
or has the interceptor publish through a channel, as
[bandwidth-estimation-from-disk](../bandwidth-estimation-from-disk) does with its estimate.

By the time a packet reaches the application there is nothing to do differently: the decoder has already put it back
in the stream, and the write to disk is the same line it would be with no FEC at all. That is the point.

Congrats, you have used WebRTC-rs!

---

### Overview (`examples/save-to-disk-h26x/README.md`)

# save-to-disk-h26x

save-to-disk-h26x is a simple application that shows how to record your webcam/microphone using WebRTC.rs and save h26x
and Opus to disk.

## Instructions

### Build save-to-disk-h26x

```shell
cargo build --example save-to-disk-h26x
```

### Open save-to-disk example page

[jsfiddle.net](https://jsfiddle.net/vfmcg8rk/1/) you should see your Webcam, two text-areas and a 'Start Session' button

### Run save-to-disk-h26x, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/save-to-disk-h26x`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/save-to-disk-h26x < my_file`

### Input save-to-disk-h26x's SessionDescription into your browser

Copy the text that `save-to-disk-h26x` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video!

In the folder you ran `save-to-disk-h26x` you should now have a file `output.h26x` play with your video player of
choice!

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/save-to-disk-vpx/README.md`)

# save-to-disk-vpx

save-to-disk-vpx is a simple application that shows how to record your webcam/microphone using WebRTC.rs and save VP8/VP9 and Opus to disk.

## Instructions

### Build save-to-disk-vpx

```shell
cargo build --example save-to-disk-vpx
```

### Open save-to-disk-vpx example page

[jsfiddle.net](https://jsfiddle.net/vfmcg8rk/1/) you should see your Webcam, two text-areas and a 'Start Session' button

### Run save-to-disk-vpx, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/save-to-disk-vpx`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/save-to-disk-vpx < my_file`

### Input save-to-disk-vpx's SessionDescription into your browser

Copy the text that `save-to-disk-vpx` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video!

In the folder you ran `save-to-disk-vpx` you should now have a file `output_vpx.ivf` play with your video player of choice!

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/simulcast/README.md`)

# simulcast

demonstrates of how to handle incoming track with multiple simulcast rtp streams and show all them back.

The browser will not send higher quality streams unless it has the available bandwidth. You can look at
the bandwidth estimation in `chrome://webrtc-internals`. It is under `VideoBwe` when `Read Stats From: Legacy non-Standard`
is selected.

## Instructions

### Build simulcast

```shell
cargo build --example simulcast
```

### Open simulcast example page

[jsfiddle.net](https://jsfiddle.net/rxk4bftc) you should see two text-areas and a 'Start Session' button.

### Run simulcast, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/simulcast`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/simulcast < my_file`

### Input simulcast's SessionDescription into your browser

Copy the text that `simulcast` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

Your browser should send a simulcast track to WebRTC.rs, and then all 3 incoming streams will be relayed back.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/simulcast_add_transceiver_from_kind/README.md`)

# simulcast

demonstrates of how to handle incoming track with multiple simulcast rtp streams and show all them back.

The browser will not send higher quality streams unless it has the available bandwidth. You can look at
the bandwidth estimation in `chrome://webrtc-internals`. It is under `VideoBwe` when `Read Stats From: Legacy non-Standard`
is selected.

## Instructions

### Build simulcast

```shell
cargo build --example simulcast
```

### Open simulcast example page

[jsfiddle.net](https://jsfiddle.net/rxk4bftc) you should see two text-areas and a 'Start Session' button.

### Run simulcast, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/simulcast`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/simulcast < my_file`

### Input simulcast's SessionDescription into your browser

Copy the text that `simulcast` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

Your browser should send a simulcast track to WebRTC.rs, and then all 3 incoming streams will be relayed back.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/stats/README.md`)

# stats

stats demonstrates how to use the [webrtc-stats](https://www.w3.org/TR/webrtc-stats/) implementation provided by WebRTC-rs.

This API gives you access to statistical information about a PeerConnection. This can help you understand what is happening during a session and why.

## Instructions

### Open stats example page

Go to [jsfiddle.net](https://jsfiddle.net/s179hacu/). You should see your Webcam, two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`.

### Run stats, with your browser's SessionDescription as stdin

In the jsfiddle, the top textarea contains your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step.

#### Linux/macOS

Run:
```bash
echo $BROWSER_SDP | cargo run --example stats
```

#### Windows

1. Paste the SessionDescription into a file.
2. Run:
```cmd
cargo run --example stats < my_file
```

### Input stats' SessionDescription into your browser

Copy the base64 string that `stats` just emitted and paste it into the second text area in the browser.

### Hit 'Start Session' in jsfiddle

The `stats` program will now print WebRTC statistics every 5 seconds, including InboundRTPStreamStats for each incoming stream and Remote IP+Ports.

You will see the following in your console:

```
=== WebRTC Stats ===
{
  "data_channels_closed": 0,
  "data_channels_opened": 0
}

Inbound RTP Stats for: video/vp8
{
  "packets_received": 1255,
  "packets_lost": 0,
  "jitter": 588.9559641717999,
  "bytes_received": 1361125,
  "track_identifier": "video-track",
  "ssrc": 1234567890,
  ...
}

Inbound RTP Stats for: audio/opus
{
  "packets_received": 2450,
  "packets_lost": 0,
  "jitter": 12.5,
  "bytes_received": 245000,
  "track_identifier": "audio-track",
  "ssrc": 987654321,
  ...
}

Remote Candidate:
{
  "address": "192.168.1.93",
  "port": 59239,
  "candidate_type": "host",
  ...
}
====================
```

## What it demonstrates

This example demonstrates:
1. How to use the `peer_connection.get_stats()` API to retrieve WebRTC statistics asynchronously.
2. How to access different types of stats from the stats report:
   - Peer connection stats via `report.peer_connection()`
   - Inbound RTP stream stats via `report.inbound_rtp_streams()`
   - ICE candidate stats via `report.iter_by_type()`
3. How to print stats periodically at regular intervals (every 5 seconds) using background async tasks.

---

### Overview (`examples/swap-tracks/README.md`)

# swap-tracks

swap-tracks demonstrates how to swap multiple incoming tracks on a single outgoing track.

## Instructions

### Build swap-tracks

```shell
cargo build --example swap-tracks
```

### Open swap-tracks example page

[jsfiddle.net](https://jsfiddle.net/dzc17fga/) you should see two text-areas and a 'Start Session' button.

### Run swap-tracks, with your browsers SessionDescription as stdin

In the jsfiddle the top textarea is your browser, copy that and:

#### Linux/macOS

Run `echo $BROWSER_SDP | ./target/debug/examples/swap-tracks`

#### Windows

1. Paste the SessionDescription into a file.
1. Run `./target/debug/examples/swap-tracks < my_file`

### Input swap-tracks's SessionDescription into your browser

Copy the text that `swap-tracks` just emitted and copy into second text area

### Hit 'Start Session' in jsfiddle, enjoy your video!

Your browser should send streams to webrtc-rs, and then a stream will be relayed back, changing every 5 seconds.

Congrats, you have used WebRTC.rs!

---

### Overview (`examples/trickle-ice-host/README.md`)

# trickle-ice-host

`trickle-ice-host` demonstrates async `webrtc` trickle ICE with host local
candidates.

## Instructions

Run:

```bash
cargo run --example trickle-ice-host
```

Or with debug logging:

```bash
cargo run --example trickle-ice-host -- --debug
```

Then open [http://localhost:8080](http://localhost:8080) and click `Start`.

The example sends the SDP answer immediately and trickles host ICE candidates to
the browser as they are gathered.

---

### Overview (`examples/trickle-ice-relay/README.md`)

# trickle-ice-relay

`trickle-ice-relay` demonstrates async `webrtc` Trickle ICE with **TURN relay-only** local candidates.

It uses the async peer-connection stack to gather relay candidates from a configured TURN server, then trickles them to
the browser while the answer is being established.

## Prerequisites

You need a TURN server running. You can use either:

### Option 1: webrtc-rs/webrtc v0.17.x branch turn server

From the webrtc-rs/webrtc v0.17.x branch repository:

```bash
RUST_LOG=trace cargo run --color=always --package turn --example turn_server_udp -- --public-ip <browser-reachable-ip> --users user=pass
```

### Option 2: pion/turn server

From the pion/turn repository:

```bash
./simple -public-ip <browser-reachable-ip> -users user=pass
```

## Run

From the repository root:

```bash
cargo run --example trickle-ice-relay
```

With explicit TURN settings:

```bash
cargo run --example trickle-ice-relay -- --turn-host <browser-reachable-ip> --turn-port 3478 --turn-user user=pass
```

With debug logging:

```bash
cargo run --example trickle-ice-relay -- --debug
```

Then open [http://localhost:8080](http://localhost:8080) and click **Start**.

---

### Overview (`examples/trickle-ice-srflx/README.md`)

# trickle-ice-srflx

`trickle-ice-srflx` demonstrates async `webrtc` trickle ICE with STUN-backed
server reflexive local candidates.

## Instructions

Run:

```bash
cargo run --example trickle-ice-srflx
```

Or with debug logging:

```bash
cargo run --example trickle-ice-srflx -- --debug
```

Then open [http://localhost:8080](http://localhost:8080) and click `Start`.

The example sends the SDP answer immediately and then trickles host and srflx
ICE candidates to the browser as they become available.

---

### Overview (`examples/trickle-ice/README.md`)

# trickle-ice

`trickle-ice` demonstrates async `webrtc` Trickle ICE with host, server reflexive (STUN), and relay (TURN) candidates.

It uses the async peer-connection stack to gather ICE candidates depending on the enabled flags, and trickles them to the browser while the connection is being established.

## Run

From the repository root, run with default settings (Host candidates only):

```bash
cargo run --example trickle-ice
```

Enable only specific candidate types:

```bash
# Only Host candidates
cargo run --example trickle-ice -- --enable-host

# Only STUN Server Reflexive candidates
cargo run --example trickle-ice -- --enable-srflx

# Only TURN Relay candidates
# Requires a TURN server running at 127.0.0.1:3478 with credentials user=pass
cargo run --example trickle-ice -- --enable-relay

# Host + STUN
cargo run --example trickle-ice -- --enable-host --enable-srflx
```

Customize STUN or TURN settings:

```bash
cargo run --example trickle-ice -- --enable-srflx --stun-server stun.l.google.com:19302

cargo run --example trickle-ice -- --enable-relay --turn-host 127.0.0.1 --turn-port 3478 --turn-user user=pass
```

With debug logging:

```bash
cargo run --example trickle-ice -- --debug
```

Then open [http://localhost:8080](http://localhost:8080) and click **Start**.

---

### Internal Channel Overflow Policy (`docs/internal-channel-overflow-policy.md`)

# Internal channel overflow policy

Four bounded channels connect the application to the peer-connection driver. Each has a
capacity, so each can be full, and every send site has to do *something* when it is. This
document says what the acceptable somethings are and which one each site uses.

The rule this exists to enforce: **no message, event or packet is discarded silently
because an internal channel was full.** "Silently" is the operative word — loss is
acceptable in one of the four cases below, invisible loss never is.

Enforced by `scripts/check-channel-overflow-policy.py`, which fails the build if a send
site on one of these channels carries no `// overflow:` tag. It checks that a decision was
recorded, not that the decision was right; the reasoning is what the comment is for.

## The four channels

All four are `pub(crate)` constants in `src/peer_connection/driver.rs`, named for the
direction they carry so a use site reads without a trip to the definition.

| Constant | Direction | Payload | Contract |
|---|---|---|---|
| `APPLICATION_TO_DRIVER_EVENT_CHANNEL_CAPACITY` | app → driver | `SenderRtp`, `SenderRtcp`, `ReceiverRtcp`, `RemoteIceTcpPassiveCandidate`, `IncomingTcpStream`, `WriteNotify`, `UpdateIceConfiguration`, `IceGathering`, `Close` | never drops |
| `DRIVER_TO_DATA_CHANNEL_EVENT_CHANNEL_CAPACITY` | driver → app, per channel | lifecycle + `OnMessage` | never drops |
| `DRIVER_TO_TRACK_REMOTE_EVENT_CHANNEL_CAPACITY` | driver → app, per remote track | lifecycle + `OnRtpPacket` / `OnRtcpPacket` | media may drop, counted |
| `DRIVER_TO_TRACK_LOCAL_EVENT_CHANNEL_CAPACITY` | driver → app, per local track | `OnRtcpPacket` only | may drop, counted |

## The four policies

**`awaited`** — the producer is the application, which blocks. Nothing is lost, and the
back-pressure lands on the caller, which is where it belongs. This is the model the rest
are shaped after.

**`nudge`** — a flag-backed notification. The durable state is an `AtomicBool` the driver
polls unconditionally every iteration; the channel event is only a wake. Dropping it loses
nothing, because if the queue is full there are already ≥256 events waiting and the driver
is about to run anyway.

> **Do not "fix" these into awaits.** `wake_writes` coalesces deliberately: making it
> `.send().await` forces a 1:1 wake per message and collapses throughput on
> cooperatively-scheduled runtimes. The `Close` nudge in `Drop` *cannot* await at all. Both
> are correct as they stand, and both look like bugs to a reader who does not know about
> the flag — which is why they are tagged.

**`detached`** — the producer is a task spawned for one job with nothing else to do. A full
channel parks that task alone. `IncomingTcpStream` is the only one: it is sent from a
per-candidate task in `RTCTcpTransport::connect`, which hands the stream over and exits.
(TCP *accepts* do not use this channel — the driver polls `accept_futures` directly.)

**`DROPS`** — discards on `Full`. Whether that is a bug depends entirely on what the
payload promised:

- On the **data channel** it is a bug, and it is
  [webrtc#858](https://github.com/webrtc-rs/webrtc/issues/858). A reliable ordered channel
  promises delivery.
- On the **media** paths it is inherent. UDP has no flow control, so there is nothing
  upstream to push back to; refusing to drop would mean buffering until the process dies.
  The requirement is that the loss is counted, not that it is prevented.

## The one constraint on any fix

**The driver loop must never block on a send.** It also drives ICE consent, DTLS
retransmits and SCTP timers — awaiting a slow consumer there expires consent and drops the
connection, which is a worse outcome than the drop being fixed.

So back-pressure on a driver → application channel means **stop pulling from the core**,
never *wait here*. `TrySendError::Full(value)` hands the payload back; keep it and stop
consuming for that channel until the application drains.

For the data channel that pressure has somewhere real to go: leaving bytes in SCTP's
reassembly queue lowers `get_my_receiver_window_credit()`, which is advertised as `a_rwnd`
in every SACK, and the peer throttles. That machinery is already implemented in `rtc-sctp`
and correct — it is bypassed today only because the core drains the stream unconditionally.

**Consequence worth stating:** SCTP's receive window is per-association, not per-stream, so
a stalled consumer on one data channel will eventually throttle every channel on that
connection. This is inherent to SCTP flow control and matches browser behaviour.

## Why a bigger queue is not the fix

Any finite capacity plus `try_send` still drops; raising it changes *when* a reliable
channel loses data, not *whether* it does. A configurable depth is defensible on its own
merits — 256 is arbitrary — but shipping it as the answer would produce an API that appears
to let a user configure their way out of data loss while only making the loss rarer and
harder to reproduce.

The meaningful inbound bound already exists and is already configurable:
`SettingEngine::set_sctp_max_receive_buffer_size`, because that is the number SCTP
advertises to the peer.

## Adding a send site

Tag it. If none of the four categories fits, that is the finding — the design needs
revisiting, not a fifth category invented at the call site.

---

### Semver (`docs/semver.md`)

# API stability policy

This document records how `webrtc` keeps its public API extensible, and the specific
decisions taken before 1.0. It exists because the two mechanisms below **cannot be
introduced after 1.0** — adding either one is itself a breaking change, so the window to
apply them closes when the major version lands.

See also `rtc/docs/semver.md`, which records the same decisions for the sans-I/O core and
the protocol subcrates.

## The two mechanisms

### `#[non_exhaustive]` on enums

Marking a public enum `#[non_exhaustive]` forces downstream `match` expressions to carry a
`_` arm. New variants can then be added in a minor release instead of a major one.

What it costs, and why it is not applied blindly:

- Downstream code loses exhaustiveness checking for that enum — the compiler stops
  reporting an unhandled case, because `_` absorbs it.
- Struct-literal construction of a non-exhaustive *variant* is also blocked downstream.
- A single-variant enum can no longer be destructured with an irrefutable `let`.

The rule we apply:

> Mark an enum `#[non_exhaustive]` when the set of variants is defined by something outside
> this codebase and can grow — an IANA registry, a protocol state machine, an error
> taxonomy, an event stream. Leave it exhaustive when the set is closed by construction —
> a fixed-width wire field, a binary role, a mathematically complete set — and where losing
> exhaustiveness checking would cost callers more than a future variant would.

Note that `#[non_exhaustive]` does **not** affect matches inside the defining crate. It
does affect that crate's own examples, integration tests, and benchmarks, since those are
separate compilation units.

### Sealed traits

A trait with a private supertrait cannot be implemented downstream:

```rust
pub(crate) mod sealed {
    pub trait Sealed {}
}

pub trait PeerConnection: crate::sealed::Sealed + Send + Sync + 'static { /* … */ }
```

Sealing buys the ability to **add required methods in a minor release** — impossible for an
open trait, where a new method breaks every downstream implementation. The cost is absolute:
nobody outside the crate can implement it, ever.

The rule we apply:

> Seal a trait when this library is the only meaningful implementor — the trait describes an
> object we construct and hand out. Keep it open when it is an extension point that exists
> precisely so users can plug in their own type.

## Decisions — traits

`webrtc` declares 15 public traits.

**Sealed** (library-implemented; sealing enables compatible method additions):

| Trait | Sole implementor |
|---|---|
| `PeerConnection` | `PeerConnectionImpl<I>` |
| `DataChannel` | `DataChannelImpl<I>` |
| `RtpSender` | `RtpSenderImpl<I>` |
| `RtpReceiver` | `RtpReceiverImpl<I>` |
| `RtpTransceiver` | `RtpTransceiverImpl<I>` |

**Open** (user extension points — sealing these would defeat their purpose):

| Trait | Why open |
|---|---|
| `Runtime` | The pluggable-runtime design depends on downstream implementations. |
| `AsyncUdpSocket`, `AsyncTcpListener`, `AsyncTcpStream`, `AsyncInterval`, `JoinHandle` | Same extension surface as `Runtime`; a custom runtime must supply all of them. |
| `PeerConnectionEventHandler` | The application's callback surface. |
| `Track`, `TrackLocal`, `TrackRemote` | Applications provide their own media sources and sinks. |

Adding a required method to any *open* trait is a breaking change. Add methods with a
default body, or accept the major bump.

## Decisions — enums

`webrtc` declares 6 public enums.

**Marked `#[non_exhaustive]`** — event streams, which gain variants as capabilities land:

| Enum | Location |
|---|---|
| `DataChannelEvent` | `data_channel/mod.rs` |
| `TrackLocalEvent` | `media_stream/track_local/mod.rs` |
| `TrackRemoteEvent` | `media_stream/track_remote/mod.rs` |

**Kept exhaustive** — channel-error types with a closed vocabulary:

| Enum | Rationale |
|---|---|
| `TrySendError<T>` | "At capacity" and "closed" are the complete set of non-blocking-send failures, and callers branch on them to decide retry-vs-abandon. |
| `TryRecvError` | As above, for receives. |
| `BroadcastRecvError` | "Closed" and "lagged" are the complete set for a broadcast receiver. |

`std`, `tokio`, and `async-channel` all leave their equivalents exhaustive; matching these
enums exhaustively is normal, correct user code and we do not want to break it for a variant
we have no plan to add.

## When adding a new public item

- **New public enum**: decide exhaustive-or-not *at the point of introduction* and record it
  here. After 1.0 the decision is frozen.
- **New public trait**: decide sealed-or-open the same way. If it describes something only
  this crate constructs, seal it — that keeps the door open for method additions.
- **New variant on a `#[non_exhaustive]` enum**: minor release, no breakage.
- **New variant on an exhaustive enum**: major release. Reconsider whether the enum was
  classified correctly rather than reaching for the major bump.
- **New method on a sealed trait**: minor release, no breakage.
- **New method on an open trait**: provide a default body, or take the major bump.

---

### Transport Objects (`docs/transport-objects.md`)

# 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:

| Not `async` | Why |
|---|---|
| `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.

---

--- METRICS ---
- Files Extracted: 40
- Estimated Token Budget: ~27058 tokens
- Canonical Reference: https://codewiki.google/github.com/webrtc-rs/webrtc
