Real-time is one of the few disciplines in software where the deadline is physics. A video frame is sixteen milliseconds. The packet that misses its window is dropped, not rescheduled, and the human watching notices. Most media libraries were written for throughput or portability, not for the frame; icey is the C++ library written for it.
The most common live video source on the internet today is the RTSP camera. Tens of millions of them, every brand, every street corner. The browser cannot speak RTSP. The bridge between them is exactly the work icey was built to do: pull from RTSP, decode with FFmpeg, repacketize, send over WebRTC, all inside one process, all under the frame budget.
docker run --rm --network host 0state/icey-server:latest
Open http://localhost:4500 and click Watch on the icey peer. The video came from a single C++ process that captured, decoded, signalled, and transported it without leaving the runtime.
One graph
icey is built around one idea. A graph: source emits packets, processor transforms, sink consumes. Everything that moves through the runtime moves through this graph; raw video frames, encoded packets, network buffers, state changes. Traditionally this would be five libraries glued together with format conversions and threading bugs; icey wires them into a single graph that knows about all of them.
┌─────────────────────────────────────────────────────────────────┐
│ 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)
The graph runs as a state machine. Backpressure propagates upstream; sinks too slow to drain mark their write queues, queues hit high water, processors drop frames before sources overrun. The pipeline negotiates the ugly parts so the application code stays clean.
PacketStream stream;
stream.attachSource(videoCapture);
stream.attach(new av::MultiplexPacketEncoder(opts), 5);
stream.attach(socket, 10);
stream.start();
The work
A webcam to a browser, 150 lines
The same PacketStream graph: camera source, H.264 encoder, WebRTC track sender. Symple handles the call signalling. Full walkthrough: WebRTC in 150 Lines of C++.
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();
}
};
A browser to disk, server-side
The browser sends video over WebRTC; the server decodes with FFmpeg and writes to any container the codecs allow. Video depositions, telehealth recording, proctoring; recording that lives on infrastructure you control instead of someone else’s cloud.
av::EncoderOptions opts;
opts.oformat = av::Format{
"MP4", "mp4",
av::VideoCodec{"H.264", "libx264", 1280, 720, 30, "yuv420p"},
av::AudioCodec{"AAC", "aac", 2, 48000, 128000, "fltp"}
};
Any file, real-time, to a browser
MP4 in, WebRTC out, data channel alongside for seek and control. See src/webrtc/samples/file-streamer.
Your own TURN relay
RFC 5766 TURN with channel binding and TCP support. Around 30% of real-world WebRTC connections need relay through symmetric NATs; the canonical fix is to run your own. See src/turn/samples/turnserver.
Capture and encode
FFmpeg’s codec API is powerful and leaks the moment you stop paying attention. icey wraps it in RAII types with custom deleters, so capture, encode, and mux clean up after themselves. VideoCapture and AudioCapture emit decoded packets straight from system devices: V4L2 on Linux, AVFoundation on macOS, DirectShow on Windows. MediaCapture handles file input, with optional looping. Encoders chain into the same PacketStream as processors, and the encoder owns pixel-format conversion through libswscale, PTS tracking, and time-base conversion, then flushes cleanly on close. The whole AV surface adapts to FFmpeg 5, 6, and 7 at compile time, so you build against whatever the distro ships.
Networking on libuv
Every socket, timer, DNS lookup, and child process runs on one libuv event loop. TCP, SSL, and UDP share a common interface with chainable adapters for middleware-style extension. The SSL layer enforces a TLS 1.2 floor with ALPN, SNI, hostname verification, and session caching through OpenSSL 3.x EVP. The HTTP module is a full client and server: WebSocket upgrade (RFC 6455), multipart forms, cookies, and streaming responses, built on llhttp, the same parser Node uses.
Everything is wired with typed signals. Signal<T> is thread-safe and multi-listener, carrying socket events, stream-state changes, and install progress through the same mechanism.
server.Connection += [](http::ServerConnection::Ptr conn) {
conn->Payload += [](auto&, const MutableBuffer& buf) {
// handle incoming data
};
};
Under the budget
The HTTP module sustains 72,000 requests per second on a single core. The WebRTC pipeline carries 1080p60 with zero memory copies between source and sender. The TURN relay swaps 36-byte STUN headers for 4-byte channel headers on the hot path. These numbers come out of building the library around the frame budget rather than around a feature list.
| 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 |
Go’s net/http runs goroutines under a garbage collector; icey runs directly on libuv with zero-copy buffer reuse. The gap in the table is that choice, measured.
Modules
icey is sixteen modules, each self-contained, each compiled only when its dependency is present.
| Module | What it does |
|---|---|
| base | Event loop, signals, packet streams, threading, timers, filesystem, logging |
| crypto | HMAC, SHA-256/512, RSA, X509; OpenSSL 3.x EVP |
| net | TCP, SSL, UDP sockets, DNS, chainable adapters |
| http | Client and server, WebSocket RFC 6455, forms, cookies, streaming, keep-alive |
| json | Serialisation via nlohmann/json |
| av | FFmpeg capture, encode, decode, record (FFmpeg 5/6/7) |
| speech | Audio intelligence primitives for decoded media |
| vision | Video intelligence primitives for 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 for native extensions |
| archo | ZIP extraction with path traversal protection |
| sched | Deferred and periodic scheduling with restore semantics |
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
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failure
Modules compile only when their dependencies are present, so av appears once FFmpeg is installed and stays out of the build when it isn’t. CI runs GCC 12+, Clang 15+, AppleClang 15+, and MSVC 2022, with ASan, TSan, and UBSan builds catching what unit tests miss.
Install
# Homebrew
brew install nilstate/tap/icey
# Arch (AUR)
yay -S icey-server
# Ubuntu / Debian (signed APT repo)
echo "deb https://apt.0state.com stable main" | sudo tee /etc/apt/sources.list.d/0state.list
sudo apt update && sudo apt install icey-server
# Nix
nix build github:nilstate/icey
# CMake FetchContent
FetchContent_Declare(icey
GIT_REPOSITORY https://github.com/nilstate/icey.git
GIT_TAG v2.4.10
)
openSUSE packaging is in progress through Factory, wired to the same https://0state.com/icey/ upstream.
The frame is sixteen milliseconds. Everything else is engineering.
