Камеры
Создание и изменение камер, массовые операции, ONVIF, PTZ, доступ к видео и архиву.
Камера — центральный объект системы: она принадлежит организации, размещена на ноде, несёт один или несколько профилей (потоков), может писать архив, отдаваться операторам и раздаваться по ссылке. Здесь собраны все маршруты, работающие с камерами, включая ONVIF, PTZ и шаблоны камер.
Все запросы требуют заголовка Authorization: Bearer $TOKEN. Базовый URL, получение токена, общий
формат ошибок и правила пагинации — в разделе Введение и аутентификация.
В примерах используются переменные BASE=https://cctv.example.com и TOKEN, а адрес камеры показан
как 192.0.2.10 — это адрес из диапазона, зарезервированного под документацию; подставьте свой.
Что нужно знать до первого запроса#
host — самостоятельное поле камеры, и Plane никогда не извлекает его из rtsp_url
(это проверено по коду). Если завести камеру через API и не передать host, столбец «Адрес» в
админке останется пустым, а ONVIF и PTZ по этой камере работать не смогут: им нужен адрес
устройства, а не ссылка на поток.
Передавайте host (и при необходимости onvif_port) всегда, даже когда ссылка RTSP уже
содержит тот же адрес. Пример создания ниже задаёт и то, и другое.
Логин и пароль устройства можно задать двумя способами, и они не связаны между собой:
- Поля
usernameиpasswordкамеры — нода подставит их в источник самостоятельно. - Прямо в
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— вспомогательный (для мозаик и превью).source—manual(ссылка задана руками) илиonvif(ссылку получает Plane с устройства).modeвычисляется, а не задаётся: уmainвсегдаalways, у остальных —on_demand(поток поднимается только когда кто-то смотрит).onvif_stream_uriзаполняется после успешногоPOST /api/v1/cameras/{id}/onvif/resolve.
Пока 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_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 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смотрит архив без отметок и без выгрузки. - Архив всегда пишется с профиля
main—dvr_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 — на странице
Теги, мозаики, шаблоны: там же объясняется, чем шаблон камеры
отличается от шаблона события.