File: README.md
[](https://webrtc.rs)
Table of Content
- Overview
- Getting Started
- The architecture
- Release lines
- What's in 0.21
- How to Provide Feedback
- Building and Testing
- Semantic Versioning
- Open Source License
- Contributing
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) areasyncPeerConnectionDriver— an internal background event loop, spawned automatically, that owns the sockets,
drives the Sans-I/Ortccore, 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.
Getting Started
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 implementcrypto::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
Runtimeabstraction (timers, task spawning, sockets, DNS) - Feature flags:
runtime-tokio(default) andruntime-smol, additive rather than mutually exclusive - Any third-party runtime works today: implement
Runtime, inject it per connection withwith_runtime.
The custom-runtime example does exactly that onasync-executor+async-io, with neither
built-in runtime compiled in runtime-mockgives 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 (
PeerConnectionhandle + backgroundPeerConnectionDriver) over the core
Release lines
v0.21.x— currentlyv0.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 fromRuntime::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 reachesEstablished 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 registersvideo/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:
- Review the architecture blog post
- Join discussions on GitHub Issues
- Chat with us on Discord
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-channelsSemantic 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. Once1.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). Newcrypto-ring(default) andcrypto-aws-lc-rs
Cargo features forward tortc. They are additive: enabling both compiles both providers andringremains the resolved default, so a dependency enablingcrypto-aws-lc-rscannot silently change
what an application runs. Building with neither compiles no provider, and the application
supplies its own. webrtc::peer_connection::cryptore-exports the provider API. Without itSettingEngine::set_crypto_providerwas uncallable from this crate — its signature namesArc<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.rscovers 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 assertswebrtcdepends on no
crypto implementation.Bind addresses are resolved on every bind, and a wildcard means "every interface"
(webrtc#874).with_udp_addrs/with_tcp_addrsvalues 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, and0.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 outlivebuild(),PeerConnectionBuilder::buildrequiresA: 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,errorfor 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
webrtcv0.20.0 is a clean, ergonomic, runtime-agnostic rewrite on top of a Sans-I/O corertc; - 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_writescoalesces deliberately: making it.send().awaitforces a 1:1 wake per message and collapses throughput on
cooperatively-scheduled runtimes. TheClosenudge inDropcannot 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:
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
// 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:
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:
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 forRTCPeerConnection.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 singleRTCIceTransport 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 returnControlling 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 carriesice_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 withAttribute::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 theRuntimetrait overasync-executor+async-io— neither Tokio nor smol — and injecting it withwith_runtime. Run it with--no-default-featuresto 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`
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.ivfThe bitrates matter: they are the numbers the estimate is compared against, and they are declared inQUALITY_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:
echo $BROWSER_SDP | cargo run --example bandwidth-estimation-from-diskThe 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:
cargo run --example bandwidth-estimation-from-disk -- -i my_fileInput 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:
Switching from low.ivf to med.ivf
Switching from med.ivf to high.ivf
Switching from high.ivf to med.ivfIt 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:
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
cargo build --example broadcastOpen 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_OFFERis the value you copied in the last step. - The
broadcastterminal 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
logsin 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_OFFERis the value you copied in the last step. - The
broadcastterminal 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
logsin 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
cargo build --example custom-runtimeRun custom-runtime
The interesting invocation disables both built-in runtimes, so the only Runtime in the process is the one the example defines:
cargo run --no-default-features --example custom-runtimeExpected output:
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:
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
- Implement
Runtime(the 8 required methods above). - Implement
AsyncUdpSocket,AsyncTcpListenerandAsyncTcpStreamfor your socket types.AsyncUdpSocketis poll-based: supplypoll_sendandpoll_recv, and the fourasyncconvenience methods come from defaults. - Return your task handle from
spawnviaJoinHandle::new(Box::new(..)), implementingJoinHandleInnerfor it. - 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
cargo build --example data-channelsOpen data-channels example page
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
- Paste the SessionDescription into a file.
- 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
cargo build --example data-channels-createOpen data-channels-create example page
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.
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:
$ 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 MbpsFile: 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:
cargo run --example data-channels-answerNext, run data-channels-offer:
cargo run --example data-channels-offerYou 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
- Run the server:
cargo run --example data-channels-simpleOpen browser at http://localhost:8080
Send messages via DataChannel and see them in terminal & browser logs.
How It Works
- The browser creates a DataChannel and generates an SDP offer
- The offer is sent to the Rust server via HTTP POST to
/offer - The server creates a PeerConnection, processes the offer, and returns an answer
- The browser sets the answer as remote description
- ICE candidates are exchanged via HTTP POST to
/candidate - Once connected, messages can be sent bidirectionally through the DataChannel
Architecture
┌─────────────┐ ┌──────────────────┐
│ 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
cargo build --example ice-restartRun ice-restart
cargo run --example ice-restartOpen 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 Restartis the button that causes a new offer to be made withiceRestart: true.ICE Connection Stateswill contain all the connection states the PeerConnection moves through.ICE Selected Pairswill print the selected pair every 3 seconds. Note how the uFrag/uPwd/Port change everytime you
start the Restart process.Inbound DataChannel Messagescontaining 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
PeerConnectionto 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
hyperto serve static pages and handle SDP signaling. - Establishing a
DataChannelover pure TCP and exchanging periodic messages.
How It Works
- The server starts an HTTP server on port
8080and configures a WebRTC TCP listener on port8443. - When a browser opens
http://localhost:8080, it generates an SDP offer with both TCP and UDP candidates. - The browser sends this SDP offer to the server via an HTTP
POSTto/doSignaling. - The server configures the
PeerConnection, sets the remote offer, and generates an answer. - The server waits for local ICE gathering to complete (disabling trickle ICE) so that the answer includes the server's
TCP passive candidate. - The server responds with the local SDP answer.
- The browser sets the answer and initiates an active TCP connection to the server's listener on port
8443. - Once connected, a
DataChannelis established, and the server sends timestamp messages every 3 seconds.
Running the Example
1. Run the Server
From the root of the project, run:
cargo run --example ice-tcpFor detailed trace logging, use:
cargo run --example ice-tcp -- --debug2. Access the Web UI
Open your browser and navigate to:
http://localhost:8080You 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
Answer side (TCP passive):
- Configures the
PeerConnectionwith a passive candidate listening on a TCP port (default:8443). - Serves an HTTP signaling server on port
60000to receive the SDP offer. - Sets the remote description, generates an answer, and returns it.
- Configures the
Offer side (TCP active):
- Configures the
PeerConnectionwith 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).
- Configures the
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:
cargo run --example ice-tcp-passive-answer2. Run the Offer Side (Active)
In a separate terminal, run:
cargo run --example ice-tcp-active-offerFor detailed logging, append the --debug flag:
cargo run --example ice-tcp-passive-answer -- --debug
cargo run --example ice-tcp-active-offer -- --debugFile: 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
ffmpeg -i $INPUT_FILE -g 30 output.ivfBuild insertable-streams
cargo build --example insertable-streamsOpen 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
- Paste the SessionDescription into a file.
- 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
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
- Paste the SessionDescription into a file.
- 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 seeData 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
ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivfNote: -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.
cargo run --example play-from-disk-fec -- -v rtc/examples/test-data/output_vp8.ivfIt 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:
cargo run --example play-from-disk-fec -- -v rtc/examples/test-data/output_vp8.ivf -i answer.txtEnjoy 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:
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
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.oggOpen 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
Run
echo $BROWSER_SDP | cargo run --example play-from-disk-h26x -- -v rtc/examples/test-data/output.264 -a rtc/examples/test-data/output.oggRun
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
- Paste the SessionDescription into a file.
- Run
cargo run --example play-from-disk-h26x -v rtc/examples/test-data/output.264 -a rtc/examples/test-data/output.ogg < my_file - 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
OggReaderand 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
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.oggRun it
cargo run --example play-from-disk-playlist-controlOr with a custom address and playlist file:
cargo run --example play-from-disk-playlist-control -- --addr 127.0.0.1:8080 --playlist-file my_music.oggThen open:
http://127.0.0.1:8080Command-line options
-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
cargo build --example play-from-disk-renegotiationCreate IVF named `output.ivf` that contains a VP8 track
ffmpeg -i $INPUT_FILE -g 30 output.ivfRun 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
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.oggBuild play-from-disk-vpx
cargo build --example play-from-disk-vpxOpen 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
- 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 - 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
- Paste the SessionDescription into a file.
- 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 - 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
cargo build --example reflectOpen 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
- Paste the SessionDescription into a file.
- 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
- Building a
PeerConnectionwith the asyncPeerConnectionBuilderpattern used by the other async examples. - Adding a custom
RtcpForwarderInterceptorthat narrows inbound RTCP to keyframe requests — PLI and FIR — and marks
those withAttribute::DeliverToApplicationso they reach the application instead of stopping at the end of the chain. - Receiving those requests on the async side through
TrackRemoteEvent::OnRtcpPacket. - 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
cargo build --example rtcp-processingRun
cargo run --example rtcp-processingWith debug logging
cargo run --example rtcp-processing -- --debugRead SDP from a file
cargo run --example rtcp-processing -- --input-sdp-file offer.txtSignaling flow
- Paste a base64-encoded SDP offer from a browser.
- Copy the printed base64 answer back into the browser.
- Start sending audio/video from the browser.
- Watch incoming RTCP packets printed per remote track.
Async RTCP delivery
With the custom forwarder registered, RTCP arrives through the remote track event loop:
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 eachPacket::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
cargo build --example rtp-forwarderOpen 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
- Paste the SessionDescription into a file.
- 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
cargo build --example rtp-to-webrtcOpen 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
- Paste the SessionDescription into a file.
- 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
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=5004ffmpeg
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
cargo build --example save-to-disk-av1Open 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
- Paste the SessionDescription into a file.
- 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
--clientkeeps the sansio example's DTLS answering-role override.--hostand--portcontrol 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
cargo run --example play-from-disk-fec -- -v rtc/examples/test-data/output_vp8.ivfIt 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:
cargo run --example save-to-disk-fec -- -i offer.txt -v output.ivfIt 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:
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:
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-03registered in theMediaEngine. An answerer can only select payload types the offer listed,
so the repair codec has to be registered here for the offer'sa=rtpmap:49 flexfec-03/90000to 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 printsFEC negotiated: …on bind, and says so loudly when
a media stream binds without a repair SSRC.FlexFec03ReceiveatSlot::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
cargo build --example save-to-disk-h26xOpen 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
- Paste the SessionDescription into a file.
- 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
cargo build --example save-to-disk-vpxOpen 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
- Paste the SessionDescription into a file.
- 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
cargo build --example simulcastOpen 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
- Paste the SessionDescription into a file.
- 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
cargo build --example simulcastOpen 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
- Paste the SessionDescription into a file.
- 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:
echo $BROWSER_SDP | cargo run --example statsWindows
- Paste the SessionDescription into a file.
- Run:
cargo run --example stats < my_fileInput stats' SessionDescription into your browser
Copy the base64 string that stats just emitted and paste it into the second text area in the browser.
Hit 'Start Session' in jsfiddle
The stats program will now print WebRTC statistics every 5 seconds, including InboundRTPStreamStats for each incoming stream and Remote IP+Ports.
You will see the following in your console:
=== WebRTC Stats ===
{
"data_channels_closed": 0,
"data_channels_opened": 0
}
Inbound RTP Stats for: video/vp8
{
"packets_received": 1255,
"packets_lost": 0,
"jitter": 588.9559641717999,
"bytes_received": 1361125,
"track_identifier": "video-track",
"ssrc": 1234567890,
...
}
Inbound RTP Stats for: audio/opus
{
"packets_received": 2450,
"packets_lost": 0,
"jitter": 12.5,
"bytes_received": 245000,
"track_identifier": "audio-track",
"ssrc": 987654321,
...
}
Remote Candidate:
{
"address": "192.168.1.93",
"port": 59239,
"candidate_type": "host",
...
}
====================What it demonstrates
This example demonstrates:
- How to use the
peer_connection.get_stats()API to retrieve WebRTC statistics asynchronously. - 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()
- Peer connection stats via
- 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
cargo build --example swap-tracksOpen 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
- Paste the SessionDescription into a file.
- 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):
cargo run --example trickle-iceEnable only specific candidate types:
# 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-srflxCustomize STUN or TURN settings:
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=passWith debug logging:
cargo run --example trickle-ice -- --debugThen 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:
cargo run --example trickle-ice-hostOr with debug logging:
cargo run --example trickle-ice-host -- --debugThen 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:
RUST_LOG=trace cargo run --color=always --package turn --example turn_server_udp -- --public-ip <browser-reachable-ip> --users user=passOption 2: pion/turn server
From the pion/turn repository:
./simple -public-ip <browser-reachable-ip> -users user=passRun
From the repository root:
cargo run --example trickle-ice-relayWith explicit TURN settings:
cargo run --example trickle-ice-relay -- --turn-host <browser-reachable-ip> --turn-port 3478 --turn-user user=passWith debug logging:
cargo run --example trickle-ice-relay -- --debugThen 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:
cargo run --example trickle-ice-srflxOr with debug logging:
cargo run --example trickle-ice-srflx -- --debugThen 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",
// 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
- 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: trueFile: .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
- 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