A WebRTC library written entirely in Dart — RFC-compliant protocols.
webdartc implements the W3C WebRTC API as a set of protocol state machines that turn inputs into outputs deterministically, which keeps the protocol logic testable and packet-level behaviour reproducible.
Data channels and media (audio + video, send + receive) are both supported.
- RFC-compliant protocols — STUN (RFC 5389/8489), ICE (RFC 8445), Trickle ICE (RFC 8840), ICE consent freshness (RFC 7675), TURN (RFC 5766/8656, with UDP / TCP / TLS transports), DTLS 1.2 (RFC 6347), SRTP (RFC 3711), SCTP (RFC 4960), DCEP (RFC 8832), RTP/RTCP (RFC 3550), SDP (RFC 4566/8866), and the RTP payload formats for H.264 (RFC 6184, STAP-A + FU-A), VP8 (RFC 7741), VP9, and Opus (RFC 7587).
- Pure state machines — protocol modules produce deterministic outputs from inputs.
- W3C surface —
PeerConnection,DataChannel, transceivers, and aWebdartcfactory that owns a sharedSettingEngine+MediaEngine. Public types drop theRTCprefix (PeerConnection, notRTCPeerConnection). - Platform-native crypto via FFI — CommonCrypto + Security.framework on
macOS, CNG (
bcrypt.dll) on Windows, and BoringSSL (built via vcpkg, statically linked into the bundledwebdartc_cryptowrapper) on Linux + Android. - Codecs — VP8 / VP9 via libvpx (Android: MediaCodec), H.264 via Apple
VideoToolbox (hardware-accelerated on macOS), Android MediaCodec
(
AMediaCodecvia FFI), or Cisco-prebuilt OpenH264 (Linux + Windows), Opus via libopus (Android: MediaCodec). The libvpx + libopus submodules are still cross-compiled with the NDK for Android (bundled load-check). See Codec backends for the per-OS codec source split (vcpkg on macOS / Windows, submodules elsewhere).
- Dart SDK
>= 3.11.0 < 4.0.0. - macOS — Xcode (VideoToolbox / CoreMedia / CoreVideo). libvpx + libopus are
source-built via vcpkg, which the build hook clones + bootstraps itself (or
honours
VCPKG_ROOT) and which brings its own CMake; no submodules needed. - Linux —
cmake clang nasm pkg-config(build libopus, assemble libvpx's x86_64 SIMD; pkg-config for vcpkg's BoringSSL port). OpenH264 is auto-downloaded; codecs come from the submodules; BoringSSL is source-built via vcpkg (the build hook clones + bootstraps vcpkg itself, or honoursVCPKG_ROOT). Submodules required. - Windows — MSVC (Visual Studio "Desktop development with C++", already
required by
flutter build windows). The VP8 / VP9 / Opus wrapper DLLs are source-built from libvpx / libopus via vcpkg (auto-cloned + bootstrapped by the build hook, orVCPKG_ROOT); the Cisco OpenH264 binary is downloaded and CNG provides crypto. No submodules needed. - Android — built through Flutter (
flutter test integration_test, notdart test); needs the Android SDK + NDK + CMake + pkg-config. The build hook cross-compiles libvpx + libopus (codecs) with the NDK and source-builds BoringSSL (crypto) via vcpkg for each ABI (ANDROID_NDK_HOME). H.264 binds the system MediaCodec (AMediaCodec) via FFI and needs no build step.
dependencies:
webdartc:
git: https://github.com/nus/webdartc.gitdart pub getimport 'package:webdartc/webdartc.dart';
final pc = PeerConnection(configuration: PeerConnectionConfiguration());
// Offer / answer (exchange the SDP via your own signaling channel).
final offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await pc.setRemoteDescription(remoteAnswer);
// Data channel.
final dc = pc.createDataChannel('chat');
dc.onMessage.listen((msg) => print('received: $msg'));
dc.send('hello');Receiving media follows the W3C path — onTrack hands you a MediaStreamTrack
that already decodes. The bundled codec backends (VP8 / VP9 / H.264 / Opus) are
auto-registered, and subscribing to the track lazily starts an internal
jitter-buffer → depacketize → decode pipeline:
pc.onTrack.listen((event) {
final track = event.track; // non-null when the codec is supported
if (track == null) return; // else use event.receiver.onRtp (raw RTP)
if (track.kind == 'video') {
track.onVideoFrame.listen((frame) { /* render */ frame.close(); });
} else {
track.onAudioData.listen((audio) { /* play */ });
}
});For raw RTP (relays, SFU forwarding, custom codecs) use event.receiver.onRtp,
which always emits packets regardless of codec registration. To manage the
backends yourself, pass autoRegisterCodecs: false to PeerConnection /
Webdartc and register a subset.
For configured ICE servers, codec preferences, or port ranges, build through the
Webdartc factory:
final webrtc = Webdartc(
settingEngine: SettingEngine(iceServers: [/* STUN / TURN */]),
mediaEngine: MediaEngine(), // VP8 + VP9 + H.264 + Opus by default
);
final pc = webrtc.createPeerConnection(); Webdartc factory (SettingEngine + MediaEngine)
│
PeerConnection (W3C API: DataChannel, transceivers, stats)
│
TransportController ← UDP send / receive
│
┌──────┬──────┼──────┬──────┬──────┬──────┐
ICE TURN DTLS SRTP SCTP RTP/RTCP SDP
│ │
STUN Crypto (CommonCrypto / BoringSSL / CNG via FFI)
Every protocol module follows one shape:
- Input —
processInput(Uint8List packet, remoteIp, remotePort) → ProcessResult - Timers —
handleTimeout(TimerToken) → ProcessResult - Output —
ProcessResultcarriesList<OutputPacket>+ an optional nextTimeout
Methods return Result<T, ProtocolError> (sealed ParseError / StateError /
CryptoError / InternalError) rather than throwing.
lib/
├── webdartc.dart # public API exports
├── api/ # Webdartc factory, SettingEngine, MediaEngine, stats
├── peer_connection/ # W3C PeerConnection, DataChannel, events
├── transport/ # TransportController — UDP send / receive
├── ice/, turn/, dtls/, srtp/, sctp/, stun/, rtp/, sdp/
├── crypto/ # platform crypto backends (FFI)
├── media/ # MediaStream, tracks, frames, FakeVideoSource
│ # receiver_track + receive_pipeline (decode path)
├── codec/
│ ├── codec_registry.dart # + default_codecs.dart (registerDefaultCodecs)
│ ├── video_codec.dart # W3C VideoEncoder / VideoDecoder
│ ├── audio_codec.dart # W3C AudioEncoder / AudioDecoder
│ ├── vp8/, vp9/ # libvpx FFI (vcpkg macOS/Windows, submodule Linux/Android)
│ ├── opus/ # libopus FFI (same split as vp8/vp9)
│ └── h264/ # OpenH264 (Linux/Windows) + VideoToolbox (macOS) backends
└── core/ # state machine base, Result<T,E>, shared types
hook/build.dart # native-asset build hook (codecs + VideoToolbox shim)
src/ # C wrappers: wvt_callback, webdartc_opus, webdartc_vp8/vp9
third_party/ # libopus + libvpx submodules (static, hidden symbols)
test/
├── crypto/, stun/, ice/, dtls/, srtp/, sctp/, rtp/, sdp/, codec/
├── fuzz/ # fuzz tests
└── e2e/ # browser e2e (Chrome / Firefox)
example/
├── ice_gather.dart # ICE candidate gathering
├── opus_codec.dart # Opus encode/decode round-trip + SNR check
├── get_user_media_macos.dart # open camera/mic via AVFoundation (macOS)
├── audio_renderer_macos.dart # speaker playback via AudioQueue (macOS)
├── audio_send/ # Dart → browser audio (Opus)
├── audio_receive/ # browser → Dart audio
├── video_sender/ # Dart → browser video (VP8 / H.264 fake source)
├── video_receiver/ # browser → Dart video (onTrack → onVideoFrame)
├── video_echo/ # browser → Dart → browser (RTP packet forward)
├── getusermedia_call/ # real camera + mic → browser (macOS, audio + video)
├── signaling/ # HTTP + WS relay (OpenAyame, for the Flutter demo)
└── serve.dart # shared static-file serving for the demos above
dart test # unit tests (runs the build hook)
dart test test/e2e/ # browser e2e (Chrome auto-downloaded)
dart test test/ice/ice_test.dart # a single fileE2E tests are tagged e2e and need Chrome or Firefox with WebDriver; helpers
live in test/e2e/. Fuzz tests are in test/fuzz/.
Each example/<name>/server.dart is a self-contained dart run entrypoint that
serves its own browser page and acts as the Dart peer.
# ICE candidate gathering against a public STUN server
dart run example/ice_gather.dart stun:stun.l.google.com:19302
# Dart → browser fake video (open http://localhost:8080 in Chrome)
dart run example/video_sender/server.dart --port=8080 --codec=h264
# browser camera → Dart decoder (macOS VideoToolbox for H.264)
dart run example/video_receiver/server.dart --port=8080 --codec=h264
# browser camera echoed back through a Dart RTP forwarder
dart run example/video_echo/server.dart --port=8080Every backend is software except VideoToolbox on macOS and MediaCodec on Android, which use the OS-provided codec (hardware-accelerated where the device offers it).
| Codec | macOS | Linux | Windows | Android |
|---|---|---|---|---|
| H.264 | VideoToolbox (HW); hook/build.dart compiles src/wvt_callback.c |
OpenH264, pinned download from ciscobinary.openh264.org |
OpenH264, same Cisco prebuilt path | MediaCodec (NDK AMediaCodec via FFI); no build step |
| VP8 | libvpx via vcpkg, statically linked | libvpx submodule, source-built + statically linked | webdartc_vp8.dll wrapper from libvpx via vcpkg + MSVC |
libvpx submodule, NDK cross-compiled per ABI |
| VP9 | same as VP8 (shares the libvpx archive) | same as VP8 | webdartc_vp9.dll (same archive as vp8) |
same as VP8 (shares the libvpx archive) |
| Opus | libopus via vcpkg, statically linked | libopus submodule, source-built + statically linked | webdartc_opus.dll wrapper from libopus via vcpkg + MSVC |
libopus submodule, NDK cross-compiled per ABI |
hook/build.dart runs on every platform and selects the path:
- VideoToolbox shim (macOS) — compiles
src/wvt_callback.cinto a bundled dylib. The shim retains eachCMSampleBufferbefore the VT callback returns and queues it for the Dart side to drain — somethingNativeCallable.listenercan't do alone. - OpenH264 (Linux + Windows) — downloads the Cisco prebuilt binary
(
_openH264Version/_openH264Sha256pin version + hash) and registers it as aDynamicLoadingBundledasset. See https://www.openh264.org/ for upstream terms. - MediaCodec (Android) — no native asset;
lib/codec/h264/mediacodec/binds the systemlibmediandk.so(AMediaCodec) via pure-Dart FFI, using the synchronous buffer API so it needs no C shim (unlike the VideoToolbox path). MediaCodec is the OS-provided, patent-licensed codec — the Android analogue of VideoToolbox. Regenerate the FFI bindings withdart run tool/gen_mediacodec_bindings.dart(needs the NDK's libclang). - libopus / libvpx source build (macOS / Windows, via vcpkg) —
vcpkg install(ports pinned bytool/lib{opus,vpx}_vcpkg/vcpkg.json) produceslibopus.a/libvpx.a; on macOS the build hook links them directly into the bundled dylibs, on Windowstool/build_lib{opus,vpx}_wrappers.dartcompiles thewebdartc_*DLLs with MSVC. vcpkg is auto-cloned + bootstrapped if not onVCPKG_ROOT/ PATH; Windows additionally needs MSVC (already aflutter build windowsprerequisite). The libvpx archive is built once per triplet and shared by VP8 + VP9. - libopus / libvpx source build (Linux / Android, via submodules) — CMake /
libvpx's
configurebuild the bundledthird_party/{opus,libvpx}submodules (Android cross-compiles through the NDK toolchain). Samewebdartc_{opus,vp8,vp9}.cwrappers, exporting onlywebdartc_*.
Every codec symbol is hidden (-fvisibility=hidden on macOS / Linux;
__declspec(dllexport) for webdartc_* only on Windows) so the bundled copies
can't collide with another libopus / libvpx in the same process. For libopus we
additionally pre-define OPUS_EXPORT= to neutralize its own
visibility("default").
| Primitive | macOS | Linux | Windows | Android |
|---|---|---|---|---|
| AES-128-CM / AES-GCM (SRTP) | CommonCrypto | BoringSSL | CNG (BCrypt) | BoringSSL |
| ECDH P-256 | Security.framework | BoringSSL | CNG (BCrypt) | BoringSSL |
| ECDSA P-256 | Security.framework | BoringSSL | CNG (BCrypt) | BoringSSL |
| HMAC-SHA1 / SHA-256 | package:crypto | package:crypto | package:crypto | package:crypto |
| CSPRNG | Random.secure() |
Random.secure() |
Random.secure() |
Random.secure() |
Linux + Android share one backend: BoringSSL is source-built via vcpkg and
statically linked into the bundled webdartc_crypto wrapper, which exports only
the wd_* passthroughs that lib/crypto/openssl.dart binds via @Native
(BoringSSL's own symbols stay hidden — the same shape as the codec wrappers).
ChaCha20-Poly1305 and the self-signed DTLS certificate use the pure-Dart
implementations (BoringSSL exposes ChaCha only via EVP_AEAD, not EVP_CIPHER).
webdartc bundles or downloads libvpx, libopus, and OpenH264 — their licenses
apply to anything you redistribute. Full text is in
THIRD_PARTY_NOTICES.md. On Linux the build hook also
drops a NOTICE.txt next to the downloaded OpenH264 binary so the governing text
travels with it.