Eight methods, one trait, and no clock of its own: the rtc sans-I/O contract
Sans-IO WebRTC implementation in Rust
At a glance
- What is it?
- rtc is a Rust implementation of WebRTC that keeps every socket, thread, and deadline in the caller's hands, so the library's entire surface is a small protocol trait. The payoff is a session that replays in a test without a socket or a sleep. The cost is that you bind the sockets, gather the ICE candidates, and pick a crypto provider per peer connection.
- Who is it for?
- rtc is the right fit if you are wiring WebRTC into an existing async runtime, a test harness, or a hardware pipeline and cannot accept a library that owns your sockets, threads, or clock. It is the wrong fit if you want a turnkey peer connection, because candidate gathering, socket binding, and event loop plumbing are all your problem.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The library takes packets in and hands packets out
The premise is a single sentence: RTC separates protocol logic from I/O operations, giving you control over networking, threading, and async runtime integration. Concretely, instead of performing network reads and writes itself, the library expects you to feed it network data and then tells you what to send. Four consequences follow from that. It is runtime independent, working with tokio, async-std, smol, or plain blocking I/O. You keep control of threading, scheduling, and I/O multiplexing. Protocol logic can be tested without real network I/O. And it drops into existing networking code rather than replacing it. The comparison the project draws is with traditional WebRTC libraries, which own the socket and therefore own the runtime too. If your application already has an event loop, that ownership is the thing you are removing from the dependency.
One trait, eight methods, and a ninth you cannot call
The whole event loop surface is the `sansio::Protocol` trait as implemented by `RTCPeerConnection`, and one detail catches people out: the trait has to be in scope with `use rtc::sansio::Protocol;` or none of those methods resolve. The inbound side is four calls. `handle_read` takes one received UDP or TCP datagram, tagged with its 5-tuple and arrival instant. `handle_timeout` tells the connection a deadline has passed. `handle_write` queues an outbound RTP, RTCP, or data channel message. `close` shuts the connection down. The outbound side is four polls: `poll_write` takes the next packet to put on the wire and is drained until it returns `None`, `poll_read` takes the next inbound message for the application, `poll_event` takes the next state change or notification, and `poll_timeout` returns the next deadline for retransmissions, keepalives, and ICE checks. A ninth method, `handle_event`, is part of the trait but cannot be used, because `RTCEvent` is currently uninhabited and no value of it can be constructed. It exists so the signature is already correct when the first inbound event variant arrives.
Ordering only matters in one direction, and it is about draining
The rule for sequencing the eight methods is narrow. Nothing sends immediately: `handle_read`, `handle_timeout`, `handle_write`, and the negotiation calls all queue packets rather than transmitting them. That means ordering is not a correctness requirement in the usual sense, with one exception. `poll_write` should be drained after anything that could have produced output, because that is where queued packets leave. Two details in the signatures carry more weight than they look like they do. `handle_read` does not take a bare buffer, it takes a `TaggedBytesMut`, so the 5-tuple and the arrival time travel with the payload rather than being set separately, which is what allows one connection to sit behind several sockets. And `poll_read` returns a `TaggedRTCMessage` rather than a decoded payload, keeping the same tagging on the way out. Negotiation is not in the method table at all: the event loop example imports `RTCConfigurationBuilder` and `RTCSessionDescription`, so configuration and offer and answer are handled with their own types rather than through the loop.
There is no ambient clock, which is what makes tests replayable
The second half of the sans-I/O bargain is time. There is no ambient clock, and every method that needs the current time takes an `Instant` from you. `poll_timeout` returns the next deadline for retransmissions, keepalives, and ICE checks, and `handle_timeout` takes the instant at which you believe that deadline arrived. Because time is an input rather than a global, a whole session becomes reproducible in a test with no socket and no sleep: you feed a recorded sequence of tagged datagrams, you hand over the instants you want the connection to believe, and the output is deterministic. The same property is why the listed benefit is testability rather than speed. It also means the burden lands on the caller in production, where somebody has to own the timer wheel and decide what happens when a deadline is missed, and the README does not describe a helper for that.
Crypto is chosen per peer connection, not installed process-wide
All cryptography goes through a pluggable `RTCCryptoProvider`, and two providers ship with the crate:
# Default: the ring provider, no C toolchain required.
rtc = "0.21"
# aws-lc-rs instead (pulls in the aws-lc-sys C toolchain).
rtc = { version = "0.21", default-features = false, features = ["crypto-aws-lc-rs"] }The Cargo features are additive, so enabling both builds successfully, and ring is preferred when both are on. The default needs no C toolchain; the alternative pulls in the aws-lc-sys C toolchain, which is a real cost on a build machine you do not control. There is no process-global provider to install. The choice is made per peer connection through `SettingEngine::set_crypto_provider`, so two peer connections inside one process may use different providers. Anything else, including OpenSSL, a FIPS-validated module, an HSM, or a platform backend, is implemented by writing to the public traits yourself, then validated against the bundled conformance suite at `rtc_crypto::conformance::assert_provider`, which sits behind the `test-support` feature.
You bind the sockets, so you tell it which candidates exist
The same rule that removes I/O removes candidate gathering. Because RTC does no I/O, it also does no candidate gathering, which means the socket binding, the port allocation, and the interface enumeration are all yours. You build candidates with the `rtc-ice` candidate constructors, the type names in the example being `CandidateConfig`, `CandidateHostConfig`, `RTCIceCandidate`, and `RTCIceCandidateInit`, and hand each one to `add_local_candidate`. The corresponding gather function takes a mutable peer connection and returns a `Result`, so a failure to build a candidate is an ordinary error value rather than a log line. This is the part of the design with the largest practical consequence, since interface enumeration, address families, and port reuse policy are exactly the things a mature WebRTC stack has spent years getting right, and here they become application code.
Fifteen member crates sit behind one facade crate
The repository is a Cargo workspace of fifteen member crates with a facade crate on top: rtc-crypto, rtc-datachannel, rtc-dtls, rtc-ice, rtc-interceptor, rtc-mdns, rtc-media, rtc-rtcp, rtc-rtp, rtc-sctp, rtc-sdp, rtc-shared, rtc-srtp, rtc-stun, and rtc-turn, resolved with `resolver = "2"`. The workspace pins every one of them at 0.21.0 with both a path and a package name, and several are declared `default-features = false`, including crypto, dtls, ice, shared, and srtp, which tells you the facade is the layer that assembles the feature set rather than each member deciding on its own. The shared metadata sets edition 2024, the keywords sansio, networking, and protocols, and the category network-programming, with a single author, Rain Liu. Licensing is dual, with both LICENSE-APACHE and LICENSE-MIT at the root and the workspace license field set to MIT/Apache-2.0, and the README links to the Rust project's own explanation of why that pairing exists. There is a .cargo/ directory, a codecov.yml, and a benchmarks/ directory, plus a dev profile that pins opt-level to 0. The table of contents also promises sections on back-pressure, architecture, common use cases, specification compliance, and semantic versioning, and the project is reachable through crates.io, docs.rs, deps.rs, codecov, and a Discord invite, with a single sponsor at gold level and two at silver.
Twenty-five examples, and six of them play the same file back
The examples directory is the fastest map of what the workspace covers, and it clusters. Data channels get the most attention with seven separate projects: create, close, simple, flow control, offer and answer, and the parent data-channels entry. ICE gets three, covering plain TCP, active and passive TCP, and a restart. The play-from-disk family is the largest single cluster, with separate projects for H.26x, VPx, forward error correction, playlist control, and renegotiation, joined by save-to-disk-av1, so three codec families are demonstrated through files rather than cameras. The rest are mdns-query-and-gather, perfect-negotiation, insertable-streams, reflect, rtcp-processing, rtp-forwarder, rtp-to-webrtc, bandwidth-estimation-from-disk, and broadcast. Two of those names describe roles rather than demos, rtp-forwarder and rtp-to-webrtc, which is a fair hint that this workspace is aimed at infrastructure that sits between endpoints rather than at applications that join calls. An examples/README.md sits alongside them, and the parent data-channels directory suggests that cluster is a workspace of its own rather than one program.
Editorial conclusion
rtc is the right fit if you are wiring WebRTC into an existing async runtime, a test harness, or a hardware pipeline and cannot accept a library that owns your sockets, threads, or clock. It is the wrong fit if you want a turnkey peer connection, because candidate gathering, socket binding, and event loop plumbing are all your problem. Before pinning, check that your toolchain is on Rust edition 2024 and read the crypto provider section, since the default pulls in ring and the alternative pulls a C toolchain.
Frequently asked questions
What does sans-I/O mean in the rtc crate?
The library handles protocol logic and you control all I/O. You feed it network data and it tells you what to send, and there is no ambient clock, so every method that needs the time takes an `Instant` from you. It works with tokio, async-std, smol, or blocking I/O.
Which methods make up the rtc event loop?
The `sansio::Protocol` trait has `handle_read` for a tagged datagram, `handle_timeout`, `handle_write`, and `close` going in, and `poll_write`, `poll_read`, `poll_event`, and `poll_timeout` coming out. A ninth, `handle_event`, is unusable because `RTCEvent` is uninhabited.
How do you choose a crypto provider in rtc?
Two providers ship with the crate: ring by default, needing no C toolchain, and aws-lc-rs behind the `crypto-aws-lc-rs` feature, which pulls in aws-lc-sys. The features are additive and ring wins when both are on, and the provider is set per peer connection with `SettingEngine::set_crypto_provider`.
Does rtc gather ICE candidates for you?
No. Because it does no I/O, it does no candidate gathering either. You bind the sockets, build candidates with the `rtc-ice` constructors such as `CandidateHostConfig`, and hand each one to `add_local_candidate`.
What Rust edition and crate version does rtc require?
Rust edition 2024, and the install section adds the crate at version 0.21 to your dependencies. The workspace pins all fifteen member crates at 0.21.0, and the repository root carries both LICENSE-APACHE and LICENSE-MIT for a dual MIT and Apache-2.0 license.
What is covered by the rtc examples directory?
Twenty-five projects: seven data channel variants, three ICE variants covering TCP, active-passive TCP, and restart, an mDNS query and gather example, perfect negotiation, insertable streams, a reflect server, RTCP processing, an RTP forwarder, an RTP to WebRTC bridge, bandwidth estimation from disk, broadcast, and a play-from-disk family covering H.26x, VPx, FEC, playlist control, and renegotiation.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/webrtc-rs-rtc)