webrtc Documentation

Async-friendly WebRTC implementation in Rust

5,159 stars Rust Rust 1 file · ~30,631 tokens #async#rtc#rust#webrtc
RAW Doc

File: README.md

[](https://webrtc.rs)

Table of Content

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

rust
use std::sync::Arc;
use webrtc::peer_connection::{
    PeerConnection, PeerConnectionBuilder, PeerConnectionEventHandler,
    RTCConfigurationBuilder, RTCIceServer, RTCPeerConnectionIceEvent, SettingEngineBuilder, crypto,
};
use webrtc::runtime::TokioRuntime;

// 1. Implement the PeerConnectionEventHandler trait to handle events
#[derive(Clone)]
struct MyHandler;

#[async_trait::async_trait]
impl PeerConnectionEventHandler for MyHandler {
    async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
        println!("New local ICE candidate gathered: {}", event.candidate);
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 2. Configure the peer connection
    let config = RTCConfigurationBuilder::default()
        .with_ice_servers(vec![RTCIceServer {
            urls: vec!["stun:stun.l.google.com:19302".to_owned()],
            ..Default::default()
        }])
        .build();

    // 3. Choose the crypto provider (optional).
    //    `crypto-ring` is on by default, so this states what you would get anyway — but the
    //    choice is per connection, not per process, so two connections here could use
    //    different providers. Enable `crypto-aws-lc-rs` for `AwsLcRsProvider`, or build with
    //    neither feature and pass your own `RTCCryptoProvider`.
    let setting_engine = SettingEngineBuilder::default()
        .with_crypto_provider(Arc::new(crypto::providers::RingProvider::new()))
        .build();

    // 4. Build the PeerConnection — the background driver starts here.
    //    The runtime is a value, injected per connection: swap `TokioRuntime` for
    //    `SmolRuntime`, or for your own `Runtime` impl, and nothing else changes.
    //    (Omit `with_runtime` entirely and `build()` uses the compiled-in default.)
    let pc = PeerConnectionBuilder::new()
        .with_configuration(config)
        .with_setting_engine(setting_engine)
        .with_runtime(Arc::new(TokioRuntime))
        .with_handler(Arc::new(MyHandler))
        .with_udp_addrs(vec!["0.0.0.0:0"])
        .build()
        .await?;

    // 5. Create an SDP offer and set it as the local description
    let offer = pc.create_offer(None).await?;
    pc.set_local_description(offer).await?;

    Ok(())
}

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 or the
39 runnable 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 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 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 below.
  • v0.20.x — the previous line.

While the version is 0.x, a minor bump may carry breaking changes
(see 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.

How to Provide Feedback

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

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:

  • 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!!!


File: CHANGELOG.md

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • Crypto provider selection (webrtc#839,
    rtc#128). New crypto-ring (default) and crypto-aws-lc-rs
    Cargo features forward to rtc. They are additive: enabling both compiles both providers and
    ring remains the resolved default, so a dependency enabling crypto-aws-lc-rs cannot silently change
    what an application runs. Building with neither compiles no provider, and the application
    supplies its own.
  • webrtc::peer_connection::crypto re-exports the provider API. Without it
    SettingEngine::set_crypto_provider was uncallable from this crate — its signature names
    Arc<dyn RTCCryptoProvider>, which a user had no way to spell.
  • Provider selection is per peer connection, so two connections in one process can use different
    providers. tests/crypto_provider_integration.rs covers same-provider, cross-provider, and
    application-supplied pairings end to end — ICE/STUN integrity, the DTLS handshake, and SRTP
    media over a live connection.
  • Document pre-1.0 API stability / extensibility policy (see docs/semver.md).

Changed

  • No cryptography happens in this crate. The TURN client now takes its provider from the peer
    connection (RTCPeerConnection::crypto_provider()) rather than constructing one, so a
    connection uses exactly one provider throughout. A CI check asserts webrtc depends on no
    crypto implementation.

  • Bind addresses are resolved on every bind, and a wildcard means "every interface"
    (webrtc#874). with_udp_addrs /
    with_tcp_addrs values are kept as configured instead of being resolved once at construction,
    so the ICE-restart rebind added in webrtc#868
    re-resolves them: a host name follows its DNS record, and 0.0.0.0 / [::] re-enumerates the
    local interfaces. A wildcard is no longer bound verbatim — one socket is bound per interface
    address (skipping loopback and link-local), which is what makes its host candidates usable, and
    what lets an ICE restart after a Wi-Fi/cellular handover pick up the interfaces the device has
    now. On a host with no usable interface the wildcard is bound as before. Because the configured
    addresses now outlive build(), PeerConnectionBuilder::build requires A: Send + 'static —
    owned addresses (String, SocketAddr, &'static str) are unaffected.

  • An address that cannot be bound is skipped rather than fatal
    (webrtc#874). The failure is logged — warn
    for an enumerated interface address, error for one the application configured — and binding
    continues with the rest; only binding nothing at all is still an error. An address left behind
    by a network handover (EADDRNOTAVAIL) therefore no longer costs the connection the interfaces
    that are still there.

Deprecated

Removed

Fixed

Security

[0.20.0] - 2026-07-31

Added

  • The async webrtc v0.20.0 is a clean, ergonomic, runtime-agnostic rewrite on top of a Sans-I/O core rtc;
  • It ships with Tokio and smol runtime backends, and any other runtime can be plugged in by implementing one trait.

File: codecov.yml

codecov:
require_ci_to_pass: yes
max_report_age: off
token: 9c7a93c8-b2b2-4da3-9990-7283701dec58

Example binaries are demos, not exercised by `cargo test`, so they always

report 0% and only drag coverage down (and inflate patch "missing lines").

ignore:

  • "examples"

coverage:
precision: 2
round: down
range: 50..90
status:
project:
default:
enabled: no
threshold: 0.2
if_not_found: success
patch:
default:
enabled: no
if_not_found: success
changes:
default:
enabled: no
if_not_found: success


File: 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. 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.


File: 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
DataChannel DataChannelImpl
RtpSender RtpSenderImpl
RtpReceiver RtpReceiverImpl
RtpTransceiver RtpTransceiverImpl

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.

File: 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, 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>, 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.


File: examples/README.md

Examples

All examples are ported from Pion. Please
check Pion Examples for more details:

Data Channel API

  • ✅ Data Channels: The data-channels example shows how you can send/recv DataChannel messages from a
    web browser.
  • ✅ 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: Example data-channels-close is a variant of data-channels that allow
    playing with the life cycle of data channels.
  • ✅ Data Channels Flow Control: Example data-channels-flow-control shows how to use flow
    control.
  • ✅ Data Channels Offer Answer: Example offer-answer is an example of two webrtc-rs
    instances communicating directly!
  • ✅ Data Channels Simple: Simple example of a WebRTC DataChannel using it as the signaling
    server.

Media API

  • ✅ 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: 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: 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: 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: 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: 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: 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: 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: The rtp-forwarder example demonstrates how to forward your audio/video streams
    using RTP.
  • ✅ RTP to WebRTC: The rtp-to-webrtc example demonstrates how to take RTP packets sent to a webrtc-rs
    process into your browser.
  • ✅ 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: The swap-tracks demonstrates how to swap multiple incoming tracks on a single outgoing
    track.
  • ✅ 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: 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: 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: 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: 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 demonstrates how to
    use Bandwidth Estimation APIs, switching between three pre-encoded renditions as the estimate moves.

Miscellaneous

  • ✅ mDNS Query and Gather Example mdns-query-and-gather demonstrates webrtc-rs hides local ip
    with mDNS.
  • ✅ ICE Restart: The ice-restart demonstrates webrtc-rs ICE Restart abilities.
  • ✅ Trickle ICE Host Example demonstrates WebRTC's Trickle ICE APIs to add Host type local
    candidate.
  • ✅ Trickle ICE ServerReflexive: Example demonstrates how to add ServerReflexive (STUN)
    type local candidate.
  • ✅ Trickle ICE Relay: Example demonstrates how to add Relay (TURN)
    type local candidate.
  • ✅ 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 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 Example ice-tcp-active-passive demonstrates RTC's ICE TCP active
    mode abilities.
  • ✅ Stats: Stats demonstrates how to use the webrtc-stats implementation provided by WebRTC-rs.
  • ✅ 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

  • ICE Single Port 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.
  • ICE Proxy 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.
  • Data Channels WHIP WHEP: This example doesn't apply to sansio RTC, since it demonstrates a WHIP/WHEP-like
    implementation.
  • WHIP WHEP: This example doesn't apply to sansio RTC, since it demonstrates using WHIP and WHEP.

File: 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 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!


File: 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 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!


File: 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 and 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.


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

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.
  2. 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!


File: 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.


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

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!


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

File: 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.


File: 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
  1. Open browser at http://localhost:8080

  2. 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

File: 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. 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!


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

text
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.

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

File: 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 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.
  2. 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!


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

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.
  2. 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.


File: 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
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) 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!


File: 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 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!


File: 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]

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

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 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!


File: 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 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!


File: 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 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.
  2. 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!


File: 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.
  3. Receiving those requests on the async side through TrackRemoteEvent::OnRtcpPacket.
  4. 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 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.


File: 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 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.
  2. 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.


File: 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 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.
  2. 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!


File: 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 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.
  2. 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.

File: examples/save-to-disk-fec/README.md

save-to-disk-fec

save-to-disk-fec is the receiving half of 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 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!


File: 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 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.
  2. 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!


File: 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 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.
  2. 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!


File: 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 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.
  2. 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!


File: 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 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.
  2. 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!


File: examples/stats/README.md

stats

stats demonstrates how to use the 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. 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:

text
=== 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.

File: 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 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.
  2. 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!


File: 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 and click Start.


File: 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 and click Start.

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


File: 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 and click Start.


File: 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 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.


File: .devcontainer/devcontainer.json

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Rust",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/rust:latest",

text
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Configure tool-specific properties.
"customizations": {
	// Configure properties specific to VS Code.
	"vscode": {
		"settings": {},
		"extensions": [
			"streetsidesoftware.code-spell-checker"
		]
	}
},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": {
	"submodules": "git config --global --add safe.directory ${containerWorkspaceFolder} && git submodule update --init --recursive",
	"rust": "rustc --version"
}

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"

}


File: .github/workflows/cargo.yml

name: cargo

on:
push:
branches: [ master, v1.x, v0.21.x, v0.20.x, v0.17.x ]
pull_request:
branches: [ master, v1.x, v0.21.x, v0.20.x, v0.17.x ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
build-os:
name: Build (${{ matrix.os }})
strategy:
matrix:
os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build
run: cargo build --verbose

build-no-features:
name: Build with No Default Features
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build
run: cargo build --verbose --no-default-features

test-features:
name: Unit Test with ${{ matrix.features }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
features:
# A crypto provider is paired with each runtime: this job varies the runtime, and a
# peer connection cannot be constructed without a provider. The no-provider
# configuration is covered separately by crypto-provider-features.
- 'runtime-smol,crypto-ring'
- 'runtime-mock,crypto-ring'
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build
run: cargo test --lib --verbose --no-default-features --features ${{ matrix.features }}

crypto-provider-features:
name: Crypto provider (${{ matrix.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: default
args: ""
- name: ring only
args: --no-default-features --features runtime-tokio,crypto-ring
- name: aws-lc-rs only
args: --no-default-features --features runtime-tokio,crypto-aws-lc-rs
# Both backends in one graph. The core crates rejected this before G3; here it is the
# guard that the forwarding features stay additive and that ring remains the resolved
# default, which tests/crypto_provider_features.rs asserts.
- name: ring + aws-lc-rs
args: --features crypto-ring,crypto-aws-lc-rs
# No built-in provider: an application must supply its own through
# SettingEngine::set_crypto_provider.
- name: no built-in provider
args: --no-default-features --features runtime-tokio
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build
run: cargo build --all-targets --verbose ${{ matrix.args }}
- name: Feature-semantics tests
run: cargo test --test crypto_provider_features --verbose ${{ matrix.args }}
# End-to-end: ICE/STUN integrity, the DTLS handshake, and SRTP media over a real
# connection, per provider pairing. Skipped in the no-built-in configuration, where the
# test file is #![cfg]-ed out.
- name: Provider integration tests
run: cargo test --test crypto_provider_integration --verbose ${{ matrix.args }}

crypto-provider-boundary:
name: Crypto stays in the core crates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
# The async layer forwards provider features and threads the provider through; it must not
# depend on a crypto implementation or perform crypto itself. The script parses dependency
# sections so that feature forwarding (ring = ["rtc/crypto-ring"]) is not mistaken for one.
- name: No crypto implementation dependency in webrtc
run: python3 scripts/check-crypto-boundary.py
# Every send on one of the four bounded internal channels must declare what it does when
# the channel is full — block the application, drop a flag-backed nudge, park a detached
# task, or discard. The script only checks that a decision was recorded at the site; it
# cannot tell a correct classification from a wrong one, but an absent one is how
# webrtc#858 got in. See docs/internal-channel-overflow-policy.md.
- name: Bounded channels declare an overflow policy
run: python3 scripts/check-channel-overflow-policy.py
# No packaging check here: webrtc cannot be packaged until the core crates are
# published — cargo resolves against the registry and the path dependency is stripped
# from the published manifest, so cargo package builds against whatever rtc is on
# crates.io rather than the submodule. Publication order is rtc-crypto, then the rtc
# protocol crates, then rtc, then webrtc. Add a plain cargo package step once rtc
# 0.21.0 is out.

test:
name: Test
strategy:
matrix:
os: [ 'ubuntu-latest' ] #, 'macos-latest', 'windows-latest' ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Run tests
run: cargo test --verbose

rustfmt_and_clippy:
name: Check rustfmt style && run clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: clippy, rustfmt
override: true
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
- name: Check formating
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check


File: .github/workflows/grcov.yml

name: coverage

on:
push:
branches: [ master, v1.x, v0.21.x, v0.20.x, v0.17.x ]
pull_request:
branches: [ master, v1.x, v0.21.x, v0.20.x, v0.17.x ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
grcov:
name: Test with runtime (${{ matrix.runtime.name }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
toolchain:
- nightly
runtime:
- name: tokio
flags: ""
- name: smol
# A provider is paired with the runtime: this job varies the runtime, and
# constructing a peer connection resolves one. --no-default-features alone drops
# the crypto feature too.
flags: "--no-default-features --features runtime-smol,crypto-ring"
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: recursive

text
- name: Install Rust
    uses: dtolnay/rust-toolchain@master
    with:
      toolchain: ${{ matrix.toolchain }}
      components: llvm-tools-preview

  - name: Install grcov
    uses: taiki-e/install-action@v2
    with:
      tool: grcov

  - name: Test
    run: cargo test --all --no-fail-fast ${{ matrix.runtime.flags }}
    env:
      CARGO_INCREMENTAL: "0"
      RUSTFLAGS: "-Cinstrument-coverage"
      LLVM_PROFILE_FILE: "cargo-test-%p-%m.profraw"

  - name: Generate coverage data
    id: grcov
    run: |
      grcov . \
          --binary-path ./target/debug/ \
          --source-dir . \
          --output-type lcov \
          --output-path lcov.info \
          --branch \
          --ignore='/**' \
          --ignore='C:/**' \
          --ignore='../**' \
          --ignore-not-existing \
          --excl-line "#\\[derive\\(" \
          --excl-br-line "#\\[derive\\(" \
          --excl-start "#\\[cfg\\(test\\)\\]" \
          --excl-br-start "#\\[cfg\\(test\\)\\]"
  - name: Upload coverage as artifact
    uses: actions/upload-artifact@v4
    with:
      name: lcov-${{ matrix.runtime.name }}.info
      path: lcov.info

  - name: Upload coverage to codecov.io
    uses: codecov/codecov-action@v5
    with:
      files: lcov.info
      flags: ${{ matrix.runtime.name }}
      fail_ci_if_error: true

File: .github/workflows/semver.yml

name: SemVer Check

on:
push:
branches: [ master, v1.x, v0.21.x, v0.20.x, v0.17.x ]
pull_request:
branches: [ master, v1.x, v0.21.x, v0.20.x, v0.17.x ]

jobs:
semver:
name: Check SemVer
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

text
- name: Run cargo-semver-checks
    uses: obi1kenobi/cargo-semver-checks-action@v2
    with:
      # Overrides the default heuristic to ONLY use default features
      feature-group: default-features