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

Камеры

Создание и изменение камер, массовые операции, ONVIF, PTZ, доступ к видео и архиву.

Камера — центральный объект системы: она принадлежит организации, размещена на ноде, несёт один или несколько профилей (потоков), может писать архив, отдаваться операторам и раздаваться по ссылке. Здесь собраны все маршруты, работающие с камерами, включая ONVIF, PTZ и шаблоны камер.

Все запросы требуют заголовка Authorization: Bearer $TOKEN. Базовый URL, получение токена, общий формат ошибок и правила пагинации — в разделе Введение и аутентификация.

В примерах используются переменные BASE=https://cctv.example.com и TOKEN, а адрес камеры показан как 192.0.2.10 — это адрес из диапазона, зарезервированного под документацию; подставьте свой.

Что нужно знать до первого запроса#

Адрес камеры не выводится из ссылки RTSP

hostсамостоятельное поле камеры, и Plane никогда не извлекает его из rtsp_url (это проверено по коду). Если завести камеру через API и не передать host, столбец «Адрес» в админке останется пустым, а ONVIF и PTZ по этой камере работать не смогут: им нужен адрес устройства, а не ссылка на поток.

Передавайте host (и при необходимости onvif_port) всегда, даже когда ссылка RTSP уже содержит тот же адрес. Пример создания ниже задаёт и то, и другое.

Учётные данные камеры: два независимых места

Логин и пароль устройства можно задать двумя способами, и они не связаны между собой:

  1. Поля username и password камеры — нода подставит их в источник самостоятельно.
  2. Прямо в rtsp_url профиля (rtsp://логин:пароль@адрес/путь) — эту строку никто не разбирает, не проверяет и не вырезает из неё учётные данные.

Оба способа рабочие; смешивать их без нужды не стоит. Если пароль содержит @, :, / или ?, внутри URL его обязательно нужно закодировать процентами — иначе ссылка распадётся на части и камера не подключится.

Массовые операции — «всё или ничего»

POST /api/v1/cameras/bulk выполняется одной транзакцией. Один идентификатор чужой организации (403 cross_org) или один несуществующий идентификатор (404 not_found) отклоняют весь пакет — частичного применения не бывает и отчёта «что прошло, а что нет» тоже. Кроме того: операции с тегами требуют, чтобы вся выборка была из одной организации, а камеру, подключённую через туннель, пакетно удалить нельзя — только по одной.

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

Пропуск поля и null в PATCH — разные вещи
  • поле отсутствует в теле — значение не меняется;
  • поле передано со значением — значение устанавливается;
  • поле передано как null — значение очищается, но только у тех полей, которые это допускают.

У камеры очищаются через null: host, onvif_port, make, model, serial, firmware, dvr_value, dvr_storage, dvr_copy_storage, dvr_copy_bucket, dvr_copy_value, interval_timeout_secs. У остальных полей null равносилен пропуску — очистить их так нельзя.

Перенос камеры на другую ноду или в другое хранилище оставляет архив позади

Архив камеры физически лежит на диске той ноды, где камера размещена. Смена node_id или dvr_storage означает, что запись начнётся заново на новом месте, а прежние записи для этой камеры на старой ноде становятся недоступны и удаляются фоновой очисткой.

Если записи нужны — выгрузите их до переноса через POST /api/v1/cameras/{id}/export. Подробнее о жизненном цикле архива: Архив (DVR).

Система защищает от части таких переносов: пока включена копия в холодное хранилище, перенос запрещён (409 dvr_cold_enabled_blocks_relocation), и пока не доиграла очистка предыдущего переноса — тоже (409 dvr_local_cleanup_pending / 409 dvr_cold_cleanup_pending).

Три списка камер — и почему их три#

Маршрут Кому доступен Что отдаёт
GET /api/v1/cameras администратор полные административные строки: нода, настройки архива, теги, живой статус
GET /api/v1/cameras/lite любой вошедший только {id, name}, в объёме бита EVENTS
GET /api/v1/mobile/cameras любой вошедший несекретные карточки оператора, в объёме LIVE ∪ DVR ∪ EVENTS

Оператору первый маршрут закрыт (403 not_admin) — именно поэтому существуют два других. Пароль камеры не входит ни в один список и ни в одну карточку: его отдаёт единственный маршрут GET /api/v1/cameras/{id}/credentials.

Фильтры ?tag= (один uuid или несколько через запятую — камера должна нести все перечисленные теги) и ?status= (тот же словарь problem|offline|error|live|parked) действуют одинаково и на административном GET /cameras, и на мобильном GET /mobile/cameras — форма параметров общая.

Жёсткие ограничения, действующие всегда: не более 32 тегов на камеру, не более 4 профилей на камеру, не более 500 идентификаторов в пакетной операции, не более 50 камер в одной подписке на поток статусов.

Общие структуры#

Объект камеры#

Эта структура «расплющена» внутрь строк списка и внутрь ответа с деталями камеры.

{
  "id": "<UUID>", "org_id": "<UUID>", "node_id": "<UUID>",
  "name": "Вход, главный холл",
  "enabled": true,
  "use_onvif": true, "onvif_events_enabled": false,
  "ptz_pan_tilt": true, "ptz_zoom": true,
  "tunnel": null,
  "host": "192.0.2.10", "onvif_port": 80,
  "make": null, "model": null, "serial": null, "firmware": null,
  "username": "operator",
  "dvr_mode": "duration", "dvr_value": 604800, "dvr_storage": "dvr1",
  "dvr_copy_storage": null, "dvr_copy_bucket": null,
  "dvr_copy_mode": "none", "dvr_copy_value": null,
  "dvr_prune_without_events": false, "dvr_event_grace_seconds": 1800,
  "mosaic_transport": "mse-ws", "mosaic_profile": "main", "mosaic_allow_switch": false,
  "interval_timeout_secs": null,
  "notes": "",
  "created_at": "2026-08-18T10:00:00Z", "updated_at": "2026-08-18T10:00:00Z"
}
Поле Смысл
enabled выключенная камера не подключается и не пишет архив, но остаётся в списках
use_onvif Plane сам получает ссылки потоков с устройства по ONVIF
onvif_events_enabled опрос событий устройства (независим от use_onvif)
ptz_pan_tilt, ptz_zoom заявленные возможности поворота и зума
tunnel заполнено у камеры, подключённой через агента: {agent_id, device_id, rtsp_lease_id, onvif_lease_id}
username несекретная половина учётных данных; пароля в этой структуре нет никогда
dvr_mode none — не пишем, duration — глубина в секундах, size — объём в байтах
dvr_value секунды или байты в зависимости от dvr_mode; null, только если dvr_mode = "none"
dvr_storage имя дискового хранилища на ноде
dvr_copy_* политика копии в холодное хранилище S3
dvr_prune_without_events удалять записи, вокруг которых не было событий
dvr_event_grace_seconds защитный интервал вокруг события при такой очистке
mosaic_transport mse-ws или hls — транспорт по умолчанию в мозаике
mosaic_profile какой профиль показывать в мозаике: main или sub
mosaic_allow_switch разрешить оператору переключать профиль вручную
interval_timeout_secs переопределение тайм-аута простоя для этой камеры

Профиль#

Профиль — это один поток камеры. У камеры от одного до четырёх профилей, и ровно один из них обязан иметь kind: "main".

{ "id": "<UUID>", "kind": "main", "source": "manual",
  "rtsp_url": "rtsp://operator:пароль@192.0.2.10:554/stream1",
  "onvif_profile_token": null, "onvif_stream_uri": null,
  "mode": "always",
  "width": 1920, "height": 1080, "codec": "h264", "fps": 25.0 }
  • kind — произвольный идентификатор потока по маске ^[a-z0-9_]{1,16}$, уникальный в пределах камеры. По соглашению main — основной поток, sub — вспомогательный (для мозаик и превью).
  • sourcemanual (ссылка задана руками) или onvif (ссылку получает Plane с устройства).
  • mode вычисляется, а не задаётся: у main всегда always, у остальных — on_demand (поток поднимается только когда кто-то смотрит).
  • onvif_stream_uri заполняется после успешного POST /api/v1/cameras/{id}/onvif/resolve.
Профиль ONVIF без разрешения адреса не стримит

Пока onvif_stream_uri пуст, нода такой профиль пропускает. Камера, заведённая с source: "onvif", но без вызова …/onvif/resolve, просто никогда не покажет видео — при этом ни одна ошибка нигде не появится.

Живой статус камеры#

Поле live_status в административном списке и в мобильной витрине вычисляется по строгому приоритету:

Значение Когда
disabled камера выключена (enabled: false) — перекрывает всё остальное
live нода сообщает, что поток идёт нормально
degraded поток идёт с проблемами
failing нода пытается подключиться и не может
parked поток по требованию сейчас никому не нужен и не поднят
unavailable нода не активна или отключена
waiting нода активна, но ещё не сообщила состояние этой камеры

Камеры#

GET /api/v1/cameras#

Администратор — суперадмин видит всю установку или одну организацию через ?org=, администратор организации — только свою.

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

Параметры запроса

Параметр Тип По умолчанию Смысл
org uuid сузить до организации (для суперадмина); чужая организация для остальных — 403 cross_org
node uuid только камеры, размещённые на этой ноде
after uuid курсор постраничной выдачи: строки с id > after
limit целое 500 размер страницы, прижимается к [1, 500]
q строка поиск по имени, адресу, модели и имени ноды; % и _ работают как шаблон
tag uuid | CSV из uuid камеры, несущие все перечисленные теги (семантика «И»): один uuid или список через запятую tag=a,b; повторы игнорируются, битый uuid — 400 bad_tag
status строка problem | offline | error | live | parked; другое значение — 400 bad_camera_status

Значения status разворачиваются так (снимок состояния берётся один раз до запроса, поэтому страница не может противоречить фильтру):

Значение Что попадает в выборку
live камеры со статусом live
error камеры со статусом degraded
parked камеры со статусом parked
offline failing и unavailable, а также включённые камеры, которых нет в снимке, если их нода неактивна или отключена
problem degraded, failing, unavailable, а также все включённые камеры, отсутствующие в снимке

Пример curl

curl -sS "$BASE/api/v1/cameras?limit=100&status=problem" \
  -H "Authorization: Bearer $TOKEN"

Пример ответа

{
  "rows": [
    {
      "id": "<UUID>", "org_id": "<UUID>", "node_id": "<UUID>",
      "name": "Вход, главный холл", "enabled": true,
      "host": "192.0.2.10", "dvr_mode": "duration", "dvr_value": 604800,
      "node_name": "node-1", "node_state": "active", "node_disabled": false,
      "tags": ["<UUID>"], "has_source": true,
      "live_status": "failing", "connected_since_ms": null, "reconnects": 17
    }
  ],
  "next": null
}

Строка — это объект камеры плюс поля списка: node_name, node_state (pending | active | retired), node_disabled, tags (массив идентификаторов тегов), has_source, live_status, connected_since_ms, reconnects.

has_source — это статическая настройка, а не признак работоспособности: он равен true, если хоть у одного профиля есть rtsp_url либо разрешённый адрес ONVIF.

Ошибки

Код Тело Причина
400 bad_camera_status значение status вне списка, в том числе пустое (?status=)
403 cross_org указана чужая организация
403 not_admin роль оператора
503 db_unavailable база недоступна

Примечания

  • ?status= с пустым значением — это не «фильтр не задан», а недопустимое значение: приходит 400 bad_camera_status. Чтобы отключить фильтр, уберите параметр целиком.
  • Корректный статус, под который ничего не подошло, возвращает {"rows":[],"next":null} с кодом 200 — это не ошибка.
  • Поиск q не экранирует % и _; подробнее — в разделе Введение.

POST /api/v1/cameras#

Администратор — суперадмин может создать камеру в любой организации через org_id, администратор организации — только в своей.

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

Тело запроса. Обязательны только name и profiles; остальное имеет значения по умолчанию.

Основное

Поле Тип По умолчанию Смысл
name строка обязательно имя камеры, пробелы по краям обрезаются, пустым быть не может
org_id uuid | null организация вызывающего чужая организация — только для суперадмина
enabled bool | null true включить сразу
notes строка | null "" заметка администратора
tags массив uuid [] не более 32; каждый тег должен существовать в целевой организации

Размещение

Поле Тип По умолчанию Смысл
node_auto bool false подобрать ноду автоматически
node_id uuid | null null разместить на конкретной ноде

node_auto и node_id взаимоисключающи, и одно из них обязательно: указать оба или ни одного — 400 camera_node_choice. Камера не может существовать «без размещения».

Устройство, ONVIF и PTZ

Поле Тип По умолчанию Смысл
host строка | null null адрес устройства для ONVIF и PTZ; из rtsp_url не выводится
onvif_port целое | null null порт ONVIF, обычно 80
use_onvif bool | null false получать ссылки потоков с устройства
onvif_events_enabled bool | null false опрашивать события устройства
ptz_pan_tilt bool | null false сохраняется как use_onvif && ptz_pan_tilt
ptz_zoom bool | null false сохраняется как use_onvif && ptz_zoom
username строка | null "" логин устройства
password строка | null "" пароль устройства, хранится в восстановимом виде
make, model, serial, firmware строка | null null справочные поля инвентаризации
interval_timeout_secs целое | null null переопределение тайм-аута простоя
PTZ без ONVIF молча выключается

ptz_pan_tilt и ptz_zoom записываются как логическое «И» с use_onvif. Камера, созданная с use_onvif: false и ptz_pan_tilt: true, сохранится с ptz_pan_tilt: false — и никакой ошибки не будет. Управление такой камерой затем вернёт 400 ptz_not_supported.

Архив

Поле Тип По умолчанию Смысл
dvr_mode строка | null "none" none | duration | size
dvr_value целое 64 | null null секунды или байты; при режиме, отличном от none, обязателен и должен быть > 0
dvr_storage строка | null null имя дискового хранилища на выбранной ноде
dvr_copy_storage строка | null null имя S3-хранилища ноды для холодной копии
dvr_copy_bucket строка | null null бакет S3, 3–63 символа ограниченного алфавита
dvr_copy_mode строка | null "none" при включении должен совпадать с dvr_mode
dvr_copy_value целое 64 | null null должен быть не меньше dvr_value
dvr_prune_without_events bool | null false удалять фрагменты без событий
dvr_event_grace_seconds целое | null 1800 защитный интервал вокруг события, допустимо [300, 86400]

Отображение в мозаике

Поле Тип По умолчанию Смысл
mosaic_transport строка | null "mse-ws" mse-ws | hls
mosaic_profile строка | null "main" main | sub; значение sub молча заменяется на main, если профиля sub нет
mosaic_allow_switch bool | null true принудительно false, если профиля sub нет

Профили (profiles, обязательное поле, от 1 до 4 элементов, ровно один main)

Поле Тип По умолчанию Правила
kind строка обязательно маска ^[a-z0-9_]{1,16}$, уникален в наборе
source строка | null "manual" manual | onvif
rtsp_url строка | null null обязателен и непуст для manual; для onvif должен отсутствовать
onvif_profile_token строка | null null токен профиля ONVIF; может быть назначен позже разрешением адресов
width, height целое | null null справочные значения
codec строка | null null справочное значение
fps число | null null справочное значение

Туннель (tunnel, объект | null) — для камеры из закрытой сети: {agent_id, device_id, rtsp_lease_id, onvif_lease_id}. Требует use_onvif: true и непустых учётных данных. У такой камеры host и onvif_port перезаписываются адресом релея — то, что вы прислали, игнорируется. См. Камеры за NAT: туннели.

Пример curl — камера с двумя потоками, учётными данными и в полях, и в ссылке, с адресом устройства и недельным архивом:

curl -sS -X POST "$BASE/api/v1/cameras" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Вход, главный холл",
    "node_auto": true,
    "host": "192.0.2.10",
    "onvif_port": 80,
    "use_onvif": true,
    "username": "operator",
    "password": "ПАРОЛЬ-КАМЕРЫ",
    "dvr_mode": "duration",
    "dvr_value": 604800,
    "dvr_storage": "dvr1",
    "tags": ["<UUID>"],
    "profiles": [
      { "kind": "main", "source": "manual",
        "rtsp_url": "rtsp://operator:ПАРОЛЬ-КАМЕРЫ@192.0.2.10:554/stream1" },
      { "kind": "sub", "source": "manual",
        "rtsp_url": "rtsp://operator:ПАРОЛЬ-КАМЕРЫ@192.0.2.10:554/stream2" }
    ]
  }'
Камера создана с адресом
curl -sS "$BASE/api/v1/cameras?q=главный" -H "Authorization: Bearer $TOKEN" \
  | python3 -c '
import sys, json
for r in json.load(sys.stdin)["rows"]:
    print(r["name"], "host =", r["host"] or "(ПУСТО — ONVIF и PTZ работать не будут)")'

Ожидаемый вывод — host = 192.0.2.10. Если там (ПУСТО…), вы забыли поле host: доставьте его запросом PATCH.

Пример ответа201 Created, объект камеры плюс node_name, node_state, node_storage_names, profiles, tags:

{
  "id": "<UUID>", "org_id": "<UUID>", "node_id": "<UUID>",
  "name": "Вход, главный холл", "enabled": true,
  "host": "192.0.2.10", "onvif_port": 80, "use_onvif": true,
  "username": "operator",
  "dvr_mode": "duration", "dvr_value": 604800, "dvr_storage": "dvr1",
  "node_name": "node-1", "node_state": "active",
  "node_storage_names": ["dvr1", "dvr2"],
  "profiles": [
    { "id": "<UUID>", "kind": "main", "source": "manual",
      "rtsp_url": "rtsp://operator:ПАРОЛЬ-КАМЕРЫ@192.0.2.10:554/stream1",
      "mode": "always" },
    { "id": "<UUID>", "kind": "sub", "source": "manual",
      "rtsp_url": "rtsp://operator:ПАРОЛЬ-КАМЕРЫ@192.0.2.10:554/stream2",
      "mode": "on_demand" }
  ],
  "tags": ["<UUID>"]
}

Ошибки

Код Тело Причина
400 name_empty имя пустое после обрезки пробелов
400 camera_node_choice заданы и node_auto, и node_id — либо не задано ни одного
400 no_profiles, too_many_profiles, no_main_profile число профилей вне [1, 4] или нет профиля main
400 bad_profile_kind, duplicate_profile_kind, bad_profile_source kind не по маске, повторяется, либо неизвестный source
400 manual_needs_rtsp_url у профиля manual нет rtsp_url
400 onvif_has_no_rtsp_url у профиля onvif передан rtsp_url
400 bad_dvr_mode, dvr_value_required режим архива и его значение не согласованы
400 dvr_storage_without_dvr, dvr_node_required, dvr_storage_required хранилище указано без записи либо не указано при записи
400 dvr_storage_not_on_node, dvr_copy_storage_not_on_node названного хранилища на выбранной ноде нет
409 dvr_storage_provisioning_required хранилище на ноде есть, но ещё не подготовлено
400 bad_dvr_copy_mode, dvr_copy_requires_dvr, dvr_copy_fields_without_copy, dvr_copy_value_required, dvr_copy_mode_must_match_local, dvr_copy_retention_below_local, dvr_copy_storage_required, dvr_copy_bucket_required, bad_dvr_copy_bucket политика холодной копии несогласованна
400 event_prune_requires_dvr, bad_dvr_event_grace очистка по событиям включена без архива либо интервал вне [300, 86400]
400 bad_mosaic_transport, bad_mosaic_profile значения вне перечислений
400 bad_interval_timeout тайм-аут меньше допустимого минимума
400 unknown_tag тега нет в целевой организации
409 tag_limit больше 32 тегов
400 unknown_node ноды с таким идентификатором нет
409 node_not_active нода не в состоянии active либо отключена
409 no_eligible_placement автоподбор не нашёл подходящей ноды (или ноды с хранилищем, если включён архив)
403 node_not_allowed_for_org, storage_not_allowed_for_org нода или хранилище запрещены политикой организации
409 camera_limit достигнут лимит камер организации
409 organization_deleting организация удаляется или отключена
404 not_found целевой организации не существует
403 cross_org, not_admin недостаточно прав
400 tunnel_onvif_required, tunnel_credentials_required, invalid_tunnel_binding, tunnel_onvif_lease_required, invalid_tunnel_rtsp_lease, invalid_tunnel_onvif_lease параметры туннеля неверны
409 tunnel_device_already_added это устройство туннеля уже привязано к камере
400 camera_profiles_not_verified у камеры через туннель профили должны быть только onvif с подтверждённым устройством токеном
502 camera_no_usable_rtsp_profiles камера через туннель ответила по ONVIF, но пригодных потоков не отдала

Примечания

  • Если у организации режим размещения force_auto, автоподбор применяется даже при node_auto: false.
  • При достижении 80 % и 100 % лимита камер система создаёт служебное событие о квоте.
  • Логин и пароль из полей username/password нода подставляет в источник сама; вписанные прямо в rtsp_url учётные данные она оставляет как есть. Поле host не заполняется ни из того, ни из другого.

GET /api/v1/cameras/{id}#

Администратор — камера должна принадлежать доступной организации.

Назначение — полные сведения об одной камере, включая профили и теги.

Параметры путиid (uuid) — камера.

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>" -H "Authorization: Bearer $TOKEN"

Пример ответа — та же структура, что возвращает создание камеры.

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 cross_org камера в другой организации

Примечанияnode_storage_names равно null, пока нода ни разу не сообщила свой список хранилищ. Это не ошибка, а «данных ещё нет».

PATCH /api/v1/cameras/{id}#

Администратор

Назначение — частичное изменение камеры. Скалярные поля правятся на месте; profiles и tags, если они переданы, заменяют весь набор целиком.

Параметры путиid (uuid).

Тело запроса. Все поля необязательны, но ведут себя по-разному:

Пропуск оставляет значение; null не принимается как очистка: name, enabled, node_auto, node_id, use_onvif, onvif_events_enabled, ptz_pan_tilt, ptz_zoom, username, password, dvr_mode, dvr_copy_mode, dvr_prune_without_events, dvr_event_grace_seconds, mosaic_transport, mosaic_profile, mosaic_allow_switch, notes.

Пропуск оставляет значение; null очищает поле: host, onvif_port, make, model, serial, firmware, dvr_value, dvr_storage, dvr_copy_storage, dvr_copy_bucket, dvr_copy_value, interval_timeout_secs.

Замена набора целиком (пропуск оставляет как было): profiles — массив профилей, проверяется заново и по-прежнему должен содержать ровно один main; tags — массив идентификаторов, не более 32, пустой массив снимает все теги.

Полей org_id и tunnel в этом запросе нет: перенос камеры в другую организацию делается через POST /api/v1/cameras/{id}/copy, привязка туннеля — при создании.

Никогда не отправляйте обратно маскированный пароль

Поле password следует опускать, чтобы сохранить текущий пароль камеры — он не читается обратно и при пропуске остаётся прежним. Если интерфейс показал пароль как *** и это значение ушло в PATCH, в базу запишется буквальная строка ***, камера перестанет авторизовываться на устройстве, а настоящий пароль будет утрачен безвозвратно.

Отправляйте password только тогда, когда действительно меняете пароль.

Пример curl — сменить имя, добавить адрес устройства и очистить бакет холодной копии:

curl -sS -X PATCH "$BASE/api/v1/cameras/<UUID>" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Вход, главный холл (запад)",
    "host": "192.0.2.10",
    "dvr_copy_bucket": null
  }'

Пример ответа — обновлённая камера в той же структуре, что и при создании.

Ошибки — все проверки создания, а также:

Код Тело Причина
404 not_found камеры или организации больше нет
403 placement_force_auto в организации режим force_auto, а вы задаёте node_id или dvr_storage вручную
400 camera_node_choice node_auto: true вместе с node_id
409 camera_changed_retry строку изменили параллельно — повторите запрос
409 organization_deleting организация удаляется или отключена
409 dvr_cold_enabled_blocks_relocation нельзя переносить камеру при включённой холодной копии
409 dvr_local_cleanup_pending, dvr_cold_cleanup_pending предыдущее освобождение архива ещё не завершилось
409 dvr_cold_target_change_requires_disable смена хранилища или бакета холодной копии требует сначала её отключить

Примечания

  • dvr_mode: "none" сам очищает dvr_storage, сбрасывает dvr_prune_without_events в false и отключает всю политику холодной копии — обнулять эти поля вручную не нужно.
  • Приведение mosaic_profile/mosaic_allow_switch к наличию профиля sub, которое делается при создании, здесь не повторяется: значения проверяются только на принадлежность перечислению.
  • При замене набора профилей уже разрешённый адрес ONVIF сохраняется, если источник профиля не изменился, — удаление sub не «гасит» работающий main.
  • Каждое успешное изменение поднимает версию конфигурации и вызывает применение на ноде.

DELETE /api/v1/cameras/{id}#

Администратор

Назначение — удалить камеру. Сначала в надёжную очередь ставится удаление её записей архива, затем удаляется сама строка (профили, теги и ссылки доступа уходят каскадом), освобождается место в квоте организации и разбирается привязка туннеля, если она была.

Параметры путиid (uuid).

Пример curl

curl -sS -X DELETE "$BASE/api/v1/cameras/<UUID>" -H "Authorization: Bearer $TOKEN"

Пример ответа

{ "id": "<UUID>", "deleted": true }

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 cross_org камера в другой организации
409 camera_changed_retry камера переехала на другую ноду параллельно — повторите

Примечания

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

POST /api/v1/cameras/bulk#

Администратор — право на каждую камеру проверяется отдельно.

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

Тело запроса

Поле Тип Правила
ids массив uuid обязательно, непустой, не более 500; сортируется и дедуплицируется на сервере
action объект обязательно; вид действия задаёт поле kind

Варианты action:

kind Дополнительные поля
"delete" нет
"set_enabled" enabled (bool, обязательно)
"add_tags" tags (массив uuid, обязателен и непуст)
"remove_tags" tags (массив uuid, обязателен и непуст)

Пример curl — выключить три камеры:

curl -sS -X POST "$BASE/api/v1/cameras/bulk" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "ids": ["<UUID>", "<UUID>", "<UUID>"],
    "action": { "kind": "set_enabled", "enabled": false }
  }'

Пример ответа

{ "affected": 2 }

Ошибки

Код Тело Причина
400 bulk_ids_empty, bulk_ids_too_many список пуст либо длиннее 500
400 bulk_tags_empty действие с тегами без тегов
404 not_found любой идентификатор не существует — отклоняется весь пакет
403 cross_org любая камера вне доступных организаций — отклоняется весь пакет
403 not_admin роль оператора
409 organization_deleting затронутая организация удаляется (при delete не проверяется — удаление во время сноса разрешено)
409 camera_changed_retry камера переехала на другую ноду в процессе — повторите
409 bulk_delete_tunneled в выборке есть камера через туннель — удаляйте такие по одной
400 bulk_tags_multi_org выборка для операции с тегами охватывает больше одной организации
400 unknown_tag тега нет в этой организации
409 tag_limit хоть у одной камеры после добавления стало бы больше 32 тегов — отклоняется весь пакет

Примечания

  • affected — это число реально изменённых камер, а не размер ids. Камера, уже находившаяся в требуемом состоянии (уже выключена, уже с этим тегом), в счётчик не попадает. affected меньше длины списка — норма, а не признак частичного применения: частичного применения не бывает.
  • delete и set_enabled меняют конфигурацию нод и вызывают её применение; операции с тегами применение не вызывают — ноды теги не читают.
  • Каждая затронутая камера получает собственную запись аудита с пометкой о пакетном режиме.

POST /api/v1/cameras/{id}/copy#

Администратор — нужны права и на исходную организацию, и на организацию назначения.

Назначение — создать новую камеру, скопировав настройки существующей, при необходимости в другую организацию. Это единственная поддерживаемая межорганизационная операция с камерой.

Параметры путиid (uuid) — камера-источник.

Тело запроса

Поле Тип По умолчанию Смысл
org_id uuid обязательно организация назначения (может совпадать с исходной)
name строка обязательно имя новой камеры
node_auto bool false подобрать ноду автоматически
node_id uuid | null null конкретная нода
storage_auto bool false подобрать хранилище архива автоматически
storage строка | null null конкретное хранилище архива

Ровно одно из node_auto/node_id обязано быть задано. Если источник пишет архив, то ровно одно из storage_auto/storage тоже обязано быть задано; если источник архив не пишет, оба должны отсутствовать.

Пример curl

curl -sS -X POST "$BASE/api/v1/cameras/<UUID>/copy" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "org_id": "<UUID>",
    "name": "Вход, главный холл (копия)",
    "node_auto": true,
    "storage_auto": true
  }'

Пример ответа201 Created, новая камера в том же виде, что и при создании (обратите внимание: "enabled": false).

Ошибки — все ошибки создания камеры, а также:

Код Тело Причина
404 not_found нет камеры-источника или организации назначения
403 cross_org нет прав на источник или на назначение
400 name_empty пустое имя
400 copy_node_choice заданы оба способа выбора ноды либо ни одного
400 copy_storage_choice у пишущего источника заданы оба способа выбора хранилища либо ни одного
400 dvr_storage_without_dvr хранилище указано, хотя источник не пишет архив
409 no_eligible_placement подходящего размещения не нашлось

Примечания

  • Копируется: учётные данные (на стороне сервера, наружу они не отдаются), профили с их токенами ONVIF и метаданными, адрес, порт, справочные поля, режим и глубина архива, политика очистки по событиям, настройки мозаики, тайм-аут простоя, заметки, флаги ONVIF и PTZ.
  • Не копируется: теги (копия создаётся без тегов), события, история архива, ссылки доступа, привязка к туннелю и вся политика холодной копии S3 — она сбрасывается намеренно, чтобы новый арендатор не начал писать в чужой бакет.
  • Копия создаётся выключенной (enabled: false). Проверьте её и включите отдельным запросом.

GET /api/v1/cameras/lite#

Любой вошедший — оператор видит ровно те камеры, события которых ему разрешено читать (бит EVENTS); администратор организации — свою организацию; суперадмин — всю установку или одну организацию через ?org=.

Назначение — минимальный список камер для выпадающих фильтров. Существует потому, что административный список оператору недоступен.

Параметры запроса

Параметр Тип По умолчанию Смысл
org uuid сузить до организации (для суперадмина); чужая — 403 cross_org
q строка поиск по имени или по идентификатору как тексту; % и _ трактуются буквально
limit целое 500 прижимается к [1, 500]

Пример curl

curl -sS "$BASE/api/v1/cameras/lite?q=холл&limit=100" \
  -H "Authorization: Bearer $TOKEN"

Пример ответа

{ "rows": [ { "id": "<UUID>", "name": "Вход, главный холл" } ] }

Ошибки

Код Тело Причина
403 cross_org указана чужая организация
503 db_unavailable база недоступна

Примечания — сортировка по имени, затем по идентификатору; курсора нет, это одна ограниченная страница. В теле только id и name — ни адреса, ни ноды, ни учётных данных здесь появиться не может. Оператор без грантов получит {"rows":[]}.

GET /api/v1/cameras/{id}/credentials#

Администратор

Назначение — показать логин и пароль камеры в открытом виде. Это единственный маршрут, отдающий пароль.

Параметры путиid (uuid).

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/credentials" -H "Authorization: Bearer $TOKEN"

Пример ответа

{ "username": "operator", "password": "ПАРОЛЬ-КАМЕРЫ" }

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 cross_org камера в другой организации
Каждый вызов попадает в журнал аудита

Обращение к этому маршруту записывается в аудит вместе с пользователем, его адресом и камерой. Это действие «по осознанному нажатию», а не то, что интерфейс или интеграция дёргает при каждом обновлении экрана. Ответ считайте секретом: он единственное место, где пароль камеры покидает Plane.

Пароли камер хранятся в восстановимом виде намеренно — нода обязана предъявить их устройству. Пароли пользователей, в отличие от них, хранятся только в виде хеша и не читаются вообще никак.

Доступ к видео и архиву#

Три маршрута ниже не передают видео. Они возвращают дескриптор: с какой ноды забирать поток и какие элементы управления показывать. Медиа затем идёт из браузера прямо на ноду, а нода самостоятельно проверяет права ещё раз.

Чужая камера — это 404, а не 403

Для всех трёх маршрутов камера чужой организации даёт 404 not_found: Plane не подтверждает даже факт её существования. Внутри своей организации отказ по недостатку прав — уже 403 с конкретной причиной.

GET /api/v1/cameras/{id}/play#

Любой вошедший — принимается либо обычный токен сессии, либо токен ссылки общего доступа, выданной на эту камеру.

Назначение — дескриптор живого просмотра одной камеры.

Параметры путиid (uuid).

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/play" -H "Authorization: Bearer $TOKEN"

Пример ответа

{
  "position": 0,
  "camera_id": "<UUID>",
  "name": "Вход, главный холл",
  "sub_path": "cam/<UUID>/sub",
  "accessible": true,
  "live": true,
  "has_sub": true,
  "media_base": "https://node.example.com:8443",
  "availability": "ready",
  "main_on_demand": false,
  "sub_on_demand": true,
  "mosaic_transport": "mse-ws",
  "mosaic_profile": "sub",
  "mosaic_allow_switch": true,
  "tags": [],
  "ptz": false, "ptz_pan_tilt": false, "ptz_zoom": false, "ptz_set_preset": false,
  "events": true
}

Значения availability:

Значение Смысл
ready камера готова, поток можно открывать
disabled камера выключена администратором
node_offline нода не на связи
node_unavailable нода не активна либо у неё нет публичного адреса
preview_only прав хватает только на статичный кадр
locked доступ закрыт

Ошибки

Код Тело Причина
401 unauthorized ни живой сессии, ни действующей ссылки на эту камеру
404 not_found камеры нет либо она в другой организации
403 not_accessible камера своя, но нет ни бита LIVE, ни PREVIEW

Примечания

  • media_base непуст только когда live: true и камера в состоянии ready. Пустой media_base — это защита: клиенту незачем открывать соединение, на которое у него нет прав.
  • Вызывающий с одним лишь PREVIEW получает accessible: true, live: false, availability: "preview_only" и пустой media_base — то есть кадр, а не поток.
  • По ссылке общего доступа ptz всегда false: ссылка — медиа-принципал, она не может управлять камерой.
  • mosaic_allow_switch в ответе дополнительно ограничен наличием профиля sub, даже если в настройках камеры стоит true.
  • Поля ptz* и events — подсказки для интерфейса. Реальную проверку выполняют сами маршруты PTZ и событий.

GET /api/v1/cameras/{id}/archive#

Любой вошедший — сессия либо токен ссылки на эту камеру.

Назначение — дескриптор архива: где лежит таймлайн и какие элементы управления архивом показать.

Параметры путиid (uuid).

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/archive" -H "Authorization: Bearer $TOKEN"

Пример ответа

{
  "camera_id": "<UUID>",
  "name": "Вход, главный холл",
  "dvr_path": "cam/<UUID>/main",
  "media_base": "https://node.example.com:8443",
  "events": true,
  "export": false,
  "export_max_span_ms": 900000
}

Ошибки

Код Тело Причина
401 unauthorized нет ни сессии, ни действующей ссылки
404 not_found камеры нет либо она в другой организации
403 no_dvr камера своя, но бита DVR нет

Примечания

  • Три независимых права. DVR — пропуск: без него дескриптор не выдаётся вовсе. EVENTS включает только отметки событий на таймлайне. EXPORT включает только кнопку выгрузки. Обладатель одного лишь DVR смотрит архив без отметок и без выгрузки.
  • Архив всегда пишется с профиля maindvr_path всегда оканчивается на /main и никогда не указывает на sub.
  • Пустой media_base означает, что нода не активна или у неё нет публичного адреса. Показывайте честное «архив недоступен», а не повторяйте запрос в цикле.
  • export_max_span_ms — действующий предел длительности одного фрагмента, по умолчанию 900 000 мс (15 минут); настраивается на Plane в пределах от 60 секунд до 24 часов.

POST /api/v1/cameras/{id}/export#

Любой вошедшийплюс бит EXPORT на эту камеру. Токен ссылки общего доступа здесь не принимается, только сессия.

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

Параметры путиid (uuid).

Тело запроса

Поле Тип Смысл
from целое начало фрагмента, unix-миллисекунды
to целое конец фрагмента, unix-миллисекунды; строго больше from

Пример curl — последние 10 минут:

now=$(( $(date +%s) * 1000 ))
curl -sS -X POST "$BASE/api/v1/cameras/<UUID>/export" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{\"from\": $(( now - 600000 )), \"to\": $now}"

Пример ответа

{
  "url": "https://node.example.com:8443/cam/<UUID>/main/clip-1755000000-600.mp4",
  "from": 1755000000000,
  "to": 1755000600000
}

Ошибки

Код Тело Причина
400 bad_range to не больше from
400 range_too_long длительность превышает export_max_span_ms
404 not_found камеры нет либо она в другой организации
403 no_export камера своя, но бита EXPORT нет — право смотреть архив не даёт права его скачивать
400 camera_not_placed нода камеры не активна либо у неё нет публичного адреса
503 audit_unavailable не удалось записать аудит — выгрузка отклонена, а не выполнена без записи

Примечания

  • В возвращённом адресе нет учётных данных. Клиент обязан дописать свой токен: "$url?token=$TOKEN". Нода проверит бит EXPORT ещё раз, уже на самом файле.
  • Адрес строится с точностью до секунды (начало округляется вниз, длительность вверх, минимум 1 с), а в аудит попадают точные запрошенные миллисекунды. Готовый файл поэтому покрывает запрошенный интервал с запасом; нода дополнительно выравнивает его по опорным кадрам.
  • Проверка интервала выполняется до проверки прав и записи аудита, поэтому отклонённый по диапазону запрос записи в аудите не оставляет. В аудит попадает только разрешённая выгрузка.

Ссылки общего доступа#

Ссылка — это выпущенный Plane токен, который нода принимает как параметр ?token= при запросе медиа. Ссылка привязана к одной камере, ограниченному набору бит привилегий, необязательному списку разрешённых сайтов-источников и сетей, списку протоколов и сроку действия. Это только медиа-доступ: административный API по ней не работает, PTZ — тоже.

POST /api/v1/cameras/{id}/shares#

Администратор

Назначение — выпустить отзываемую ссылку на одну камеру. Токен возвращается ровно один раз.

Параметры путиid (uuid) — камера.

Тело запроса

Поле Тип По умолчанию Правила
name строка обязательно 1–128 символов, пробелы по краям обрезаются
bits целое обязательно запрашиваемая маска привилегий; лишние биты отсекаются, ноль недопустим
unlimited bool false выпустить бессрочную ссылку
ttl_secs целое | null null срок жизни в секундах, обязателен при unlimited: false; допустимо [60, 7776000] (от минуты до 90 суток)
allowed_protocols массив строк ["hls","mse"] от 1 до 3 значений из hls, mse, rtsp
allowed_referers массив строк [] до 32 имён сайтов, каждое не длиннее 253 символов; пустой список — без привязки к сайту
allowed_networks массив строк [] до 32 адресов или подсетей CIDR; каждый разбирается и приводится к канонической форме

unlimited и ttl_secs взаимоисключающи: допустимо либо unlimited: true без ttl_secs, либо unlimited: false с ttl_secs. Любая другая комбинация — 400 bad_ttl.

Пример curl — суточная ссылка на живой просмотр и превью (биты 1 + 32 = 33):

curl -sS -X POST "$BASE/api/v1/cameras/<UUID>/shares" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Подрядчик, монтаж",
    "bits": 33,
    "ttl_secs": 86400,
    "allowed_protocols": ["hls"],
    "allowed_referers": ["cctv.example.com"]
  }'

Пример ответа201 Created:

{
  "id": "<UUID>",
  "camera_id": "<UUID>",
  "name": "Подрядчик, монтаж",
  "bits": 33,
  "allowed_referers": ["cctv.example.com"],
  "allowed_protocols": ["hls"],
  "allowed_networks": [],
  "expires_at": "2026-08-19T10:00:00Z",
  "created_by": "<UUID>",
  "created_by_name": "Администратор",
  "created_at": "2026-08-18T10:00:00Z",
  "token": "eyJhbGciOi…",
  "live_url": "https://cctv.example.com/player/live/<UUID>?token=eyJhbGciOi…"
}

expires_at равен null у бессрочной ссылки. live_url и dvr_url вообще отсутствуют в ответе, если соответствующий бит не выдан или в списке протоколов нет ни одного браузерного (hls, mse).

Ошибки

Код Тело Причина
400 bad_name имя пустое или длиннее 128 символов
400 bad_ttl недопустимая комбинация unlimited/ttl_secs либо срок вне [60, 7776000]
400 no_bits после отсечения лишнего маска оказалась нулевой
403 bits_exceed_grant запрошены биты сверх допустимых (см. примечание)
400 dvr_requires_hls_or_mse запрошены архив или выгрузка, а в протоколах только rtsp
400 bad_protocols список протоколов пуст или длиннее 3
400 too_many_referers, bad_referer нарушены ограничения списка сайтов
400 too_many_networks, bad_network нарушены ограничения списка сетей либо адрес не разобран
400 unlimited_share_limit исчерпан лимит бессрочных ссылок: 64 на одну камеру либо 32 768 на всю установку
403 cross_org камера в другой организации
404 not_found камеры не существует
Токен показывается один раз, а права ограничены возможностями камеры

Токен есть только в ответе на создание. В базе хранится лишь идентификатор ссылки — он нужен для отзыва. Прочитать токен повторно нельзя ниоткуда: ни из списка ссылок, ни из журнала. Потеряли — выпускайте новую ссылку и отзывайте старую.

Права ссылки — пересечение двух ограничений: полномочий выпускающего на эту камеру и физических возможностей самой камеры. Всегда доступны LIVE, PREVIEW, EVENTS. DVR и EXPORT — только если камера пишет архив (dvr_mode отличен от none). PTZ — только если у камеры включён ONVIF. Даже суперадминистратор не может выпустить ссылку на архив камеры, которая не пишет: запрос будет отклонён с 403 bits_exceed_grant.

GET /api/v1/cameras/{id}/shares#

Администратор

Назначение — список действующих ссылок камеры (отозванные и просроченные не показываются).

Параметры путиid (uuid).

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/shares" -H "Authorization: Bearer $TOKEN"

Пример ответа

{
  "shares": [
    { "id": "<UUID>", "camera_id": "<UUID>", "name": "Подрядчик, монтаж",
      "bits": 33, "allowed_referers": ["cctv.example.com"],
      "allowed_protocols": ["hls"], "allowed_networks": [],
      "expires_at": "2026-08-19T10:00:00Z",
      "created_by": "<UUID>", "created_by_name": "Администратор",
      "created_at": "2026-08-18T10:00:00Z" }
  ]
}

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 cross_org камера в другой организации

Примечания — тот же набор полей, что при создании, без token, live_url и dvr_url. Сортировка — от новых к старым. Пагинации нет. created_by_name равен null, если выпустивший ссылку пользователь уже удалён.

DELETE /api/v1/shares/{id}#

Администратор

Назначение — немедленно отозвать ссылку.

Параметры путиid (uuid) — идентификатор ссылки (не камеры).

Пример curl

curl -sS -X DELETE "$BASE/api/v1/shares/<UUID>" -H "Authorization: Bearer $TOKEN"

Пример ответа

{ "ok": true }

Ошибки

Код Тело Причина
404 not_found ссылки не существует
403 cross_org ссылка относится к чужой организации
Отзыв действует не мгновенно на всех участках

Дескрипторы Plane (/play, /archive) перестают принимать токен сразу — они сверяются с базой. Ноды прекращают отдавать медиа в пределах своего цикла опроса списка отзыва. Планируйте небольшую задержку на медиа-тракте: если доступ нужно прекратить в ту же секунду, дополнительно выключите камеру.

ONVIF#

POST /api/v1/onvif/discover#

Администратор

Назначение — однократный поиск ONVIF-устройств в локальной сети и список тех, кто ответил, с пометкой, какие из них уже заведены как камеры.

Тело запроса — не требуется (передавайте пустое тело; параметры поиска встроенные).

Пример curl

curl -sS -X POST "$BASE/api/v1/onvif/discover" -H "Authorization: Bearer $TOKEN"

Пример ответа

{
  "devices": [
    {
      "uuid": "urn:uuid:…",
      "xaddrs": ["http://192.0.2.10/onvif/device_service"],
      "scopes": ["onvif://www.onvif.org/name/Cam1"],
      "host": "192.0.2.10", "port": 80,
      "name": "Cam1", "hardware": "…",
      "known": false
    }
  ]
}

known: true означает, что камера с таким адресом уже есть в доступных вызывающему организациях.

Ошибки

Код Тело Причина
400 not_onvif, no_host, no_onvif_profiles устройство ответило не по протоколу либо без пригодных данных
502 onvif_upstream отказ со стороны устройства, см. ниже
503 db_unavailable база недоступна

Примечания — поиск ограничен по времени и по числу устройств, поэтому «болтливый» ответчик не переполнит выдачу. Ищутся только устройства в той же сети, что и Plane: за маршрутизатором многоадресный запрос не проходит — такие камеры заводите вручную или через туннель.

POST /api/v1/onvif/inspect#

Администратор

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

Тело запроса

Поле Тип Обязательное
host строка да
port целое | null нет
username строка да
password строка да

Пример curl

curl -sS -X POST "$BASE/api/v1/onvif/inspect" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"host":"192.0.2.10","port":80,"username":"operator","password":"ПАРОЛЬ-КАМЕРЫ"}'

Пример ответа

{
  "profiles": [
    { "token": "Profile_1", "name": "MainStream",
      "width": 1920, "height": 1080, "codec": "H264", "fps": 25.0,
      "stream_uri": "rtsp://192.0.2.10:554/…" }
  ],
  "device": { "manufacturer": "…", "model": "…", "firmware_version": "…",
              "serial_number": "…", "hardware_id": "…" }
}

stream_uri равен null, если устройство не отдало адрес потока.

Ошибки

Код Тело Причина
400 not_onvif, no_host, no_onvif_profiles ответ не соответствует протоколу либо профилей нет
502 onvif_upstream отказ со стороны устройства, см. ниже
503 db_unavailable база недоступна

Примечания

  • Переданные учётные данные используются для одного запроса и не сохраняются и не пишутся в журналы. Возвращённые stream_uri — «голые» адреса без учётных данных; подстановка происходит уже после сохранения камеры.
  • У этого маршрута нет привязки к организации и нет списка разрешённых адресов. Любой администратор организации может заставить Plane обратиться к любому хосту, до которого Plane дотягивается по сети. Это административная возможность, а не безопасный для арендаторов справочник — учитывайте это, раздавая роль администратора организации.

GET /api/v1/cameras/{id}/onvif/profiles#

Администратор

Назначение — получить с сохранённой камеры её текущий список профилей ONVIF, используя сохранённые учётные данные. Ничего не записывает.

Параметры путиid (uuid).

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/onvif/profiles" -H "Authorization: Bearer $TOKEN"

Пример ответа

{ "profiles": [ { "token": "Profile_1", "name": "MainStream",
                  "width": 1920, "height": 1080, "codec": "H264", "fps": 25.0 } ] }

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 cross_org камера в другой организации
400 not_onvif у камеры use_onvif: false
400 no_host у камеры не задан host
502 onvif_upstream отказ со стороны устройства, см. ниже
503 db_unavailable база недоступна

Примечания — пароль камеры читается внутри Plane для авторизации запроса к устройству, но клиенту не отдаётся и записью «просмотр пароля» в аудите не считается.

POST /api/v1/cameras/{id}/onvif/resolve#

Администратор

Назначение — опросить камеру и записать результат в её профили: назначить токены ONVIF там, где их не было (самое высокое разрешение — в main, самое низкое — в sub), и сохранить адреса потоков. После этого конфигурация применяется на ноде.

Параметры путиid (uuid).

Тело запроса — не требуется.

Пример curl

curl -sS -X POST "$BASE/api/v1/cameras/<UUID>/onvif/resolve" -H "Authorization: Bearer $TOKEN"

Пример ответа — обновлённая камера в той же структуре, что и при создании; у профилей ONVIF теперь заполнен onvif_stream_uri.

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 cross_org камера в другой организации
400 not_onvif у камеры use_onvif: false
400 no_host у камеры не задан host
400 no_onvif_profiles устройство не отдало ни одного пригодного профиля
502 onvif_upstream отказ со стороны устройства, см. ниже
503 db_unavailable база недоступна

Примечания — операция пишется в аудит; при неуспешной авторизации на устройстве дополнительно создаётся служебное событие об ошибке авторизации камеры. Пока разрешение адресов не выполнено, профиль onvif не стримит — заведение ONVIF-камеры без этого шага незаметно оставляет её без видео.

Отказ со стороны камеры: 502 onvif_upstream#

Любой маршрут ONVIF и PTZ может завершиться неудачей не на стороне Plane, а на стороне устройства. Такой случай — это 502 с расширенным телом:

{
  "error": "onvif_upstream",
  "detail": "camera_auth_failed",
  "kind": "…",
  "retry_after_seconds": 30
}
detail Смысл Что делать
camera_unreachable устройство не отвечает проверьте сеть, питание, адрес и порт
camera_auth_failed устройство отвергло логин или пароль исправьте учётные данные камеры
camera_auth_cooldown Plane сам придерживает повторные попытки после серии отказов подождите retry_after_seconds
camera_device_locked устройство заблокировало учётную запись подождите retry_after_seconds, при необходимости разблокируйте на самой камере

Текст ошибки от устройства наружу намеренно не передаётся. retry_after_seconds равен null, если подсказки о задержке нет; когда значение есть — блокируйте кнопку повтора ровно на этот срок, иначе попытки будут продлевать блокировку устройства.

PTZ#

POST /api/v1/cameras/{id}/ptz#

Любой вошедшийплюс право PTZ на эту камеру: камера должна заявлять ptz_pan_tilt или ptz_zoom; администратору достаточно прав на организацию камеры; оператору нужен бит PTZ на метке, которую камера несёт (её организация или один из её тегов). Операция set_preset дополнительно требует роли администратора.

Назначение — управление камерой: непрерывное движение, остановка, переход к предустановке, создание предустановки.

Параметры путиid (uuid).

Тело запроса — вид операции задаёт поле op:

op Поля Примечание
"move" pan, tilt, zoom — числа, по умолчанию 0.0 значения ограничиваются диапазоном [-1, 1]; пропущенная ось означает «не двигать»
"stop" нет немедленная остановка
"goto_preset" preset — строка, обязательно токен предустановки, полученный из списка
"set_preset" name — строка, обязательно только для администраторов

Пример curl — плавный поворот вправо, затем остановка:

curl -sS -X POST "$BASE/api/v1/cameras/<UUID>/ptz" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"op":"move","pan":0.4}'

curl -sS -X POST "$BASE/api/v1/cameras/<UUID>/ptz" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"op":"stop"}'

Пример ответа

{ "ok": true }

Для set_preset дополнительно возвращается токен, который назначила камера:

{ "ok": true, "token": "Preset_3" }

Ошибки

Код Тело Причина
404 not_found камеры не существует
403 forbidden нет права PTZ на эту камеру
403 ptz_set_preset_admin_only оператор с битом PTZ пытается создать предустановку
400 not_onvif у камеры use_onvif: false
400 ptz_not_supported не заявлены ни поворот, ни зум
400 ptz_pan_tilt_not_supported ненулевые pan/tilt у камеры, умеющей только зум
400 ptz_zoom_not_supported ненулевой zoom у камеры, умеющей только поворот
400 no_host у камеры не задан host
400 ptz_not_resolved ещё не разрешён токен профиля ONVIF — выполните …/onvif/resolve
502 onvif_upstream отказ со стороны устройства
503 db_unavailable база недоступна

Примечания

  • Команда move взводит сторожевой таймер на 2 секунды. Если новых команд не приходит, Plane сам отправляет камере Stop (до трёх попыток). Новая move отменяет отложенную остановку. Клиент, ведущий камеру джойстиком, обязан повторять move чаще, чем раз в 2 секунды; отпускание джойстика лучше подтверждать явной stop, а не полагаться на таймер.
  • Записи аудита по PTZ схлопываются: подряд идущие действия одного пользователя по одной камере с промежутком менее 30 секунд превращаются в одну запись.
  • Возможности PTZ берутся из полей камеры и записываются с учётом use_onvif, поэтому камера, сохранённая без ONVIF, не может стать PTZ-управляемой, что бы ни стояло в её флагах.

GET /api/v1/cameras/{id}/ptz/presets#

Любой вошедший — те же права PTZ, что и у команды управления; дополнительной проверки на администратора нет.

Назначение — список предустановок камеры. Кэша на стороне Plane нет: список запрашивается у самой камеры.

Параметры путиid (uuid).

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/ptz/presets" -H "Authorization: Bearer $TOKEN"

Пример ответа

{ "presets": [ { "token": "Preset_1", "name": "Вход" },
               { "token": "Preset_2", "name": null } ] }

Ошибки — те же, что у команды управления, кроме ошибок конкретных операций: 404 not_found, 403 forbidden, 400 not_onvif, 400 ptz_not_supported, 400 no_host, 400 ptz_not_resolved, 502 onvif_upstream, 503 db_unavailable.

Примечанияname равен null, если камера имени не сообщает. Токены непрозрачны: не считайте их числами и не полагайтесь на их порядок — передавайте обратно ровно то, что получили, в поле preset операции goto_preset.

Телеметрия и статус в реальном времени#

GET /api/v1/cameras/{id}/telemetry/history#

Администратор

Назначение — временной ряд метрик одного профиля камеры (графики на карточке камеры). Plane определяет размещение и обращается к ноде сам; адрес и учётные данные ноды до браузера не доходят.

Параметры путиid (uuid) — камера.

Параметры запроса

Параметр Тип По умолчанию Смысл
profile строка обязательно вид профиля, например main
range строка "1m" глубина: 1m, 5m, 1h, 24h

Пример curl

curl -sS "$BASE/api/v1/cameras/<UUID>/telemetry/history?profile=main&range=1h" \
  -H "Authorization: Bearer $TOKEN"

Пример ответа

{
  "id": "cam/<UUID>/main", "core": 3, "started_ms": 1755000000000,
  "range": "1m", "cadence_s": 1, "cap": 60,
  "samples": [
    { "ts": 1755000000, "bitrate": 2048, "fps": 25.0, "viewers": 3, "packets_in_per_s": 500 }
  ]
}

Ошибки

Код Тело Причина
400 bad_profile_kind profile не соответствует маске вида профиля
400 bad_telemetry_range range вне четырёх допустимых значений
404 camera_profile_not_running камера вне доступной области, такого профиля нет либо нода его не знает
503 node_unavailable нода недоступна или у неё нет адреса управления
502 node_bad_reply нода вернула не тот формат

GET /api/v1/cameras/runtime/stream#

Администратор — операторы пользуются мобильным вариантом этого потока.

Назначение — поток SSE с состоянием набора камер в реальном времени. Авторизация и определение размещения выполняются один раз при подписке; все подписчики затем обслуживаются одним общим опросом нод.

Параметры запроса

Параметр Тип По умолчанию Смысл
ids строка обязательно идентификаторы камер через запятую; пустые элементы пропускаются, повторы убираются; после дедупликации от 1 до 50
detail bool false добавить подробности по профилям; требует ровно одного идентификатора

Пример curl

curl -N -sS "$BASE/api/v1/cameras/runtime/stream?ids=<UUID>,<UUID>" \
  -H "Authorization: Bearer $TOKEN" -H 'Accept: text/event-stream'

Пример ответаtext/event-stream; кадры без имени события, с id: <номер> и комментарием поддержания соединения каждые 15 секунд:

id: 42
data: {"seq":42,"sampled_at":"2026-08-18T10:00:00Z","rows":{"<UUID>":{"alive":true,"profiles":2,"observed_profiles":2,"bitrate":2048.0,"fps":25.0,"packets_in_per_s":500.0,"codecs":["h264"],"errors":[],"health":"ok","profile_states":{"main":{"kind":"main","enabled":true,"observed":true,"alive":true,"on_demand":false,"bitrate":2048.0,"fps":25.0,"codecs":["h264"],"errors":[],"health":"ok","source_connected_ms":1755000000000,"reconnects":0}}}},"profiles":{}}

Поле profiles заполняется только при detail=true, иначе это пустой объект.

Ошибки

Код Тело Причина
400 bad_camera_id один из элементов ids не является идентификатором
400 camera_batch_must_be_1_to_50 ноль идентификаторов либо больше 50 после дедупликации
400 camera_detail_requires_one_camera detail=true при количестве идентификаторов, отличном от одного
403 camera_not_accessible ни один идентификатор не разрешился в доступной области
429 {"error":"live_stream_capacity","scope":"global"|"principal","retry_after":N} исчерпаны слоты потоков; смотрите заголовок Retry-After
429 too_many_runtime_subscriptions, runtime_camera_demand_exhausted переполнены таблицы подписок

Примечания

  • Неизвестные и недоступные идентификаторы молча выбрасываются из подписки, если разрешился хотя бы один: поток тогда охватывает меньше камер, чем вы просили. Сверяйте состав rows с тем, что запрашивали.
  • Слот подписки удерживается всё время, пока открыто тело ответа: клиент, который открыл поток и не читает его, продолжает занимать слот. Закрывайте соединение явно.
  • Поток завершается, когда истекает актуальность авторизации (не более 5 минут). Долгоживущая страница обязана переподключаться. Общие правила разбора кадров — в разделе Введение.

Шаблоны камер#

Шаблон камеры — это сохранённый набор настроек, который подставляется при создании камеры (профили, параметры архива, размещение). Полное описание маршрутов /api/v1/camera-templates — на странице Теги, мозаики, шаблоны: там же объясняется, чем шаблон камеры отличается от шаблона события.