Четыре камеры, один SSD и желание не покупать коробку за двадцать тысяч. Оказалось, что 90% задачи закрывает один флаг ffmpeg.
Готовых NVR полно — Frigate, Shinobi, ZoneMinder. Все они хорошие и все хотят от меня либо детекции с нейросетью, либо PHP, либо и того и другого. Мне нужно было принципиально меньше: писать четыре потока на диск, хранить неделю, уметь достать кусок за вчерашний вечер. Всё.
Первое, что нужно понять про запись RTSP: перекодировать не надо. Камера уже отдаёт H.264, аппаратно сжатый её собственным чипом. Всё, что требуется — снять пакеты с сети и разложить по файлам.
ffmpeg -rtsp_transport tcp -i rtsp://cam1/stream \
-c copy -f segment -segment_time 300 \
-reset_timestamps 1 -strftime 1 \
/srv/nvr/cam1/%Y%m%d-%H%M%S.mkv
Разберём по частям, потому что каждый флаг здесь выстрадан:
-c copy — то самое. Ноль декодирования, ноль энкодинга. CPU на поток: около 2%, и это в основном сеть.-rtsp_transport tcp — по умолчанию ffmpeg берёт UDP, и на wifi-камере вы получите битые кадры и missed frames в логе. TCP медленнее на микросекунды и надёжнее на порядок.-f segment -segment_time 300 — режем на пятиминутки. Не часовые: файл, который пишется, всегда рискует остаться недописанным.-reset_timestamps 1 — без него у каждого следующего сегмента таймстемпы продолжают расти от начала записи, и плеер показывает файл длиной «5 минут, начиная с 14 часов». Раздражает.-strftime 1 — имя файла из времени. Дальше вся логика ретеншена строится на именах, и это оказалось на удивление удобно.MKV, а не MP4 — осознанно. MP4 хранит индекс (moov) в конце файла: если процесс умрёт посреди сегмента, файл не откроется вообще. Matroska пишется потоково, и оборванный файл проигрывается до места обрыва. Для записи, которая по определению может прерваться, это решающий аргумент.
«Кольцевой буфер» звучит солидно, на деле это find по крону. Неделя хранения:
find /srv/nvr -name '*.mkv' -mmin +10080 -delete
Работает, но плохо: диск переполнится раньше, если камера вдруг начнёт отдавать битрейт выше расчётного (а она начнёт — ночью, когда шум с матрицы убивает компрессию). Поэтому ретеншен не по времени, а по месту:
#!/bin/sh
# держим занятость раздела ниже 85%, удаляя самые старые сегменты
TARGET=85
while [ "$(df --output=pcent /srv/nvr | tr -dc '0-9')" -gt "$TARGET" ]; do
OLDEST=$(find /srv/nvr -name '*.mkv' -printf '%T@ %p\n' | sort -n | head -1 | cut -d' ' -f2-)
[ -n "$OLDEST" ] || break
rm -f "$OLDEST"
done
Раз в пять минут по крону. Тупо, предсказуемо, никогда не переполняет диск. Глубина хранения плавает от пяти дней до девяти — и это честнее, чем гарантировать неделю и однажды упасть.
Опасение было такое: непрерывная запись убьёт SSD. Посчитаем. Четыре камеры по 4 Мбит/с — это 2 МБ/с, около 170 ГБ в сутки, порядка 60 ТБ в год. Для дешёвого потребительского SSD на 1 ТБ с ресурсом ~600 TBW это десять лет. Спокойно.
Важнее другое: запись строго последовательная, крупными блоками, без random write. Это самый щадящий из возможных паттернов. А вот mdraid1 из двух дисков я всё-таки собрал — не ради ресурса, а потому что дешёвый SSD умирает не по износу, а внезапно.
Что действительно стоит сделать — вынести систему на отдельный носитель. Логи, база и swap на том же разделе, куда льётся видео, — вот это уже random write, и вот это его и убьёт.
ffmpeg при обрыве RTSP просто завершается. Нужен присмотр, и systemd для этого достаточно:
[Service]
ExecStart=/usr/local/bin/record-cam1.sh
Restart=always
RestartSec=5
StartLimitIntervalSec=0
StartLimitIntervalSec=0 здесь принципиально: без него systemd после пяти рестартов подряд решит, что юнит сломан, и опустит руки. А камера может не отвечать полчаса — питание моргнуло, роутер перезагрузился. Нам нужен именно бесконечный ретрай.
Плюс таймауты, чтобы ffmpeg не висел вечно на мёртвом сокете:
-timeout 5000000 -reconnect 1 -reconnect_streamed 1
Ради этого всё и затевалось. Имена файлов — это время, значит:
# всё, что писалось вчера с 21:00 до 22:00
ls /srv/nvr/cam1/20260410-21*.mkv
Склеить в один файл, если нужно отдать:
printf "file '%s'\n" /srv/nvr/cam1/20260410-21*.mkv > /tmp/list.txt
ffmpeg -f concat -safe 0 -i /tmp/list.txt -c copy /tmp/evening.mkv
Снова -c copy, снова мгновенно.
Весь «NVR» — это systemd-юнит на камеру, один скрипт ретеншена в кроне и папка на диске. Сотня строк вместе с пробелами. CPU под нагрузкой — 8% на четыре потока, память — десятки мегабайт.
Чего тут нет и не будет: детекции движения, приложения, уведомлений. Если понадобится — рядом встанет Frigate и будет смотреть тот же поток, не мешая записи. Но пока не понадобилось: за год я лазил в архив четыре раза, и каждый раз хватало ls с маской по времени.