Async-friendly WebRTC implementation in Rust
See the code
Async-friendly WebRTC implementation in Rust
Sponsored with π by
Silver Sponsors:
Bronze Sponsors:
AdrianEddy
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:
PeerConnection β the user-facing async API handle; all operations (create offers/answers, add tracks,
create data channels) are asyncPeerConnectionDriver β an internal background event loop, spawned automatically, that owns the sockets,
drives the Sans-I/O rtc core, handles timeouts, and dispatches eventsRuntime β 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.
Add the crate:
[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.
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.
β Runtime independence
Runtime abstraction (timers, task spawning, sockets, DNS)runtime-tokio (default) and runtime-smol, additive rather than mutually exclusiveRuntime, 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 inruntime-mock gives tests a deterministic virtual clock, so timing-dependent behaviour is testable instantly and
without socketsβ Clean event handling
PeerConnectionEventHandler) for every event, with default no-op methods so you
implement only what you need β no per-event callback registrationArc<MyHandler>. Methods take &self, so mutable handler state
goes behind a single lock rather than being captured per closureβ Sans-I/O foundation
PeerConnection handle + background PeerConnectionDriver) over the corev0.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.
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.
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.
# 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
This project follows Semantic Versioning:
0.x.Y): Bug fixes and internal improvements with no public API changes.0.X.0): Backwards-compatible additions or deprecations to the public API.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.
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.
Contributors or Pull Requests are Welcome!!!
Overall
Maintained
Code review
crates.io
Latest release
Releases this year
(top 24 of 86)
1,044 followers Β· starred Feb 2022
61 followers Β· starred May 2022
70 followers Β· starred Dec 2022
828 followers Β· starred Oct 2020
Async-friendly WebRTC implementation in Rust
See the code
Async-friendly WebRTC implementation in Rust
Sponsored with π by
Silver Sponsors:
Bronze Sponsors:
AdrianEddy
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:
PeerConnection β the user-facing async API handle; all operations (create offers/answers, add tracks,
create data channels) are asyncPeerConnectionDriver β an internal background event loop, spawned automatically, that owns the sockets,
drives the Sans-I/O rtc core, handles timeouts, and dispatches eventsRuntime β 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.
Add the crate:
[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.
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.
β Runtime independence
Runtime abstraction (timers, task spawning, sockets, DNS)runtime-tokio (default) and runtime-smol, additive rather than mutually exclusiveRuntime, 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 inruntime-mock gives tests a deterministic virtual clock, so timing-dependent behaviour is testable instantly and
without socketsβ Clean event handling
PeerConnectionEventHandler) for every event, with default no-op methods so you
implement only what you need β no per-event callback registrationArc<MyHandler>. Methods take &self, so mutable handler state
goes behind a single lock rather than being captured per closureβ Sans-I/O foundation
PeerConnection handle + background PeerConnectionDriver) over the corev0.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.
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.
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.
# 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
This project follows Semantic Versioning:
0.x.Y): Bug fixes and internal improvements with no public API changes.0.X.0): Backwards-compatible additions or deprecations to the public API.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.
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.
Contributors or Pull Requests are Welcome!!!
Overall
Maintained
Code review
crates.io
Latest release
Releases this year
(top 24 of 86)
1,044 followers Β· starred Feb 2022
61 followers Β· starred May 2022
70 followers Β· starred Dec 2022
828 followers Β· starred Oct 2020