dobrovolskiy.com
RU
← Notes2026-08-30 · by Dmitriy Dobrovolskiy

An ultra-light IP camera viewer and recorder in C: dozens of RTSP streams on a slow PC

Case study of Ultralight IP CAM Viewer: a 17k-line C11 application on FFmpeg + SDL2 that shows a grid of RTSP cameras, records hourly MPEG-TS, plays back a timeline, drives ONVIF PTZ and runs on very weak hardware.

Ultralight IP CAM Viewer is a Windows application in plain C11 on FFmpeg and SDL2: a matrix of RTSP cameras, continuous recording to hourly MPEG-TS segments, a scrubbable DVR timeline, ONVIF PTZ from the mouse and a C plugin ABI. One worker thread per camera and a statically linked minimal FFmpeg keep it light enough to run far more cameras per machine than typical NVR software — it was built for a real 15-camera warehouse site.

Why write yet another camera viewer

A client runs a warehouse and workshop with fifteen IP cameras and one old office PC to watch them on. Every NVR package he tried either ate the machine alive or stopped at a handful of streams. The ask was simple: show all cameras at once, keep an archive, let me scroll back in time, do not crash, do not need a new computer.

The answer was to write it in C with two dependencies I control: FFmpeg for demuxing and decoding, SDL2 for rendering, audio and threads. Everything else — text rendering, the ONVIF client, the UI — is hand-written. The result is about 17 000 lines of C and a portable distribution of an exe plus two DLLs.

Threading discipline is the whole trick

Each camera gets one worker thread that opens the RTSP session, decodes and hands frames to the renderer. Per-camera locks, nothing shared between cameras. Decode threads never touch the disk; the recorder for each camera is fully asynchronous and never blocks decoding. The UI thread only peeks at the latest frame. An RTSP open-throttle semaphore avoids the handshake bursts that make NVRs refuse connections when all cameras start at once.

The grid switches between preview and main streams per tile, pre-buffers on warm start and shows cached thumbnails immediately, so the application is usable before the first key frame arrives.

Recording and the time machine

Recording writes 1-hour MPEG-TS segments per camera into recordings/<cam>/<dd.MM.yyyy>/<start__end>.ts; the file is renamed with its exact span on close. An archive trimmer enforces a MaxArchiveGB cap.

The DVR mode (F9) scans the recordings into a coverage index and draws a timeline strip at the bottom that you can pan, zoom and seek in; playback runs at 1× and is drawn over the live tile, so you never lose the context of which camera you are looking at.

ONVIF without gSOAP

PTZ heads are driven from the mouse. The ONVIF client — device probe, media profile discovery, ContinuousMove/Stop — is a hand-rolled SOAP 1.2 implementation with WS-Security UsernameToken digest authentication. It lives in its own thread with a one-slot mailbox (the newest command wins) and a safety watchdog that stops the head if commands stop arriving.

A plugin ABI, and a face counter with zero dependencies

Analysis is not compiled in. Plugins are C libraries with a versioned ABI (magic number, ABI version, size-checked structs); they receive YUV420P frames and S16 audio and return overlay rectangles. The shipped Face Counter plugin detects faces with a pico cascade, re-identifies them with LBP histograms and counts unique visitors per day — no OpenCV, no external libraries, installed from its own MSI.

Robustness and packaging

SEH crash logging, a UI-hang watchdog thread, a startup trace log and automatic reconnect cover the failure modes seen on site. Settings live in settings.ini written by a background writer; camera manager, settings and plugin manager are in-app overlays (F1/F2/F10) with English and Russian text. The build is CMake + Ninja under MSYS2 UCRT64, with a script that produces the minimal static FFmpeg; installers are a WiX MSI and an MSIX.

What I would do differently

Start the plugin ABI earlier. It was added after the face-counter request, and retrofitting it meant touching the frame path in every worker. Everything else — the single-worker-per-camera rule, no disk access in decode threads — I would keep exactly as it is; that is why the old office PC copes.

FAQ

How many cameras can it handle?

The shipped configuration runs 15 cameras on a slow office PC. The limit is decode throughput, which scales with cores; because there is no framework overhead and recording never blocks decoding, one machine handles noticeably more streams than typical NVR software.

Does it work with Hikvision / Dahua / TP-Link cameras?

It works with anything that speaks RTSP (H.264 video plus audio). The client site mixes Dahua/Hikvision-style `/user=…_channel=1` URLs and TP-Link `/stream1` URLs. PTZ needs ONVIF.

Can you adapt it for my site or add analytics?

Yes. Camera-specific quirks, a custom overlay or a new analysis plugin are typical hour-long sessions; the plugin ABI is designed for exactly that.

Want something like this?

I build it live on a Zoom call, you watch the screen, the timer stops when you say stop. First 15 minutes are free.

$50 / hourBook an hour

More projects

Ultralight IP CAM ViewerUltra-light multi-camera RTSP viewer and recorder: dozens of cameras on one slow PC or server