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 libraries do not respect this; they were written for throughput, or portability, or someone’s product roadmap. icey is the C++ library written for the frame.
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.
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 are not vendor benchmarks; they are what the frame budget looks like when the library is built around it.
| 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 architectural choice is the number.
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.
Install
# Homebrew
brew install nilstate/tap/icey
# Arch (AUR)
yay -S 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
)
The frame is sixteen milliseconds. Everything else is engineering.
