icey is a modular C++20 toolkit that unifies real-time device capture, media encoding, computer vision processing, STUN/TURN/ICE signalling, and network streaming into a single coherent pipeline architecture. Capture from a camera, run it through an encoder, pipe it over WebRTC to a browser; all async, all composable, no glue code.
The library has been running in production for over a decade. The current tree includes sharper support surfaces around the media core: sched for trusted long-running maintenance work, graft for native plugin loading, pacm for extension delivery, and speech / vision for intelligence-facing media primitives.
Architecture
The core abstraction is the PacketStream; a composable pipeline where sources emit packets, processors transform them, and sinks consume them. Everything is polymorphic through the IPacket interface, so a video frame, an audio buffer, and a network message all flow through the same machinery.
┌─────────────────────────────────────────────────────────────────┐
│ PacketStream │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Source │───▶│ Processor │───▶│ Sink │ │
│ │ │ │ │ │ │ │
│ │ Camera │ │ FFmpeg H.264 │ │ WebRTC Track Sender │ │
│ │ File │ │ Opus encode │ │ Network socket │ │
│ │ Network │ │ OpenCV │ │ File recorder │ │
│ │ Device │ │ Custom │ │ HTTP response │ │
│ └──────────┘ └──────────────┘ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
WebRTC send path:
MediaCapture → VideoEncoder → WebRtcTrackSender → [libdatachannel]
│
Browser ◀── RTP/SRTP ◀── DTLS ◀── ICE (libjuice) ◀───┘
│
icey TURN server
(relay for symmetric NATs)
WebRTC receive path:
[libdatachannel] → WebRtcTrackReceiver → FFmpeg decode → file/display
│
└─── ICE → DTLS → SRTP decrypt → RTP depacketise → raw frames
Signalling (Symple v4):
C++ server/client ◀──── WebSocket ────▶ Browser (symple-player)
Auth, presence, rooms, call protocol (init/accept/offer/answer/candidate)
Sources emit. Processors transform. Sinks consume. Priority ordering controls the chain. The stream manages lifecycle; start, stop, pause, resume, error recovery; as a state machine. You don’t manage threads. You don’t manage memory. You describe the flow.
PacketStream stream;
stream.attachSource(videoCapture);
stream.attach(new av::MultiplexPacketEncoder(opts), 5);
stream.attach(socket, 10);
stream.start();
Backpressure propagates through the chain. Write queues have high water marks. Frame dropping kicks in under load. The pipeline handles the ugly parts so application code stays clean.
What you can build
Stream a webcam to any browser
150 lines of C++. Camera capture, H.264 encoding, WebRTC transport, Symple signalling.
session.IncomingCall += [&](const std::string& peerId) {
session.accept();
};
session.StateChanged += [&](wrtc::PeerSession::State state) {
if (state == wrtc::PeerSession::State::Active) {
stream.attachSource(capture.get(), false, true);
stream.attach(encoder, 1, true);
stream.attach(&session->media().videoSender(), 5, false);
stream.start();
}
};
Record a browser’s camera server-side
Browser sends WebRTC, your C++ server decodes with FFmpeg, writes to any format. Video depositions, telehealth recording, proctoring; server-side recording without cloud vendor lock-in.
Stream any video file to a browser
Feed an MP4 in, get a real-time WebRTC stream out. Data channel for seek commands.
Run your own TURN relay
Production-grade RFC 5766 TURN server with channel binding and TCP support. ~30% of real-world WebRTC connections need relay through symmetric NATs.
Media capture and encoding
The AV module wraps FFmpeg’s codec infrastructure in RAII types that don’t leak. VideoCapture and AudioCapture continuously emit decoded packets from system devices; cameras via V4L2, AVFoundation, or DirectShow depending on platform. MediaCapture handles file input with optional looping.
Encoding chains VideoPacketEncoder and AudioPacketEncoder into the PacketStream as processors. For WebRTC, set encoder options for browser-safe low-latency encoding. For file output, use MultiplexPacketEncoder:
av::EncoderOptions opts;
opts.oformat = av::Format{
"MP4", "mp4",
av::VideoCodec{"H.264", "libx264", 640, 480, 25, "yuv420p"},
av::AudioCodec{"AAC", "aac", 2, 48000, 128000, "fltp"}
};
The encoder handles pixel format conversion (libswscale), PTS tracking, time base conversion, and codec flushing on close. All FFmpeg resources are wrapped in custom deleters. Compatible with FFmpeg 5, 6, and 7; the API adapts at compile time.
Networking on libuv
Every socket, timer, DNS lookup, and child process runs on libuv’s event loop. TCP, SSL, and UDP sockets share a common interface with chainable adapters for middleware-style extension. The SSL layer enforces TLS 1.2 minimum, supports ALPN negotiation, SNI, hostname verification, and session caching; all through OpenSSL 3.x’s EVP API.
The HTTP module implements client and server with WebSocket upgrade (RFC 6455), multipart forms, cookie management, and streaming responses. Built on llhttp; the same parser Node.js uses.
Performance
Benchmark suite isolating each layer of the stack. Baseline is a raw libuv + llhttp server: hand-rolled C, no abstractions, no connection management, no header building.
HTTP/1.1 keep-alive (realistic production scenario, connections reused across requests):
| Server | Req/sec | Latency |
|---|---|---|
| Raw libuv+llhttp | 96,088 | 1.04ms |
| icey | 72,209 | 1.43ms |
| Go 1.25 net/http | 53,878 | 2.31ms |
| Node.js v20 | 45,514 | 3.56ms |
75% of raw libuv throughput with a full HTTP stack: connection pooling, header construction, signal-driven dispatch, WebSocket upgrade, streaming responses. The difference against Go is architectural; Go’s net/http uses goroutines with an M:N scheduler and garbage collector, icey talks directly to libuv’s event loop with zero-copy buffer reuse and no GC pauses. All benchmark code ships with the library.
Runtime surfaces beyond media
schedgives long-running services a real deferred/periodic task module with explicit restore and shutdown semanticsgraftis the native plugin boundary: manifest validation, shared-library loading, typed entrypointspacmhandles delivery and install layout for native extensions, worker payloads, and other versioned assetsspeechandvisionhold intelligence-facing helpers without forcing the rest of the runtime to turn into AI glue code
NAT traversal
Full RFC implementations of STUN (5389) and TURN (5766). Actual server and client implementations:
- STUN: Binding requests for NAT detection, transaction IDs, message integrity, fingerprint validation
- TURN: Relay allocations (UDP and TCP), permission management, channel binding for fast-path data relay, configurable lifetime enforcement
Channel binding replaces 36-byte STUN headers with 4-byte channel headers on the data path; significant when you’re relaying real-time media at scale.
Signal-driven architecture
The event system uses typed Signal<T> templates with attach() for callback registration. Thread-safe, multi-listener, used everywhere from socket events to stream state changes to installation progress.
server.Connection += [](http::ServerConnection::Ptr conn) {
conn->Payload += [](auto&, const MutableBuffer& buf) {
// handle incoming data
};
};
16 modules, zero bloat
Each module is self-contained with explicit dependencies. Enable what you need, skip what you don’t:
| Module | What it does |
|---|---|
| base | Event loop, signals, packet streams, threading, timers, filesystem, logging |
| crypto | HMAC, SHA-256/512, RSA, X509; all OpenSSL 3.x EVP |
| net | TCP/SSL/UDP sockets, DNS, chainable adapters |
| http | Client/server, WebSocket RFC 6455, forms, cookies, streaming, keep-alive |
| json | JSON serialisation (nlohmann/json) |
| av | FFmpeg capture, encode, decode, record, stream (FFmpeg 5/6/7) |
| speech | Audio intelligence primitives for decoded media streams |
| vision | Video intelligence primitives for sampled decoded frames |
| symple | Presence, messaging, rooms, WebRTC call signalling |
| stun | RFC 5389 STUN for NAT traversal |
| turn | RFC 5766 relay server with channel binding |
| webrtc | WebRTC via libdatachannel: media bridge, peer sessions, codec negotiation |
| graft | Native plugin ABI and shared-library loading |
| pacm | Package delivery, install layout, and extension metadata |
| archo | ZIP extraction with path traversal protection |
| sched | Deferred and periodic scheduling with restore semantics |
All external dependencies resolve through CMake FetchContent; libuv 1.50, llhttp 9.2.1, OpenSSL 3.x, nlohmann/json 3.11.3, minizip-ng, zlib 1.3.1. No system package requirements beyond a C++20 compiler.
Build and test
git clone https://github.com/nilstate/icey.git
cd icey
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON
cmake --build build --parallel $(nproc)
ctest --test-dir build --output-on-failure
Modules build automatically when their dependencies are found. Install FFmpeg and OpenCV on your system, and the av module builds with them:
# Install optional deps (Ubuntu/Debian)
sudo apt install libavcodec-dev libavformat-dev libswscale-dev libopencv-dev
# Build - CMake auto-detects everything
cmake -B build -DCMAKE_BUILD_TYPE=Release
Use as a dependency:
include(FetchContent)
FetchContent_Declare(icey
GIT_REPOSITORY https://github.com/nilstate/icey.git
GIT_TAG v2.1.0
)
FetchContent_MakeAvailable(icey)
target_link_libraries(myapp PRIVATE icy_base icy_net icy_http)
CI runs GCC 12+, Clang 15+, AppleClang 15+, and MSVC 2022. BUILD_TESTS=ON by default; ASan, TSan, and UBSan sanitizer builds catch the things unit tests miss.
