webrtc-rs/webrtc

Async-friendly WebRTC implementation in Rust

Rust

5,160

1,910 commits

updated Oct 11, 2026

See the code

README

WebRTC.rs

License: MIT/Apache 2.0 Discord Twitter

Async-friendly WebRTC implementation in Rust

Sponsored with πŸ’– by

Gold Sponsors:
Recall.ai

Silver Sponsors:
Stream Chat
ChannelTalk
Bronze Sponsors:
AdrianEddy

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:

[dependencies]
webrtc = "0.21.0"

Feature flags:

FeatureDefaultDescription
runtime-tokioβœ…Timers, task spawning and sockets via Tokio
runtime-smolThe same, via smol
runtime-mockMockRuntime, a deterministic virtual-clock runtime for tests (no I/O)
crypto-ringβœ…The ring-based crypto provider
crypto-aws-lc-rsThe 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.

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:

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:

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

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

async
rtc
rust
webrtc

webrtc-rs/webrtc

Async-friendly WebRTC implementation in Rust

Rust

5,160

1,910 commits

updated Oct 11, 2026

See the code

README

WebRTC.rs

License: MIT/Apache 2.0 Discord Twitter

Async-friendly WebRTC implementation in Rust

Sponsored with πŸ’– by

Gold Sponsors:
Recall.ai

Silver Sponsors:
Stream Chat
ChannelTalk
Bronze Sponsors:
AdrianEddy

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:

[dependencies]
webrtc = "0.21.0"

Feature flags:

FeatureDefaultDescription
runtime-tokioβœ…Timers, task spawning and sockets via Tokio
runtime-smolThe same, via smol
runtime-mockMockRuntime, a deterministic virtual-clock runtime for tests (no I/O)
crypto-ringβœ…The ring-based crypto provider
crypto-aws-lc-rsThe 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.

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:

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:

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

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

async
rtc
rust
webrtc

Project health

Overall

4.6/10

Maintained

10/10

Code review

3/10
OpenSSF Scorecard

crates.io

Latest release

0.21.0 Β· Sep 19, 2026

Releases this year

10

Significant stargazers

(top 24 of 86)

snek

1,044 followers Β· starred Feb 2022

Yonggang Luo

61 followers Β· starred May 2022

tottoto

70 followers Β· starred Dec 2022

Nazar Mokrynskyi

828 followers Β· starred Oct 2020