SIRINVIDEO CCTV Документация администратора и пользователя На сайт 2026.08

node.conf — все директивы

Полный справочник конфигурации ноды: что можно править руками, что принадлежит Plane, и почему опечатка останавливает сервис.

Вся конфигурация ноды — один файл /etc/hastreamer-cctv-node/node.conf. Его читает бинарь /usr/local/bin/hastreamer-cctv при старте и повторно — при перезагрузке конфигурации (systemctl reload, то есть сигнал SIGHUP). Юнит systemd называется hastreamer-cctv-node.

Файл состоит из двух половин. Верхняя — системная: порты, TLS, сертификаты, политика доступа, тюнинг. Она ваша, правится руками и сохраняется навсегда. Нижняя — область между маркерами MANAGED BY cctv-plane: камеры, диски архива, настройка авторизации просмотра. Она принадлежит Plane и восстанавливается при каждом старте.

Здесь описаны все 112 форм директив, которые понимает разборщик: 38 директив верхнего уровня и 74 вложенных. Директивы, которой нет в этом справочнике, не существует — нода такой файл не примет.

Модель владения файлом#

Область, которой владеет Plane#

Plane владеет фрагментом файла, ограниченным двумя строками-маркерами:

# >>> MANAGED BY cctv-plane — edits will be overwritten >>>
storage archive /srv/cctv/archive {  }
auth_backend {  }
stream cam/<UUID>/main {  }
# <<< MANAGED BY cctv-plane <<<

Маркеры сравниваются побайтово (тире в первом — длинное, «—»), пара должна встречаться ровно один раз и в правильном порядке. Половина пары или второй экземпляр — жёсткая ошибка: malformed cctv-plane managed region (need exactly one ordered marker pair).

Последняя одобренная Plane копия этой области лежит отдельным файлом — /etc/hastreamer-cctv-node/cctv-node/managed.conf. При каждом старте нода собирает конфигурацию заново: ваша системная часть плюс этот снимок, — и атомарно записывает результат обратно в node.conf. Поэтому нода переживает недоступность Plane со всем набором камер целиком; поэтому же правки внутри маркеров не выживают.

Камера, дописанная руками, останавливает ноду

Исходов два, и второй хуже.

  • Правка внутри маркеров — молча заменяется снимком при следующем старте или перезагрузке конфигурации. Ни ошибки, ни предупреждения: вашего текста просто больше нет.
  • Блок stream, auth_backend, principal или public_paths вне маркеров — нода отказывается принять файл: stream may appear only in the plane-managed region (несколько имён перечисляются через , ). При старте это hastreamer-cctv: managed config rejected: … и выход с кодом 1. systemd поднимает процесс заново каждые 2 секунды — нода уходит в цикл перезапусков и не отдаёт ни одного кадра.

Восстановление:

sudo journalctl -u hastreamer-cctv-node -n 50 --no-pager   # причина отказа — в первых строках

# дальше одно из двух: вернуть резервную копию…
sudo cp /root/node.conf.bak /etc/hastreamer-cctv-node/node.conf
# …либо открыть файл и удалить лишний блок
sudo -e /etc/hastreamer-cctv-node/node.conf

sudo systemctl restart hastreamer-cctv-node
systemctl is-active hastreamer-cctv-node                    # active

Камеры добавляйте в Plane — Добавление камер, диски — Хранилища DVR.

Три класса владения#

Класс Директивы Что произойдёт при ручной правке
Владеет Plane stream, storage, auth_backend Возвращаются из снимка managed.conf при следующем старте или перезагрузке конфигурации. Менять их нужно в интерфейсе или API Plane
Владеет Plane, только внутри области principal, public_paths Plane их сегодня не пишет, а вне маркеров они запрещены — на управляемой ноде пользоваться ими нельзя
Выдал Plane, но лежит снаружи cctv Формально ваша половина файла, фактически — удостоверение ноды: любая правка ломает спаривание, см. cctv { }
Ваши (node-local) всё, что не перечислено выше: слушатели, tls, certificate, acme_directory, acme_contact, trusted_proxies, allow_origins, auth, session_keys, все директивы тюнинга, cluster, template Сохраняются при перезапусках, перезагрузках конфигурации и любых обновлениях от Plane

Есть ещё два ограничения на файл целиком: он должен быть корректным UTF-8 и не длиннее 128 МиБ, иначе managed config is not UTF-8 или managed config exceeds 128 MiB cap. Ведущая метка порядка байтов (BOM), которую дописывают некоторые редакторы под Windows, снимается разборщиком и файл не ломает.

session_keys — ваш блок, вопреки ожиданиям

Plane пишет в область только хранилища, auth_backend и потоки. Настройка билетов session_keys { } в область не попадает никогда, поэтому сроки жизни билетов вы правите руками и они переживают любую синхронизацию с Plane.

Грамматика файла#

Директивы, блоки и точка с запятой#

Инструкция — это имя [аргументы] ; либо имя [аргументы] { вложенные инструкции }.

http 9443;                       # одно значение, обязательная точка с запятой
trusted_proxies 10.0.0.0/8 ::1;  # несколько значений в одной строке
acme;                            # флаг без значения — означает «включено»

tls {                            # блок: вместо ';' — фигурные скобки
    cert  /etc/ssl/node.crt;
    key   /etc/ssl/node.key;
}

storage archive /srv/cctv {      # именованный блок: имя, аргументы и тело
    identity 3f2a9c14-1c0e-4f7b-9d21-8a6f0b4e5c77;
}

Точка с запятой обязательна у каждой директивы со значением; без неё — line 12: directive "http" not terminated with ';'. Блок точкой с запятой не закрывается: писать tls { … }; не нужно (одиночная лишняя ; игнорируется, но полагаться на это не стоит). Незакрытая скобка даёт unexpected end of file inside a block (missing '}'), лишняя — line N: unmatched '}'.

Тело { } обязательно у cctv, tls, auth, auth_backend, session_keys, cluster, principal, certificate, stream и template. У storage и vod тело необязательно: storage fast /srv/dvr; — корректное однострочное объявление со значениями по умолчанию.

Комментарии#

# начинает комментарий до конца строки. Блочных комментариев нет. # внутри кавычек — обычный символ, а не комментарий. Ваши комментарии сохраняются побайтово: при перезаписи файла из Plane или через API заменяется только управляемая область.

Кавычки и экранирование#

Слово без кавычек кончается на пробеле, ;, {, } или #. Если значение содержит любой из этих символов — возьмите его в двойные кавычки:

plane_url "https://cctv.example.com:8443";
input "rtsp://svc:pa$$;word@10.20.0.11:554/Streaming/Channels/101";
Последовательность внутри кавычек Результат
\" символ "
\\ символ \
любая другая \x сохраняется как есть, вместе с обратной косой чертой
перенос строки остаётся переносом в значении — так в файле хранится PEM-ключ

Незакрытая кавычка — line N: unterminated quote.

Как значение получает тип#

Разборщик определяет тип каждого значения сам, по порядку: целое число → дробное число → on/true как истина и off/false как ложь → в остальных случаях строка.

Из этого следует, что значение, похожее на число, станет числом. Там, где это было бы вредно — URL, адреса почты, секреты, ключи S3, acme_directory, acme_contact, поля auth_backend, storage s3 и principal, — разборщик принудительно оставляет строку, поэтому секрет из одних цифр не испортится. Кавычки в таких местах не обязательны, но всегда безопасны.

Флаги без значения#

Каждая логическая директива понимает обе формы — это общее правило, а не особенность конкретной директивы:

on_demand;        # флаг без значения → включено
on_demand on;     # то же самое явно
on_demand off;    # выключено

Так пишутся: acmecertificate), admin_ip_bindauth), bind_ipsession_keys) и флаги потока http_ts, hls_ts, audio_only_playlist, disabled, on_demand.

Повторы и порядок#

  • Порядок директив свободный — разборщик собирает их в набор, а не читает как сценарий.
  • Механизма include нет. Файл ровно один. Снимок cctv-node/managed.conf — не include.
  • Повторённая скалярная директива молча берёт последнее значение: http 8080; и ниже http 9443; дают 9443 без предупреждения. Держите по одной строке на директиву.
  • Три директивы накапливаются, а не перезаписываются: trusted_proxies, allow_origins, public_paths. Очистить накопленный список можно только удалением строк.
  • Повторённые input внутри одного stream образуют цепочку резервирования по порядку; повторённые permission внутри principal — список его прав.
  • Повторённые блоки certificate, storage, stream, principal, template, vod добавляют записи; совпадение имён — ошибка валидации вида duplicate certificate name "x".

Неизвестное имя — это ошибка#

Неизвестные директивы отвергаются на разборе, с номером строки, на любом уровне вложенности:

line 7: unknown directive "cors"
line 12: unknown stream directive "frobnicate"
line 4: unknown tls directive "bogus"
line 9: unknown cctv directive "plane_key"

Формат файла выбирается по расширению: .toml — TOML, .json — JSON, любое другое (в том числе .conf) — описанная здесь текстовая грамматика. Не называйте конфигурацию ноды node.json.

Единицы измерения#

Сроки и объёмы хранения архива записываются суффиксом сразу после числа, без пробела. Суффиксы времени и размера различаются регистром и длиной, и здесь легко ошибиться на два порядка.

Суффикс времени Значение Пример
s секунды 90s
m минуты 30m — тридцать минут
h часы 12h
d сутки 14d
w недели 8w
mo месяцы 6mo — полгода
y годы 1y
Суффикс размера Значение Пример
K КиБ, 1024 байта 512K
M МиБ 900M
G ГиБ 500G
T ТиБ 2T

Суффиксы размера пишутся только заглавной буквой: 500g не размер и не время — такая строка не будет принята. Суффикс времени m — это минуты, месяцы обозначаются двумя буквами, mo.

dvr archive 30m;    # НЕВЕРНО, если имелись в виду месяцы: это 30 МИНУТ архива
dvr archive 30mo;   # верно: 30 месяцев
dvr archive 500g;   # НЕВЕРНО: суффикс размера только заглавной буквой
dvr archive 500G;   # верно: 500 ГиБ

Опечатка в суффиксе опаснее любой другой в этом файле: 30m — совершенно корректная строка, нода спокойно запустится и через полчаса начнёт удалять записи. Неверный суффикс даёт stream "x": bad retention_time "…" (s/m/h/d/w/mo/y) или stream "x": bad retention_size "…" (K/M/G/T), а верный, но не тот — молчание и потерю архива. Строку dvr на управляемой ноде формирует Plane; проверяйте её глазами после каждого изменения срока хранения.

Что происходит, если в файле ошибка#

Опечатка не «деградирует» — нода не стартует

Фатальны при загрузке все четыре класса ошибок:

  • неизвестное имя директивы — line 7: unknown directive "cors";
  • недопустимое значение перечисления — source_transport must be "tcp", "udp", or "udp-then-tcp" (got "tpc");
  • значение вне диапазона — udp_ingest_shards 32 out of range (1..=16);
  • директива протокола, которого нет в этой сборке, — например webrtc 8000; даёт `webrtc_udp_port` is set but WebRTC is not in this build (feature `egress-webrtc`).

Ни один из них не превращается в «значение по умолчанию» и не пишется в журнал как предупреждение. Всегда держите резервную копию файла до правки и проверяйте изменение перезагрузкой конфигурации, а не перезапуском.

Когда Поведение
При старте Ошибка печатается в журнал (hastreamer: config error: …), процесс завершается с кодом 1. systemd (Restart=on-failure, RestartSec=2s) поднимает его снова — получается цикл перезапусков
При systemctl reload (SIGHUP) config reload REJECTED: … (running config unchanged). Работающая нода продолжает отдавать видео со старой конфигурацией — это безопасный способ проверить правку
Несколько ошибок сразу Валидатор собирает их все и печатает через ; — файл чинится за один проход, а не по одной ошибке за перезапуск

Найти строку с ошибкой:

sudo systemctl reload hastreamer-cctv-node
sudo journalctl -u hastreamer-cctv-node -n 30 --no-pager
config reload REJECTED: line 42: unknown directive "cors" (running config unchanged)

Номер строки — от начала файла, включая комментарии и управляемую область.

Горячее применение и перезапуск#

В таблицах этого справочника столбец «Применение» означает:

  • горячее — изменение действует после systemctl reload, без перезапуска и без разрыва соединений;
  • рестарт — новое значение записывается в файл, но работающий процесс живёт со старым до systemctl restart;
  • рестарт потока (или рестарт потоков) — перезапускается только затронутый поток: его зрители переподключаются, остальные потоки изменения не замечают;
  • рабочая нагрузка — набор объектов сверяется поштучно, сервис не перезапускается; что именно будет перезапущено, зависит от того, какие объекты изменились.

Требуют перезапуска сервиса — 23 директивы#

http · https · rtsp · rtsps · rtmp · rtmps · webrtc (порт) · webrtc public_ip= · cores · low_latency · rtsp_frame_source · source_transport · rtp_max_payload · max_sessions · dvr_cleanup_secs · media_timeout · on_demand_grace · udp_ingest_rcvbuf · udp_ingest_shards · udp_ingest_pool_slots · udp_pull_rate · acme_directory · acme_contact

Изменение такой директивы помечается как требующее перезапуска: в ответе API и в интерфейсе Plane появляется строка вида HLS/API listener (http_port 8080 → 9443). Это уведомление, а не применение — работающий процесс продолжает слушать старый порт.

Применяются горячо#

trusted_proxies · allow_origins · auth { } · tls { } · certificate { } · auth_backend { } · session_keys { } · principal { } · public_paths · log_level

Рабочая нагрузка — сверяется по каждому объекту#

stream · template · storage · cluster · vod

Поток перезапускается только тогда, когда изменился его собственный отпечаток — весь блок stream, кроме labels. Практические следствия:

  • правка input, dvr, segment_seconds, on_demand — перезапускается этот один поток, остальные сохраняют аптайм и сессии зрителей;
  • правка только labels — не перезапускается ничего, новые права доступа действуют сразу;
  • правка path, segment_secs или identity у хранилища — перезапускаются все потоки, которые в него пишут;
  • правка адреса или ключей учётной записи S3 — перезапускаются все потоки, которые в неё копируют.
Блок cctv { } в таблицах отсутствует, но требует перезапуска

Объект доверия к Plane строится один раз при старте, а в таблицу рестартовых директив cctv не входит — перезагрузка конфигурации о его изменении не предупредит. Считайте любую правку cctv рестартовой. Правильный ответ, впрочем, — не править его руками вообще.

Часть 1. Что редактируется вручную#

Всё в этом разделе — ваше. Пишите это выше открывающего маркера управляемой области, в любом порядке.

Слушатели: http, https, rtsp, rtsps#

http  9443;      # HLS-fMP4, MSE-WS, превью, REST API, плеер, ответчик ACME HTTP-01
https 8443;      # то же самое поверх TLS
rtsp  8554;      # RTSP-раздача: rtsp://node.example.com:8554/<путь-потока>
rtsps 8555;      # RTSP поверх TLS, использует тот же сертификат
Параметр Тип и допустимые значения По умолчанию Применение
http номер порта 1–65535; ноль запрещён 8080; в стартовой конфигурации от Plane — 80, 9080 или порт из http://-адреса управления рестарт
https номер порта 1–65535, 0 — выключено 0; в стартовой конфигурации от Plane — порт из https://-адреса управления рестарт
rtsp номер порта 1–65535; ноль запрещён 8554 рестарт
rtsps номер порта 1–65535, 0 — выключено 0 рестарт
rtmp номер порта, 0 — выключено; в этой сборке допустим только 0 0 рестарт
rtmps номер порта, 0 — выключено; в этой сборке допустим только 0 0 рестарт
webrtc webrtc <порт> [public_ip=<адрес>], 0 — выключено; в этой сборке допустим только 0 0, public_ip пустой рестарт
Что пишет сюда Plane

Слушатели в стартовой конфигурации выводятся из схемы эффективного адреса управления (control_url, а если он пуст — public_url), и вручную их согласовывать не нужно:

Эффективный адрес управления Что в файле
https://node.example.com http 80; и https 443; плюс блок tls { self_signed_names node.example.com; }
https://cctv.example.com:9443 http 9080; и https 9443; плюс тот же блок tls { } (нода на одном хосте с Plane)
http://node.example.com:8090 только http 8090;, блока tls { } нет (TLS терминирует прокси впереди)

Поэтому нода поднимается по HTTPS с первого запуска — самоподписанной парой на своё настоящее имя. Прежней последовательности «сначала обычный http, потом руками перевести на https» больше нет.

Слушатели http и rtsp обязательны и не выключаются: http_port and rtsp_port must be non-zero. Все порты, которые вы задали ненулевыми, должны различаться — http_port and rtsp_port must differ (both 8080), https and rtsps both use port 8443 — listener ports must be distinct. Номер порта — 16-битное число: значение больше 65535 файл не пройдёт. webrtc без аргумента даёт line N: 'webrtc' needs a UDP port, а с ненулевым портом — отказ по отсутствующей возможности сборки (см. Чего в этом файле нет).

Директивы api не существует

REST API ноды, встроенный плеер, HLS-fMP4, MSE-over-WebSocket, превью и ответчик ACME HTTP-01 работают на слушателе http (и на https, если он включён). Отдельного порта или отдельной директивы для API нет и не будет — не ищите. Управляющие запросы к этому API принимаются только с подписью ключа Plane, локального логина у ноды нет.

Сменили порт или схему — поменяйте адреса ноды в Plane

Plane обращается к ноде по её адресу управления. Если сменить порт или включить/выключить https на ноде и не обновить адреса в Plane, нода останется живой, но перейдёт в состояние offline и перестанет получать конфигурацию. Меняйте оба значения в одно окно обслуживания: в админке — Ноды → нода → Сеть и TLS → Адреса ноды → Изменить, через API — PATCH /api/v1/nodes/<UUID> (см. Ноды и хранилища).

Отдельно запомните: ноду, которая терминирует TLS, нельзя опрашивать по http:// — она перенаправит запрос на HTTPS, а при перенаправлении теряется заголовок авторизации, и Plane получит 401.

tls { } — сертификат по умолчанию#

Блок задаёт удостоверение, которым нода отвечает, когда клиент не прислал имя домена (SNI) или когда ни один блок certificate под это имя не подошёл.

tls {
    cert      node-snapshot.cert.pem;    # путь относительно хранилища TLS
    key       node-snapshot.key.pem;
    sync_from /var/lib/hastreamer-cctv-plane/store/identity.pem;
    self_signed_names node.example.com localhost;
}
Параметр Тип и допустимые значения По умолчанию Применение
cert путь к PEM-цепочке: абсолютный или относительно хранилища TLS пусто горячее
key путь к PEM-ключу, то же правило пусто горячее
sync_from путь к исходному PEM, который копируется в cert и key пусто горячее
self_signed_names список доменных имён localhost горячее

Хранилище TLS — это tls-state внутри рабочего каталога сервиса, то есть /var/lib/hastreamer-cctv-node/tls-state. Относительные пути отсчитываются от него.

Если cert и key пусты, нода один раз выпускает самоподписанную пару на имена из self_signed_names и сохраняет её в <хранилище TLS>/self-signed-cert.pem и self-signed-key.pem. Отпечаток не меняется при перезапусках — на него можно закрепиться в клиенте.

sync_from нужен там, где Plane и нода стоят на одном сервере: нода следит за исходным файлом, проверяет его, атомарно переименовывает в свои cert и key и на лету подменяет сертификат в резолвере — без перезапуска и без отдельного юнита systemd. Отсюда правило валидации: tls: `sync_from` requires `cert` and `key` (the node-owned snapshot paths the source is copied into). Никогда не указывайте в cert живой файл Plane напрямую: в момент продления он перезаписывается, и нода прочитает его наполовину.

certificate «имя» { } — сертификаты по доменам (SNI)#

Именованный блок — один сертификат на один или несколько доменов. Их может быть сколько угодно; выбор происходит по имени домена из запроса клиента.

certificate node-public {                       # сертификат, загруженный файлами
    hosts node.example.com *.cam.example.com;
    cert  certs/node-public/fullchain.pem;
    key   certs/node-public/privkey.pem;
    ca    certs/node-public/chain.pem;
}

certificate node-auto {                         # сертификат, выпускаемый автоматически
    hosts node.example.com;
    acme;
}
Параметр Тип и допустимые значения По умолчанию Применение
hosts список имён: точное имя или маска *.суффикс в одну метку пусто горячее
cert путь к PEM-цепочке, абсолютный или относительно хранилища TLS пусто горячее
key путь к PEM-ключу пусто горячее
ca путь к промежуточным сертификатам, необязателен пусто горячее
acme флаг: acme;, acme on;, acme off; выключено горячее

Хотя бы одно имя обязательно, иначе блок никогда не будет выбран: certificate "x": needs at least one hosts entry (else it is never selected). Маска — ровно одна метка: *.cam.example.com допустимо, **.example.com — нет (bad wildcard host "**.x" (use *.suffix, one label)). Имена блоков должны быть уникальными (duplicate certificate name "x") и непустыми (a certificate needs a non-empty name).

Без acme пути обязательны: certificate "x": `cert` and `key` paths are required (or set `acme` to auto-issue). С acme они, наоборот, запрещены: certificate "x": `acme` and explicit `cert`/`key` are mutually exclusive.

ACME — автоматический выпуск сертификатов#

Параметр Тип и допустимые значения По умолчанию Применение
acme_directory URL каталога центра сертификации https://acme-v02.api.letsencrypt.org/directory рестарт
acme_contact адрес электронной почты для уведомлений пусто рестарт
acme_directory https://acme-staging-v02.api.letsencrypt.org/directory;   # тестовый каталог
acme_contact ops@example.com;

Чтобы сертификат действительно выпустился, нужны четыре условия — по порядку:

  1. Включён хотя бы один TLS-слушательhttps или rtsps с ненулевым портом. Без него выпускать сертификат некуда, и задача ACME вообще не запускается: в журнале не будет ни одной строки про ACME.
  2. Есть блок certificate <имя> { … acme; } с конкретными именами доменов. Проверки по DNS нет, поэтому маска *.example.com невозможна: certificate "x": ACME/HTTP-01 cannot issue the wildcard "*.y" — give a concrete hostname.
  3. Центр сертификации должен достучаться до http://<домен>/.well-known/acme-challenge/<токен> по порту 80. Нода отвечает на этот путь открытым текстом на своём слушателе http, до всякой авторизации и маршрутизации.
  4. Разрешён исходящий HTTPS до адреса из acme_directory.
Порт 80 нужен и после установки

Центр сертификации всегда стучится на порт 80 — этот номер нельзя изменить настройкой. Если у ноды http 9443, проверка не дойдёт и сертификат не выпустится никогда. Есть два решения: поставить http 80; (нода тогда раздаёт видео и API на 80-м порту) либо пробросить внешний порт 80 на порт слушателя правилом DNAT на межсетевом экране. Порт нужен не только при первом выпуске, но и при каждом продлении — не закрывайте его после установки.

Как это работает дальше:

  • Задача на нулевом ядре просыпается раз в 60 секунд; первый проход отложен, чтобы слушатели успели подняться до того, как центр сертификации начнёт проверку.
  • Каждый проход читает действующую конфигурацию, поэтому блок certificate { acme; }, добавленный через Plane или API, подхватывается в течение минуты — без перезапуска.
  • Продление начинается за 30 суток до конца срока действия: у неудачного продления есть недели на повторные попытки, прежде чем живой сертификат истечёт.
  • После неудачи — выдержка с удвоением: 15 минут, затем 30, 60 и так далее, но не больше 6 часов. Так центр сертификации не упирается в лимиты запросов. Причина последней неудачи видна в API и в бейдже сертификата в интерфейсе.
  • Выпущенное складывается в <хранилище TLS>/certs/<имя>/acme.cert.pem и acme.key.pem, рядом acme.meta.json. Ключ учётной записи ACME сохраняется в <хранилище TLS>/acme/account.key.pem при первом выпуске, поэтому учётная запись переживает перезапуски.
  • Свежая пара подставляется в резолвер SNI на лету: перезапуска нет, зрители не отваливаются. Если пара на диске повреждена, домен обслуживает сертификат по умолчанию, а пара выпускается заново на следующем проходе.

Пока настраиваете новый домен, используйте тестовый каталог (https://acme-staging-v02.api.letsencrypt.org/directory): сертификаты будут недоверенными, зато лимиты мягкие. Затем переключитесь на боевой каталог и перезапустите сервис — директива рестартовая.

trusted_proxies — кому верить в заголовках#

trusted_proxies 10.0.0.0/8 192.168.0.0/16 ::1;
Параметр Тип и допустимые значения По умолчанию Применение
trusted_proxies список адресов и подсетей CIDR, накапливается при повторении пусто горячее

Заголовкам X-Forwarded-For, Forwarded и X-Real-IP нода верит только тогда, когда непосредственный собеседник по TCP входит в этот список. Пустой список (значение по умолчанию) означает, что заголовкам не верят никогда, — прямой клиент не сможет подделать свой адрес в журнале и в правилах доступа. Ошибочная подсеть отвергается тем же разборщиком, который применяет живую политику, поэтому «записалось, но не применилось» тут невозможно.

allow_origins — список разрешённых источников CORS#

allow_origins https://cctv.example.com https://ops.example.com;
Параметр Тип и допустимые значения По умолчанию Применение
allow_origins список источников для заголовка Access-Control-Allow-Origin, накапливается * — любой источник горячее

По умолчанию видео и API доступны браузерным скриптам с любой страницы. В рабочей установке перечислите здесь адреса своих интерфейсов. Ограничение хотлинка страницами конкретных сайтов — это другая настройка, allowed_referers у потока.

auth { } — общий секрет ноды#

auth {
    session_secret "ОБЩИЙ-СЕКРЕТ-ФЛОТА";
}
Параметр Тип и допустимые значения По умолчанию Применение
session_secret строка, непустая не задан — случайный секрет на каждый запуск горячее
admin_ip_bind флаг: admin_ip_bind; или admin_ip_bind off; выключено горячее
admin_password запрещён на CCTV-ноде не задан горячее

session_secret на ноде — это не пароль администратора. Это детерминированный корень, из которого выводится ключ подписи билетов медиасессий, когда session_keys { secret } не задан. Что это даёт: билет, выданный одной нодой, проходит проверку на другой после перенаправления зрителя, а сами билеты переживают перезапуск сервиса. Поэтому значение имеет смысл делать одинаковым на всех нодах установки. Пустая строка отвергается: auth.session_secret must not be empty (omit it for an auto-generated per-boot secret).

admin_password на CCTV-ноде запрещён — управление идёт только из Plane: auth.admin_password is not allowed on a cctv-node — it has no admin login; management is via the plane. admin_ip_bind привязывает выданный админ-токен к адресу входа; админского входа у ноды нет, поэтому директива здесь ни на что не влияет.

session_keys { } — билеты медиасессий#

Билет — короткоживущий ключ, с которым плеер забирает сегменты видео после проверки прав. Пока включена авторизация просмотра, билеты обязательны; этот блок только настраивает их.

session_keys {
    ttl 3600;
    vod_session_ttl 86400;
    bind_ip off;
    secret "ОТДЕЛЬНЫЙ-СЕКРЕТ-ПОДПИСИ";
}
Параметр Тип и допустимые значения По умолчанию Применение
ttl секунды, целое 3600 горячее
vod_session_ttl секунды, от 300 до 604800 86400 горячее
bind_ip флаг: bind_ip; или bind_ip off; выключено горячее
secret строка пусто — ключ выводится из auth.session_secret горячее

ttl — срок жизни билета живого просмотра; он продлевается сам, пока плеер опрашивает плейлист. vod_session_ttl ограничивает билет продолжения для конечных записей и жёстко проверяется: session_keys.vod_session_ttl must be 300..=604800 seconds; got N.

bind_ip привязывает билет к адресу зрителя. Включайте, если зрители сидят в фиксированной сети; выключайте, если они переходят между мобильной сетью и Wi-Fi или сидят за пулом NAT — иначе просмотр будет обрываться при каждой смене адреса.

Смена secret немедленно обесценивает все выданные билеты: зрителям придётся переоткрыть поток. Устаревшая директива gate не существует — line N: unknown session_keys directive "gate".

Тюнинг и производительность#

Четырнадцать скалярных директив верхнего уровня. Все, кроме log_level, требуют перезапуска.

Параметр Тип и допустимые значения По умолчанию Применение
cores целое; 0 — все производительные ядра 0 рестарт
low_latency on или off on рестарт
log_level одно из: debug, info, warn, error info горячее
rtsp_frame_source cmaf или raw_au cmaf рестарт
source_transport tcp, udp или udp-then-tcp tcp рестарт
rtp_max_payload байты, применяется в диапазоне 376–65507 1200 рестарт
max_sessions целое; 0 — без ограничения 0 рестарт
dvr_cleanup_secs секунды 600 рестарт
media_timeout секунды; 0 — сторож выключен 20 рестарт
on_demand_grace секунды 30 рестарт
udp_ingest_rcvbuf байты, от 64 КиБ до 1 ГиБ 16777216 (16 МиБ) рестарт
udp_ingest_shards целое от 1 до 16 2 рестарт
udp_ingest_pool_slots целое от 64 до 4096 1024 рестарт
udp_pull_rate попыток в секунду, от 0 до 100000; 0 — без ограничения 100 рестарт

cores — сколько ядер занимает нода. 0 означает «все производительные»: на гибридных процессорах берутся P-ядра, энергоэффективные пропускаются; на однородных — все. Положительное число закрепляет ровно столько ядер, начиная с самых быстрых.

cores 1 — это не «одно ядро на поток»

Директива задаёт, сколько ядер занимает нода целиком, а не сколько ядер достаётся камере. cores 1 сводит обработку всех камер на одно ядро: оно упирается в полку, видео начинает рассыпаться, остальные ядра простаивают. Если не уверены — оставьте cores 0.

low_latency включает режим малой задержки в плейлистах HLS. Выключение (off) отменяет его для всех зрителей сразу; отдельный зритель может отказаться от него сам параметром ?ll=off.

log_level — минимальная важность сообщений, попадающих в журнал ноды. Единственная директива тюнинга, которая применяется горячо. Отладочная диагностика в релизной сборке отсутствует физически, поэтому debug включает лишь то, что скомпилировано.

rtsp_frame_source выбирает, из какого кольца RTSP-раздача берёт кадры: cmaf — из общего кольца фрагментов (дешевле по процессору, задержка на один фрагмент), raw_au — покадрово (меньше задержка, дороже). Неверное значение: rtsp_frame_source must be "cmaf" or "raw_au" (got "x").

source_transport — транспорт по умолчанию, которым нода забирает поток с камер: tcp (RTP внутри управляющего соединения), udp или udp-then-tcp (сначала UDP, при неудаче TCP). Ошибка: source_transport must be "tcp", "udp", or "udp-then-tcp" (got "x"). У камеры, заведённой в Plane, транспорт можно переопределить индивидуально.

rtp_max_payload — предел полезной нагрузки пакета RTP при раздаче по RTSP/UDP. Значение 1200 безопасно проходит через туннели VPN без фрагментации на уровне IP. Если весь трафик идёт внутри одной локальной сети с MTU 1500, значение можно поднять примерно до 1460.

max_sessions ограничивает общее число одновременных сессий раздачи по всем протоколам. Запрос сверх лимита получает по RTSP отказ 453. 0 снимает ограничение.

dvr_cleanup_secs — период обслуживания архива: применение сроков хранения, вытеснение по заполненности диска, уборка осиротевших файлов.

media_timeout — сторож замершего видео: если соединение с камерой живо, но кадры не идут столько секунд, нода принудительно переподключается. 0 выключает сторож — не рекомендуется.

on_demand_grace — сколько секунд поток с признаком on_demand живёт без единого зрителя, прежде чем нода перестанет тянуть камеру. Тот же таймер задаёт окно ожидания при подъёме, чтобы поток не «моргал» при редких обращениях.

Последние четыре директивы имеют смысл только при приёме потоков по RTSP/UDP, то есть при source_transport udp или udp-then-tcp; при транспорте tcp они ни на что не влияют. udp_ingest_rcvbuf — размер приёмного буфера сокета (ядро дополнительно ограничивает его значением net.core.rmem_max), udp_ingest_shards — сколько пар сокетов RTP/RTCP заводится на ядро, udp_ingest_pool_slots — сколько буферов io_uring выделено на ядро (по 64 КиБ, выделяются по мере надобности; поднимайте в первую очередь, если /api/v1/system показывает рост счётчика ENOBUFS), udp_pull_rate — темп попыток подключения к источникам на ядро, который превращает «стадо» одновременных переподключений после сбоя в ровный подъём. Значения вне диапазона отвергаются дословно, например udp_ingest_shards 32 out of range (1..=16).

cluster { } — сборка из нескольких нод#

Блок скомпилирован в эту сборку, но в стандартной установке видеонаблюдения не используется: Plane его не пишет, потоки распределяет он сам. Приведён для полноты.

cluster {
    key "СЕКРЕТ-КЛАСТЕРА";
    peer node-a 10.0.0.10:9000 {
        public https://node-a.example.com;
        zone dc1;
        http 9443;
        rtsp 8554;
        storage archive /srv/cctv/archive { identity 3f2a9c14-1c0e-4f7b-9d21-8a6f0b4e5c77; }
    }
}
Параметр Тип и допустимые значения По умолчанию Применение
key строка — общий секрет для рукопожатия между нодами пусто рабочая нагрузка
peer <имя> <адрес> блок; ровно два аргумента, адрес вида host:port рабочая нагрузка

Внутри peer { } все директивы необязательны:

Параметр Тип и допустимые значения По умолчанию Применение
public базовый URL этой ноды для зрителей пусто — берётся хост из адреса рабочая нагрузка
zone строка: ЦОД, стойка, сегмент — для выбора ближайшей ноды пусто рабочая нагрузка
http, https, rtsp, rtsps, rtmp, rtmps, webrtc номера портов этой ноды; 0 — взять значение верхнего уровня 0 рабочая нагрузка
storage <имя> <путь> { } хранилища этой ноды; перекрывают одноимённое хранилище верхнего уровня рабочая нагрузка

Себя в списке нода находит по имени из аргумента запуска --node <имя> или переменной окружения HS_NODE. Неверное число аргументов: line N: `peer` takes <name> <addr>. Вложенное storage имеет те же поля и те же проверки, что и обычное (segment_secs, evict_at_percent, identity).

template «имя» { } — общие настройки для потоков#

template описывает набор значений по умолчанию и принимает ровно те же вложенные директивы, что и stream (см. следующий раздел). Поток подключает шаблон директивой template <имя>;, и его собственные директивы перекрывают унаследованные.

template cam-defaults {
    segment_seconds 2;
    prepush 4.5;
    rtsp_transports tcp;
}

Наследование только одноуровневое: шаблон, ссылающийся на другой шаблон, — ошибка, как и ссылка на несуществующее имя. Директива prefix допустима только внутри шаблона и превращает его в шаблон публикации, материализующий потоки по пути <prefix>/<имя>; значение — один непустой сегмент пути: template "x": publish prefix must be a single non-empty path segment (no '/'). На управляемой ноде потоки создаёт Plane, поэтому шаблон применим ограниченно — он полезен как заготовка на будущее и при ручной отладке.

Часть 2. Что принадлежит Plane#

Эти блоки Plane пишет сам — внутри управляемой области, а cctv { } вне её. Читать и сверять их полезно, править бесполезно или опасно. Ниже они описаны затем, чтобы вы понимали, что видите в файле, и могли проверить, то ли прислал Plane, чего вы ожидали.

cctv { } — удостоверение ноды (не редактировать)#

cctv {
  id 9190375b-4b9c-4281-8a55-f16afc89e0e0;        # идентификатор ноды в Plane
  plane_url "https://cctv.example.com";           # куда нода обращается за конфигурацией
  generation 1;                                   # поколение спаривания
  plane_pubkey "-----BEGIN PUBLIC KEY-----
REDACTED
-----END PUBLIC KEY-----
";                                                # единственный ключ, чьи команды нода выполняет
}

Блок лежит снаружи маркеров, в вашей половине файла, но формирует его Plane в момент создания ноды. Это удостоверение целиком: без него нода не спарится, а с изменённым — перестанет принимать команды. В CCTV-сборке блок обязателен.

Параметр Тип и допустимые значения По умолчанию Применение
id UUID в каноническом виде с дефисами пусто — не пройдёт проверку рестарт
plane_url URL, начинается с http:// или https://, до 2048 символов, без пробелов пусто — не пройдёт проверку рестарт
generation целое от 1 и выше 0 — не пройдёт проверку рестарт
plane_pubkey открытый ключ ES256 в формате PEM (SPKI) пусто — не пройдёт проверку рестарт

Что означает каждое поле:

  • id — идентификатор логической ноды в базе Plane. По нему Plane узнаёт, чью конфигурацию отдавать и чьи хранилища считать своими. Ошибка: cctv.id must be a canonical UUID.
  • plane_url — адрес контрол-плейна, по которому нода забирает конфигурацию и ключи проверки прав. Ошибка: cctv.plane_url must be a bounded HTTP(S) origin.
  • generation — поколение спаривания. Plane увеличивает его при перевыпуске файла, чтобы старый скачанный node.conf нельзя было подсунуть обратно. Ошибка: cctv.generation must be positive.
  • plane_pubkey — открытый ключ, подписью которого Plane заверяет управляющие запросы. Нода выполняет команду только если подпись верна, срок жизни предъявленного токена не больше 60 секунд и он не использовался повторно. Другого способа управлять нодой нет: локального пароля администратора у неё не существует. Ошибка: cctv.plane_pubkey must be an ES256 SPKI PEM public key.
Правка любого из четырёх полей ломает спаривание

Нода либо не пройдёт проверку конфигурации и уйдёт в цикл перезапусков, либо запустится и будет молча отвергать команды Plane — в админке она станет offline при живом процессе. Восстанавливать блок по памяти нельзя: скачайте свежий файл конфигурации в карточке ноды в Plane, перенесите из него блок cctv { } целиком и перезапустите сервис.

stream «путь» { } — камера#

Один блок — один поток одной камеры. Путь блока — это адрес раздачи: https://node.example.com/<путь>/master.m3u8 для HLS и rtsp://node.example.com:8554/<путь> для RTSP. Ровно те же вложенные директивы принимает template.

stream cam/0f3d5b2a-11c4-4a90-93d7-2b6c8e0f41aa/main {
	input "rtsp://svc:REDACTED@10.20.0.11:554/Streaming/Channels/101";
	labels 6mIWXn0nSA6QaHnHRk4KaQ north-wing;
	on_demand on;
	dvr archive 14d;
}
Параметр Тип и допустимые значения По умолчанию Применение
input URL источника и необязательные ключ=значение; повторяется обязателен хотя бы один рестарт потока
segments_count целое, сегментов в окне плейлиста; 0 — значение по умолчанию 0, то есть 5 рестарт потока
segment_seconds дробное, секунды; 0 — значение по умолчанию 0, то есть 2 с рестарт потока
prepush дробное, секунды отставания нового зрителя от «живого края»; 0 — по протоколу 0 рестарт потока
audio_only_playlist флаг: добавить в мастер-плейлист вариант «только звук» выключено рестарт потока
http_ts флаг; в этой сборке недопустим выключено рестарт потока
hls_ts флаг; в этой сборке недопустим выключено рестарт потока
multicast_out группа:порт и параметры ttl=, iface=, loopback=, tos=; в этой сборке недопустим пусто рестарт потока
disabled флаг: поток не запускать, настройки сохранить выключено рестарт потока
on_demand флаг: тянуть камеру только под зрителя выключено рестарт потока
tls_verify system, insecure, pin: плюс 64 шестнадцатеричных символа, ca: плюс путь пусто, равно system рестарт потока
rtsp_transports both, tcp или udp — что разрешено зрителям по RTSP пусто, равно both рестарт потока
caps список вкладок плеера: либо только разрешающие имена, либо только запрещающие с минусом пусто — набор выводится автоматически рестарт потока
allowed_referers список доменов, которым разрешено встраивать плеер; none разрешает и прямую ссылку пусто — без ограничения рестарт потока
labels список меток авторизации, не более 40, каждая от 1 до 64 байт пусто горячее
dvr строка записи архива, см. ниже запись выключена рестарт потока
host список имён нод, которым разрешено захватывать поток (только кластер) пусто рестарт потока
restream список имён нод, которым разрешено ретранслировать (только кластер) пусто рестарт потока
template имя шаблона, значения которого наследуются не задан рестарт потока
prefix один сегмент пути; допустим только внутри template не задан рестарт потока

input повторяется и образует цепочку резервирования в порядке записи: нода спускается вниз по списку при отказе и поднимается обратно при восстановлении — по ключевому кадру и с выдержкой, чтобы не мигать между источниками. Параметр source_timeout=N задаёт для конкретного источника время без кадров, после которого он считается неживым (0 — значение по умолчанию протокола). Ошибки: line N: `input` needs a url, line N: `input` option "x" must be key=value, line N: unknown `input` option "x". Совсем без источника — stream "x": needs `source` or a non-empty `sources` chain.

on_demand экономит трафик и процессор на редко просматриваемых камерах, но на пишущем потоке игнорируется: запись означает постоянный приём, иначе архив был бы с дырами.

tls_verify — как проверять сертификат источника с адресом rtsps:// или https://. Значение pin: с отпечатком SHA-256 листового сертификата в шестнадцатеричном виде подходит для камер с самоподписанными сертификатами; insecure отключает проверку. Опечатка отвергается сразу: stream "x": bad tls_verify "…": <детали>.

caps ограничивает вкладки протоколов на странице плеера /<путь>/embed.html. Словарь: hls, hls-ll, mse-ws, dash, webrtc, whip, dvr, timeshift, preview. Либо перечисляете разрешённые, либо только запрещённые с минусом; смешивать нельзя: stream "x": caps cannot MIX deny (-x) and allow (bare) tokens — use one mode. Протокол, которого эта сборка не отдаёт, разрешать бессмысленно: stream "x": cap "webrtc" not served (not in the derivable set […]).

allowed_referers — защита от встраивания видео на чужие страницы, а не аутентификация. Совпадение по границе точки: example.com разрешает и cdn.example.com, но никогда evilexample.com. Указывать надо домены, не ссылки: stream "x": allowed_referers token "…" must be a host, not a URL path.

labels — непрозрачные метки: зритель видит поток, если его выданные права пересекаются с этим списком. UUID с дефисами приводится к 22-символьной короткой форме при загрузке. Ограничения: stream "x": at most 40 labels (N given), label "…" exceeds 64 bytes, label "…" must not contain whitespace/control chars. Изменение меток никогда не перезапускает поток — права меняются на лету, картинка не моргает.

Пути потоков должны быть непустыми и уникальными: duplicate stream path "x", a stream has an empty path.

Строка dvr — запись архива#

dvr archive 14d 500G copy=s3s://bucket@coldtier 90d 2T;

Аргументы позиционные, но распознаются по форме. Единственное, что важно в порядке: всё до copy= относится к локальному диску, всё после — к копии в S3.

Форма аргумента Как трактуется
слово имя локального хранилища (диска)
число с суффиксом s, m, h, d, w, mo, y срок хранения по времени
число с суффиксом K, M, G, T срок хранения по объёму
mode=async или mode=cross режим репликации архива в кластере
copy=s3://bucket@account или copy=s3s://bucket@account ссылка на холодный ярус S3, s3s — поверх TLS
значение с косой чертой цель нода/том в кластере

Проверки, которые чаще всего срабатывают, — дословно:

  • stream "x": record needs a known local DISK storage (got "y"); use copy= for an s3 cold tier
  • stream "x": bad retention_time "…" (s/m/h/d/w/mo/y) и bad retention_size "…" (K/M/G/T)
  • stream "x": copy= target "y" is not a declared s3 storage
  • stream "x": copy= needs a LOCAL retention — `dvr <storage> <time|size> copy=…` — else local media never evicts
  • stream "x": copy= requires local storage "y" to have a canonical non-nil lowercase UUID identity
  • stream "x": s3 retention time "7d" is shorter than local retention time "14d"; cold retention must be uncapped or cover local retention to prevent delete/re-upload churn

Последнее правило стоит понять: холодная копия обязана жить не меньше локальной, иначе система будет удалять запись в S3 и тут же загружать её снова.

storage «имя» «путь» { } — диск под архив#

storage archive /srv/cctv/archive {
	identity 3f2a9c14-1c0e-4f7b-9d21-8a6f0b4e5c77;
	segment_secs 1800;
	evict_at_percent 85;
}

Два позиционных аргумента обязательны — имя и путь: line N: `storage` takes <name> <path>, or <name> s3 { … }.

Параметр Тип и допустимые значения По умолчанию Применение
identity строка до 128 символов без пробелов и косых черт; на CCTV-ноде обязательна пусто рестарт потоков
segment_secs ровно одно из: 900, 1800, 3600 900 рестарт потоков
evict_at_percent процент занятости файловой системы, 0 — вытеснение выключено 90 рестарт потоков

identity — неизменяемый идентификатор диска в Plane. Он же записывается маркером на сам том, поэтому размонтированный архив отличим от пустой точки монтирования — и нода не начнёт писать в корневую файловую систему вместо диска. Ошибки: storage "x": a cctv-node disk requires its Plane-managed identity, storage "x": invalid managed disk identity.

segment_secs — целевая длительность файла записи. Набор закрыт: storage "x": segment_secs must be one of 900, 1800 or 3600 seconds. Более короткие значения множат число файлов и объектов, более длинные затягивают завершение записи и восстановление после сбоя.

evict_at_percent — клапан по заполненности: когда файловая система переходит порог, самые старые записи удаляются, пока занятость не опустится на 2 процентных пункта ниже порога.

Путь на CCTV-ноде должен быть абсолютным и нормализованным, без ., .. и символических ссылок (storage "x": a cctv-node disk path must be absolute and normalized); корень запрещён (storage "x": filesystem root cannot be a managed DVR storage); два хранилища не могут указывать в один каталог (managed DVR storages "a" and "b" share path "/srv/x" in node scope "node"); имена уникальны (duplicate storage name "x").

storage «имя» s3 { } — холодный ярус в S3#

storage coldtier s3 {
	endpoint s3.example.com;
	region eu-central-1;
	access_key REDACTED;
	secret_key REDACTED;
	addressing vhost;
	tls_verify system;
}
Параметр Тип и допустимые значения По умолчанию Применение
endpoint хост или хост:порт без схемы; обязателен рестарт потоков
region строка us-east-1 рестарт потоков
access_key строка; обязателен рестарт потоков
secret_key строка; обязателен рестарт потоков
addressing vhost или path пусто, равно vhost рестарт потоков
tls_verify system, insecure, pin: плюс 64 шестнадцатеричных символа, ca: плюс путь пусто, равно system рестарт потоков

Схема (s3:// или s3s://) в endpoint не пишется — она живёт в ссылке copy= у потока: s3 storage "x": endpoint is a bare host[:port] — the s3://|s3s:// scheme lives on the stream's copy= reference. Имя корзины тоже задаётся в copy=, а не здесь. Способ адресации path нужен объектным хранилищам, которые не поддерживают имя корзины в домене.

У учётной записи S3 нет ни path, ни identity — это поля дискового хранилища. Писать поток напрямую в S3 нельзя: S3 — только холодная копия того, что уже записано на диск.

auth_backend { } — авторизация просмотра#

Определяет, как нода проверяет права зрителя. Plane всегда пишет type jwks: ключи для проверки подписи токена нода забирает по HTTP из его набора ключей.

auth_backend {
	type jwks;
	url https://cctv.example.com/.well-known/jwks.json;
	issuer hastreamer-cctv-plane;
	perms_claim grants;
	revoked_url https://cctv.example.com/.well-known/cctv-revoked;
}
Параметр Тип и допустимые значения По умолчанию Применение
type одно из: none, internal, hs256, jwks, http пусто горячее
url адрес набора ключей для jwks или адрес проверки для http пусто горячее
issuer ожидаемый издатель токена; проверяется, если задан пусто горячее
audience ожидаемая аудитория токена; проверяется, если задана пусто горячее
perms_claim имя поля токена, в котором лежит список прав grants горячее
secret общий секрет для hs256; в выдаче API скрыт пусто горячее
revoked_url адрес списка отозванных токенов, который нода периодически опрашивает пусто — список не опрашивается горячее

Ошибки: auth_backend.type must be one of none|internal|hs256|jwks|http (got "x"), auth_backend type "jwks" requires a url, auth_backend type "hs256" requires a secret, auth_backend type "internal" needs at least one `principal` block.

Если блока нет вообще, раздача открыта всем. Поэтому у управляемой ноды он обязан присутствовать в области: managed region must contain the plane playback auth_backend. Токен, чей идентификатор попал в список по revoked_url, получает отказ 403 в пределах интервала опроса — так работает немедленное отключение доступа.

principal и public_paths — только внутри области#

Обе директивы существуют в грамматике, но на управляемой ноде бесполезны: Plane их не формирует, а вне маркеров они запрещены и останавливают загрузку.

principal alice {
	pass  "REDACTED";
	token "REDACTED";
	permission read     live/*;
	permission playback dvr/cam1 hls rtsp;
	permission read     label=north label=vip hls;
}

public_paths health status/*;
Параметр Тип и допустимые значения По умолчанию Применение
pass пароль для базовой аутентификации пусто горячее
token токен-предъявитель; сосуществует с pass пусто горячее
permission действие, затем путь и метки label=, затем протоколы; повторяется пусто горячее
public_paths список путей раздачи, освобождённых от проверки прав; накапливается пусто горячее

Имя блока — это имя пользователя; principal any { } описывает анонимного зрителя. В строке permission первый элемент — действие, следующий считается путём, если не начинается с label=; остальные label= — метки, всё прочее — протоколы. Путь и метки складываются по «или». Работает всё это только при auth_backend { type internal; }.

Чего в этом файле нет#

  • Директивы api не существует. REST API, плеер, HLS-fMP4, MSE-WS, превью и ответчик ACME живут на слушателях http и https. Отдельного порта у API нет.
  • Блока license { } не существует. Путь к файлу лицензии, адрес активации и логика ограничений зашиты в бинарь и не настраиваются. Сама лицензия — отдельный файл /etc/hastreamer-cctv/license.txt, см. Лицензирование. Попытка описать лицензию в конфигурации даёт line N: unknown directive "license".
  • Механизма include нет. Один файл; снимок cctv-node/managed.conf включением не является.
  • Нет протоколов, не вошедших в сборку. CCTV-сборка содержит: приём с камер по RTSP, раздачу по RTSP, HLS-fMP4, MSE-over-WebSocket, превью одним ключевым кадром в MP4 и fMP4, запись и воспроизведение архива со сдвигом по времени, холодный ярус S3, членство в кластере. Директива, называющая отсутствующий протокол, — это ошибка загрузки, а не молчаливое игнорирование:
Что вы написали Что ответит нода
rtmp <порт>; или rtmps <порт>; `rtmp_port`/`rtmps_port` is set but RTMP ingest is not in this build (feature `ingest-rtmp`)
webrtc <порт>; `webrtc_udp_port` is set but WebRTC is not in this build (feature `egress-webrtc`)
vod <имя> <корень> { } `vod` libraries are configured but VoD is not in this build (feature `vod`)
hls_ts; в потоке stream "X": `hls_ts` egress needs the `egress-hls-ts` feature — not in this build
http_ts; в потоке stream "X": `http_ts` egress needs the `egress-http-ts` feature — not in this build
multicast_out …; в потоке stream "X": `multicast_out` egress needs the `egress-multicast` feature — not in this build
input file://… stream "X": a `file://` source needs the `ingest-file` feature — not in this build
input rtmp://… stream "X": an `rtmp(s)://` source needs the `ingest-rtmp` feature — not in this build
input http://…, udp://…, multicast://… stream "X": an `http(s)/udp/multicast` source needs the `ingest-hls` feature — not in this build

Пригодные схемы источника на ноде видеонаблюдения — rtsp:// и rtsps://; дополнительно всегда доступна встроенная синтетическая схема demo:// для проверки раздачи без камеры.

Блок видео по запросу (VoD) разбирается, но отвергается валидацией: и форма vod <имя> <корень> { segment_duration N; } с библиотекой, и одиночный настроечный блок vod { index_cache N; index_ttl N; }. Настроечный блок сам по себе загрузку не ломает (значения index_cache от 1 до 4096, index_ttl от 1 до 86400 секунд), но в этой сборке ни на что не влияет.

Полный пример node.conf#

Управляемая нода на отдельном сервере: HTTPS с автоматическим сертификатом, один диск архива и одна камера в двух профилях. Домены — примеры, все секреты вырезаны.

# /etc/hastreamer-cctv-node/node.conf
# Всё ВЫШЕ маркеров — ваше и сохраняется. Всё МЕЖДУ маркерами принадлежит Plane и
# восстанавливается при каждом старте из /etc/hastreamer-cctv-node/cctv-node/managed.conf.

# ── Слушатели (требуют перезапуска) ─────────────────────────────────────────────
http  80;                   # HLS-fMP4, MSE-WS, превью, REST API, плеер, проверка ACME HTTP-01.
                            # Нулём быть не может. Порт 80 выбран, чтобы работал выпуск
                            # сертификата; Plane обращается к ноде по этому же адресу —
                            # держите Control URL в Plane согласованным.
https 443;                  # видео и API поверх TLS; 0 выключил бы TLS и вместе с ним ACME
rtsp  8554;                 # RTSP-раздача: rtsp://node.example.com:8554/<путь-потока>
rtsps 8555;                 # RTSP поверх TLS, тот же сертификат; 0 — выключено
                            # rtmp, rtmps и webrtc не указаны: их нет в сборке видеонаблюдения

# ── Размер рабочего пула и журнал ───────────────────────────────────────────────
cores 0;                    # 0 = все производительные ядра. НЕ ставьте 1 «на всякий случай»:
                            # это сведёт все камеры на одно ядро
log_level info;             # debug, info, warn, error — применяется горячо

# ── Удостоверение ноды: выдал Plane, руками не трогать ──────────────────────────
cctv {
  id 9190375b-4b9c-4281-8a55-f16afc89e0e0;         # идентификатор ноды в Plane
  plane_url "https://cctv.example.com";            # адрес контрол-плейна
  generation 1;                                    # поколение спаривания
  plane_pubkey "-----BEGIN PUBLIC KEY-----
REDACTED
-----END PUBLIC KEY-----
";                                                 # ключ, которым Plane подписывает команды
}

# ── Сертификат по умолчанию: чем нода отвечает без имени домена (горячее) ───────
tls {
  self_signed_names node.example.com;              # cert и key пусты → нода один раз выпускает
                                                   # самоподписанную пару и хранит её в tls-state:
                                                   # отпечаток не меняется при перезапусках.
                                                   # Настоящий сертификат для домена — ниже.
                                                   # Если Plane стоит на этом же сервере, укажите
                                                   # здесь cert, key и sync_from (порты тогда
                                                   # берите не 80/443 — их занимает Plane)
}

# ── Автоматический сертификат для домена ноды ───────────────────────────────────
acme_directory https://acme-v02.api.letsencrypt.org/directory;   # рестартовая; на этапе настройки
                                                                 # используйте тестовый каталог
acme_contact ops@example.com;                                    # рестартовая; уведомления о сроках

certificate node-public {
  hosts node.example.com;   # только конкретное имя: маска через HTTP-01 невозможна
  acme;                     # флаг без значения; вместе с cert и key не сочетается
}
# Условие выпуска: центр сертификации обращается на порт 80 по адресу
# http://node.example.com/.well-known/acme-challenge/<токен>. Здесь http = 80, поэтому проверка
# проходит. При http 9443 понадобился бы проброс внешнего порта 80.

# ── Политика доступа по HTTP (горячее) ──────────────────────────────────────────
trusted_proxies 10.0.0.0/8 ::1;              # верить заголовкам прокси только с этих адресов
allow_origins https://cctv.example.com;      # по умолчанию разрешены любые источники

# ── Билеты медиасессий (горячее) ────────────────────────────────────────────────
session_keys {
  ttl 3600;                 # срок жизни билета живого просмотра, секунды
  vod_session_ttl 86400;    # для записей; допустимо от 300 до 604800
  bind_ip off;              # off, если зрители переходят между Wi-Fi и мобильной сетью
}

auth {
  session_secret "REDACTED-ОБЩИЙ-СЕКРЕТ-ФЛОТА";
                            # это НЕ пароль администратора (он на CCTV-ноде запрещён), а корень
                            # ключа подписи билетов. Одинаковый на всех нодах — билеты переживают
                            # перезапуск и проходят проверку после перенаправления зрителя
}

# ── Приём с камер и обслуживание архива (требуют перезапуска) ───────────────────
source_transport tcp;       # транспорт по умолчанию: tcp, udp или udp-then-tcp
media_timeout 20;           # переподключить камеру, если кадры не идут 20 секунд (0 — выключено)
on_demand_grace 30;         # через сколько секунд без зрителей отпускать камеру on_demand
dvr_cleanup_secs 600;       # период уборки архива: сроки хранения, вытеснение, осиротевшие файлы

# ════════════════════════════════════════════════════════════════════════════════
# НИЖЕ — ОБЛАСТЬ PLANE. Правки здесь исчезают при следующем старте.
# Камеры и диски меняйте в интерфейсе Plane.
# ════════════════════════════════════════════════════════════════════════════════
# >>> MANAGED BY cctv-plane — edits will be overwritten >>>
storage archive /srv/cctv/archive {          # диск архива: имя и абсолютный нормализованный путь
	identity 3f2a9c14-1c0e-4f7b-9d21-8a6f0b4e5c77;  # идентификатор диска и маркер на самом томе
	segment_secs 1800;                       # длительность файла записи: только 900, 1800 или 3600
	evict_at_percent 85;                     # вытеснять старое при занятости выше 85 % (0 — не вытеснять)
}
auth_backend {                               # кто имеет право смотреть
	type jwks;                               # подпись токена проверяется по набору ключей Plane
	url https://cctv.example.com/.well-known/jwks.json;
	issuer hastreamer-cctv-plane;            # ожидаемый издатель токена
	perms_claim grants;                      # поле токена со списком прав
	revoked_url https://cctv.example.com/.well-known/cctv-revoked;   # список отозванных токенов
}
stream cam/0f3d5b2a-11c4-4a90-93d7-2b6c8e0f41aa/main {   # путь раздачи этого потока
	input "rtsp://svc:REDACTED@10.20.0.11:554/Streaming/Channels/101";  # основной профиль камеры;
	                                         # в кавычках, потому что в логине и пароле бывают
	                                         # символы, значимые для грамматики. Повторный input —
	                                         # это резервный источник
	labels 6mIWXn0nSA6QaHnHRk4KaQ north-wing;  # метки прав: организация и тег. Их правка
	                                           # НЕ перезапускает поток
	dvr archive 14d;                         # писать на диск archive, хранить 14 суток
	                                         # (14d — сутки; 14m было бы 14 МИНУТ, 14mo — месяцев)
}
stream cam/0f3d5b2a-11c4-4a90-93d7-2b6c8e0f41aa/sub {    # второй профиль той же камеры
	input "rtsp://svc:REDACTED@10.20.0.11:554/Streaming/Channels/102";
	labels 6mIWXn0nSA6QaHnHRk4KaQ north-wing;
	on_demand on;                            # тянуть камеру только под зрителя; отпускать через
	                                         # on_demand_grace. На пишущем потоке игнорируется
}
# <<< MANAGED BY cctv-plane <<<

Порядок безопасного редактирования#

  1. Сделайте копию файла. Это единственный способ вернуться за секунду.
  2. Правьте только выше открывающего маркера.
  3. Проверьте правку перезагрузкой конфигурации — при ошибке нода останется работать на старой.
  4. Прочитайте журнал — там либо отчёт о применённых изменениях, либо ошибка с номером строки.
  5. Перезапустите сервис, только если меняли рестартовую директиву (список — в разделе Горячее применение и перезапуск).
sudo cp /etc/hastreamer-cctv-node/node.conf /root/node.conf.bak      # 1. копия
sudo -e /etc/hastreamer-cctv-node/node.conf                          # 2. правка вне маркеров
sudo systemctl reload hastreamer-cctv-node                           # 3. проверка без риска
sudo journalctl -u hastreamer-cctv-node -n 30 --no-pager             # 4. вердикт
sudo systemctl restart hastreamer-cctv-node                          # 5. только для рестартовых
Правка применилась

После шага 3 в журнале не должно быть строки config reload REJECTED. Успешная перезагрузка выглядит так:

config reload: applied epoch 7 (hash 68d0b9f93ab3999d)

Если файл после правки совпал с действующей конфигурацией, будет config reload: no change.

После перезапуска убедитесь, что сервис жив и слушатели подняты:

systemctl is-active hastreamer-cctv-node      # active
sudo ss -tlnp | grep hastreamer               # ожидаемые порты в состоянии LISTEN
Запрет на изменение конфигурации из Plane

Если нода настроена вручную и её конфигурацию нельзя менять извне, создайте файл-замок:

sudo touch /etc/hastreamer-cctv-node/node.conf.locked

После этого любой записывающий запрос к конфигурации через API возвращает 423 Locked. Чтение состояния и перезапуск продолжают работать. Снимается замок удалением файла.

Учтите: на спаренной ноде тем же замком блокируется и доставка конфигурации из Plane — новые камеры и диски на такую ноду не приедут. Ставьте его на время разбирательства, а не насовсем.

Частые ошибки при ручной правке#

Симптом Причина Что делать
Нода в цикле перезапусков после правки Ошибка в файле, выход с кодом 1 journalctl -u hastreamer-cctv-node -n 50 — в сообщении есть номер строки; вернуть копию
stream may appear only in the plane-managed region Камера дописана руками вне маркеров Удалить блок, добавить камеру в Plane
Правка исчезла после перезапуска Она была внутри маркеров Повторить её выше открывающего маркера
managed region exists but no plane-approved snapshot is present Область есть, снимка managed.conf нет Дать Plane прислать конфигурацию заново либо удалить область целиком
Порт сменили, ничего не изменилось Слушатели рестартовые systemctl restart, а не reload
Нода стала offline после смены порта В Plane остался старый Control URL Обновить адрес управления ноды в Plane
Сертификат так и не выпустился Порт 80 недоступен снаружи, либо нет ни одного TLS-слушателя, либо в hosts маска Пробросить порт 80, включить https, указать конкретное имя
Архив исчезает через полчаса В сроке хранения m вместо mo Исправить срок в Plane: 30mo — месяцы
Перезапустились все потоки от мелкой правки Изменено поле хранилища или учётной записи S3 Так и задумано: от них зависят все пишущие потоки

Установка и спаривание ноды — Установка с нуля: Node. Разбор сбоев в работе — Диагностика неисправностей.