Understand

Architecture

icey-server is not a monolith. It is a thin assembly layer over independent icey library modules. Every piece of the server — HTTP, signalling, media transport, relay, encoding — is a module you can use on its own.

This page explains how the server maps to the library. If you ran icey-server and want to understand what is underneath, start here.

The Stack

Browser ─── HTTP GET /     ──── Static files (web UI)
        ─── HTTP GET /api  ──── REST status endpoints
        ─── WSS /ws        ──── Symple signalling + presence
        ─── WebRTC         ──── Media (H.264 + Opus)
        ─── TURN           ──── NAT traversal relay (port 3478)

Everything on the right side is an icey library module:

icey-server
├── http     → serves UI, REST endpoints, WebSocket upgrade
├── symple   → signalling, peer presence, rooms, call control
├── webrtc   → session negotiation, RTP track send/receive
├── turn     → RFC 5766 relay for NAT traversal
├── av       → FFmpeg capture, H.264/Opus encode/decode, MP4 mux
├── vision   → decoded frame sampling, motion detection events
├── speech   → decoded audio VAD, speech activity events
├── stun     → STUN message parsing (used by TURN)
├── net      → TCP, SSL/TLS, UDP sockets
├── crypto   → OpenSSL hashing, HMAC, certificates
├── json     → configuration, signalling payloads
└── base     → event loop (libuv), signals, PacketStream, logging

The server's main() wires these together. The modules do the work.

Module Map

Tip

Every module above is independently usable. You do not need the server to use the library. icey::http is a standalone HTTP server. icey::turn is a standalone TURN relay. icey::av is a standalone FFmpeg wrapper. The server is one assembly — your application can be a different one.

Data Flow

Everything flows through PacketStream. Plug in a source, chain processors, attach a sink. Borrowed packets stay zero-copy until the first queue or retained adapter; that boundary is explicit in the graph. The pipeline handles backpressure, frame dropping, and teardown so you don't. Nothing runs that you didn't ask for.

┌─────────────────────────────────────────────────────────────────┐
│                        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)

Camera to browser in 150 lines. Browser to file in 130. The pipeline handles the plumbing.

In icey-server, the pipeline takes a different shape per mode:

Stream:  MediaCapture → VideoPacketEncoder → WebRtcTrackSender → browser
                       → AudioPacketEncoder → WebRtcTrackSender → browser

Record:  browser → WebRtcTrackReceiver → VideoDecoder → MultiplexPacketEncoder → MP4

Relay:   browser (source) → WebRtcTrackReceiver → WebRtcTrackSender → browser (viewers)

Decoded branches can feed vision and speech processors without changing the transport path.

The PacketStream concept page explains ownership rules and retention boundaries. The WebRTC session flow explains session negotiation and control.

Why icey

libWebRTC (Google)libdatachannelGStreamericey
Build systemGN/NinjaCMakeMesonCMake
Build timeHoursMinutes30+ minMinutes
Binary size50MB+SmallLargeSmall
SSLBoringSSL (conflicts)OpenSSLOpenSSLOpenSSL
Media codecsBundledNoneGObject pluginsFFmpeg (any codec)
Capture/encodeIncludedNoPlugin pipelinePacketStream pipeline
SignallingNoNoNoSymple (built-in)
TURN serverNoNoNoRFC 5766 (built-in)
LanguageC++C++17C/GObjectC++20

libdatachannel gives you the WebRTC transport pipe. icey gives you the pipe, the water, and the faucet.

HTTP performance

The same runtime model that handles media also handles HTTP. On a single-core micro VM:

ServerReq/secLatency
Raw libuv+llhttp96,0881.04ms
icey72,2091.43ms
Go 1.25 net/http53,8782.31ms
Node.js v2045,5143.56ms

75% of raw libuv throughput with a complete HTTP stack. 34% faster than Go's net/http. All three share the same foundation (libuv for async IO, llhttp for parsing); the difference is pure runtime overhead.

Why This Matters

Most media server projects are either:

  • A library with good internals but no deployable product surface
  • A deployable server where the internals are not designed for reuse

icey is both. The server proves the library works under real conditions. The library means you are not locked into the server's opinions about how to assemble the stack.

If the server does 90% of what you need, run it. If it does 60%, read the module guides and build your own assembly. The modules are the same either way.

Next Steps