4. Cameras
Раздел описывает камеры наблюдения, используемые системой как источник точных данных о занятости парковочных зон.
4.1 Совместимость с текущей реализацией
Раздел сохраняет существующие основные эндпоинты:
GET /camerasPOST /cameras/newGET /cameras/<camera_id>PUT /cameras/<camera_id>DELETE /cameras/<camera_id>GET /cameras/nextGET /cameras/<camera_id>/snapshot
Для сохранения совместимости:
- существующие обязательные поля камеры сохранены;
- существующие пути эндпоинтов сохранены;
- новые поля добавлены как дополнительные;
- клиенты, которые не используют новые параметры, могут продолжать работать по прежней схеме;
- live-кадр без query-параметров по-прежнему читается из источника камеры;
- сохранённые кадры детекций загружаются из S3, проверяются и расшифровываются API в памяти;
- прямые и presigned-ссылки S3 клиенту не возвращаются.
4.2 Общие правила
Авторизация
Все эндпоинты раздела требуют авторизации.
Authorization: Bearer <access_token>
Модель доступа
Для эндпоинтов раздела используются следующие разрешения:
cameras.view— просмотр камер;cameras.create— создание камер;cameras.update— изменение камер;cameras.delete— удаление камер.
Для всех режимов GET /cameras/<camera_id>/snapshot текущая реализация требует
cameras.view.
Правила видимости данных
Администратор видит все камеры. Остальные пользователи видят только камеры активных партнёров, в которых состоят; недоступная камера возвращается как 404.
Общие ошибки
| Код | Тип | Описание |
|---|---|---|
| 400 | Bad Request | Невалидный JSON или неверные типы полей. |
| 401 | Unauthorized | Токен отсутствует, невалиден, истёк или сессия завершена. |
| 403 | Forbidden | У пользователя недостаточно прав для выполнения действия. |
| 404 | Not Found | Камера не найдена. |
| 409 | Conflict | Конфликт данных, например камера с таким title уже существует. |
| 415 | Unsupported Media Type | Неверный Content-Type. Используй application/json. |
| 422 | Unprocessable Entity | Ошибка валидации. |
| 500 | Internal Server Error | Необработанная ошибка сервера. |
| 502 | Bad Gateway | Сохранённый объект повреждён или не соответствует детекции. |
| 503 | Service Unavailable | Сервис или S3-хранилище временно недоступны. |
Пример ответа:
{
"error_description": "Unsupported Media Type: expected application/json"
}
4.3 Формат данных
Время
Все временные значения передаются в формате UTC ISO 8601:
2026-04-06T10:00:00Z
Калибровка камеры
Поле calib хранит вложенный JSON-объект с параметрами калибровки, коррекции и вытягивания изображения.
Поле:
- может быть
null; - сохраняется и возвращается сервером как JSON;
- не нормализуется сервером по отдельным подполям в рамках MVP.
4.4 Модель Camera
Объект камеры в ответах API.
Базовые поля
camera_id(integer) — уникальный идентификатор камеры.title(string) — человекочитаемое название или описание камеры.source(string) — URL видеопотока или строка подключения (rtsp://...,.m3u8и т.п.).image_width(integer) — ширина изображения видеопотока в пикселях.image_height(integer) — высота изображения видеопотока в пикселях.calib(json | null) — параметры калибровки камеры.latitude(float,-90..90) — широта камеры.longitude(float,-180..180) — долгота камеры.created_at(string, ISO 8601) — дата создания.updated_at(string, ISO 8601) — дата последнего обновления.
Дополнительные поля
partner_id(integer | null) — идентификатор партнёрской организации-владельца камеры.created_by_user_id(integer | null) — идентификатор пользователя, создавшего камеру.is_active(boolean) — активна ли камера.last_snapshot_at(string | null, ISO 8601) — время последнего снапшота.
Поля
partner_id,created_by_user_idиis_activeявляются дополнительными и не обязательны для старых клиентов.
Пример
{
"camera_id": 1,
"title": "Кронверкский просп., парковка напротив ИТМО",
"source": "rtsp://...",
"image_width": 1920,
"image_height": 1080,
"calib": {
"image_width": 1920,
"image_height": 1080,
"K": [
[1739.237279181759, 0.0, 947.5335576199107],
[0.0, 2244.705015334057, 564.6946579168148],
[0.0, 0.0, 1.0]
],
"D": [-0.37062084436192333, 0.05057465862770827, 0.033198096980616335, 0.012812747166936252],
"balance": 0.0,
"model": "opencv_fisheye_k1k2k3k4"
},
"latitude": 59.955976,
"longitude": 30.309426,
"partner_id": 10,
"created_by_user_id": 123,
"is_active": true,
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T10:30:00Z"
}
4.5 Модель CameraMapItem
Облегчённая модель камеры для отображения на карте.
camera_id(integer) — уникальный идентификатор камеры.title(string) — название камеры.latitude(float) — широта камеры.longitude(float) — долгота камеры.partner_id(integer | null) — идентификатор партнёра-владельца.is_active(boolean) — активна ли камера.
Пример
{
"camera_id": 1,
"title": "Кронверкский просп., парковка напротив ИТМО",
"latitude": 59.955976,
"longitude": 30.309426,
"partner_id": 10,
"is_active": true
}
4.6 GET /cameras
Возвращает список камер, доступных текущему пользователю.
Требуемые разрешения
cameras.view
Query-параметры (необязательные)
q(string) — фильтр по подстроке вtitle.partner_id(integer) — фильтр по партнёру-владельцу камеры.is_active(boolean) — фильтр по активности камеры.bbox(string) — пространственный фильтр в формате<min_longitude>,<min_latitude>,<max_longitude>,<max_latitude>.view(string) — режим ответа. Поддерживаемые значения:full— полная модель камеры (по умолчанию);map— облегчённая модель для отображения на карте.
Примеры bbox
30.30,59.92,30.35,59.97
Параметр
bboxрекомендуется использовать при загрузке камер для карты, чтобы не запрашивать все камеры сразу.
Пример запроса — полная модель
GET /api/v1/cameras?q=просп HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Пример запроса — режим карты
GET /api/v1/cameras?bbox=30.30,59.92,30.35,59.97&view=map HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Response (200)
- при
view=full— массив объектовCamera; - при
view=map— массив объектовCameraMapItem.
Пример ответа (200) — view=full
[
{
"camera_id": 1,
"title": "Кронверкский просп., парковка напротив ИТМО",
"source": "rtsp://...",
"image_width": 1920,
"image_height": 1080,
"calib": {
"image_width": 1920,
"image_height": 1080,
"K": [
[1739.237279181759, 0.0, 947.5335576199107],
[0.0, 2244.705015334057, 564.6946579168148],
[0.0, 0.0, 1.0]
],
"D": [-0.37062084436192333, 0.05057465862770827, 0.033198096980616335, 0.012812747166936252],
"balance": 0.0,
"model": "opencv_fisheye_k1k2k3k4"
},
"latitude": 59.955976,
"longitude": 30.309426,
"partner_id": 10,
"created_by_user_id": 123,
"is_active": true,
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T10:30:00Z"
}
]
Пример ответа (200) — view=map
[
{
"camera_id": 1,
"title": "Кронверкский просп., парковка напротив ИТМО",
"latitude": 59.955976,
"longitude": 30.309426,
"partner_id": 10,
"is_active": true
},
{
"camera_id": 2,
"title": "Ломоносова, 9 — двор",
"latitude": 59.927366,
"longitude": 30.338487,
"partner_id": 10,
"is_active": true
}
]
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 401 | Unauthorized | Токен отсутствует, невалиден или истёк. |
| 403 | Forbidden | Недостаточно прав для просмотра камер. |
| 422 | Unprocessable Entity | Валидация не пройдена, например bbox имеет неверный формат или диапазон. |
4.7 POST /cameras/new
Создаёт новую камеру.
Эндпоинт сохранён без изменения пути для совместимости с текущим кодом.
Требуемые разрешения
cameras.create
Для не-администратора partner_id должен принадлежать одному из его активных партнёров. Если активных партнёров несколько, partner_id обязателен.
Headers
Authorization: Bearer <access_token>Content-Type: application/json
Request body (required)
Базовые поля
title(string, 1..200) — описание камеры.source(string) — URL видеопотока или строка подключения.image_width(integer) — ширина изображения.image_height(integer) — высота изображения.calib(json | null) — калибровка камеры.latitude(float,-90..90) — широта камеры.longitude(float,-180..180) — долгота камеры.
Дополнительные поля
partner_id(integer, optional) — партнёр-владелец камеры.
Если
partner_idне передан, сервер может определить владельца по текущему контексту доступа пользователя.
Пример запроса
POST /api/v1/cameras/new HTTP/1.1
Host: api.parktrack.live
Content-Type: application/json
Authorization: Bearer <token>
{
"title": "Кронверкский просп., парковка напротив ИТМО",
"source": "https://example.com/stream.m3u8",
"image_width": 1920,
"image_height": 1080,
"calib": null,
"latitude": 59.955976,
"longitude": 30.309426,
"partner_id": 10
}
Response (201)
camera_id(integer) — идентификатор созданной камеры.
Пример ответа (201)
{
"camera_id": 1
}
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 400 | Bad Request | Невалидное тело запроса. |
| 401 | Unauthorized | Токен отсутствует, невалиден или истёк. |
| 403 | Forbidden | Недостаточно прав для создания камеры. |
| 404 | Not Found | Указанный partner_id не найден. |
| 409 | Conflict | Камера с таким title уже существует. |
| 415 | Unsupported Media Type | Неверный Content-Type. |
| 422 | Unprocessable Entity | Ошибка валидации, например title пустой или координаты вне диапазона. |
Пример ответа (409)
{
"error_description": "Camera with this title already exists"
}
4.8 GET /cameras/<camera_id>
Возвращает подробную информацию о камере.
Path-параметры
camera_id(integer, required) — идентификатор камеры.
Требуемые разрешения
cameras.view
Headers
Authorization: Bearer <access_token>
Пример запроса
GET /api/v1/cameras/1 HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Response (200) — объект Camera
Пример ответа (200)
{
"camera_id": 1,
"title": "Кронверкский просп., парковка напротив ИТМО",
"source": "https://example.com/stream.m3u8",
"image_width": 1920,
"image_height": 1080,
"calib": null,
"latitude": 59.955976,
"longitude": 30.309426,
"partner_id": 10,
"created_by_user_id": 123,
"is_active": true,
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T10:10:00Z"
}
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 401 | Unauthorized | Токен отсутствует, невалиден или истёк. |
| 403 | Forbidden | Недостаточно прав для просмотра камеры. |
| 404 | Not Found | Камера не найдена. |
4.9 PUT /cameras/<camera_id>
Обновляет данные камеры.
Допускается частичное обновление.
Path-параметры
camera_id(integer, required) — идентификатор камеры.
Требуемые разрешения
cameras.update
Headers
Authorization: Bearer <access_token>Content-Type: application/json
Request body
title(string, 1..200, optional) — описание камеры.source(string, optional) — URL видеопотока или строка подключения.image_width(integer, optional) — ширина изображения.image_height(integer, optional) — высота изображения.calib(json | null, optional) — калибровка камеры.latitude(float, optional) — широта камеры.longitude(float, optional) — долгота камеры.is_active(boolean, optional) — активность камеры.
Поля
camera_id,created_at,updated_at,created_by_user_idизменяются сервером и игнорируются в теле запроса.
Пример запроса
PUT /api/v1/cameras/1 HTTP/1.1
Host: api.parktrack.live
Content-Type: application/json
Authorization: Bearer <token>
{
"title": "Кронверкский просп., камера №1 (в сторону парка)",
"latitude": 59.955980,
"longitude": 30.309430
}
Пример запроса — частичное обновление калибровки
PUT /api/v1/cameras/1 HTTP/1.1
Host: api.parktrack.live
Content-Type: application/json
Authorization: Bearer <token>
{
"calib": {
"image_width": 1920,
"image_height": 1080,
"model": "opencv_fisheye_k1k2k3k4"
}
}
Response (200) — объект Camera
Пример ответа (200)
{
"camera_id": 1,
"title": "Кронверкский просп., камера №1 (в сторону парка)",
"source": "https://example.com/stream.m3u8",
"image_width": 1920,
"image_height": 1080,
"calib": null,
"latitude": 59.95598,
"longitude": 30.30943,
"partner_id": 10,
"created_by_user_id": 123,
"is_active": true,
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T12:00:00Z"
}
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 400 | Bad Request | Невалидное тело запроса. |
| 401 | Unauthorized | Токен отсутствует, невалиден или истёк. |
| 403 | Forbidden | Недостаточно прав для изменения камеры. |
| 404 | Not Found | Камера не найдена. |
| 422 | Unprocessable Entity | Ошибка валидации, например title пустой или координаты вне диапазона. |
4.10 DELETE /cameras/<camera_id>
Удаляет камеру.
Эндпоинт сохранён для совместимости с текущей реализацией.
Path-параметры
camera_id(integer, required) — идентификатор камеры.
Требуемые разрешения
cameras.delete
Headers
Authorization: Bearer <access_token>
Response (204) Тело ответа отсутствует.
Пример запроса
DELETE /api/v1/cameras/1 HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 401 | Unauthorized | Токен отсутствует, невалиден или истёк. |
| 403 | Forbidden | Недостаточно прав для удаления камеры. |
| 404 | Not Found | Камера не найдена. |
| 409 | Conflict | Удаление невозможно из-за активных зависимостей, если сервер не выполняет каскадное удаление. |
Пример ответа (404)
{
"error_description": "Camera not found"
}
4.11 GET /cameras/next
Возвращает следующую камеру для очередного цикла обработки.
Эндпоинт предназначен для внутренних сервисов обработки видеопотока и совместим с текущей реализацией.
Требуемые разрешения
cameras.view
Headers
Authorization: Bearer <access_token>
Response (200)
Минимальный объект камеры для обработки:
camera_id(integer)source(string)image_width(integer)image_height(integer)calib(json | null)
Сервер может дополнительно вернуть:
partner_id(integer | null)is_active(boolean)
Пример запроса
GET /api/v1/cameras/next HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Пример ответа (200)
{
"camera_id": 1,
"source": "https://example.com/stream.m3u8",
"image_width": 1920,
"image_height": 1080,
"calib": null,
"partner_id": 10,
"is_active": true
}
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 401 | Unauthorized | Токен отсутствует или невалиден. |
| 403 | Forbidden | Недостаточно прав. |
| 404 | Not Found | Нет активных камер. |
Пример ответа (404)
{
"error_description": "No cameras added"
}
4.12 GET /cameras/<camera_id>/snapshot
Возвращает live-кадр камеры либо сохранённый кадр конкретной детекции.
Без query-параметров сервер пытается получить новый кадр из source камеры.
Сохранённые raw- и annotated-кадры загружаются из S3 в зашифрованном формате
PTSNAP01, проходят проверку AES-256-GCM и расшифровываются только в памяти API.
Path-параметры
camera_id(integer, required) — идентификатор камеры.
Требуемые разрешения
cameras.view
Headers
Authorization: Bearer <access_token>
Query-параметры (необязательные)
annotated(boolean, defaultfalse) — вернуть сохранённый кадр с разметкой.last_detection(boolean, defaultfalse) — приannotated=falseвернуть последний сохранённыйraw-кадр детекции вместо live-кадра.fallback_to_raw(boolean, defaultfalse) — приannotated=trueразрешить возвратraw, еслиannotatedдля выбранной детекции отсутствует.detection_run_id(integer >= 1) — выбрать конкретное распознавание этой камеры вместо последнего. Приannotated=falseвозвращаетсяraw, приannotated=true—annotatedлибоraw, если разрешён fallback.
Приоритет выбора:
- Если указан
detection_run_id, выбирается только это распознавание. - Иначе при
annotated=trueвыбирается последнее распознавание с подходящим кадром. - Иначе при
last_detection=trueвыбирается последний сохранённыйraw. - Во всех остальных случаях возвращается новый live-кадр из источника камеры.
Пример — live-кадр
GET /api/v1/cameras/1/snapshot HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Пример — последний сохранённый кадр с разметкой
GET /api/v1/cameras/1/snapshot?annotated=true&fallback_to_raw=true HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Пример — исходный кадр конкретной детекции
GET /api/v1/cameras/1/snapshot?detection_run_id=101 HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Пример — размеченный кадр конкретной детекции
GET /api/v1/cameras/1/snapshot?detection_run_id=101&annotated=true HTTP/1.1
Host: api.parktrack.live
Authorization: Bearer <token>
Response (200)
Тело ответа — бинарный JPEG с Content-Type: image/jpeg.
Для сохранённого кадра API также возвращает:
| Заголовок | Значение |
|---|---|
Content-Disposition | inline; filename="raw.jpg" или inline; filename="annotated.jpg" |
Cache-Control | private, no-store |
X-Detection-Run-Id | идентификатор выбранного распознавания |
X-Snapshot-Captured-At | время кадра из аутентифицированного заголовка PTSNAP01 |
X-Snapshot-Variant | фактически возвращённый вариант: raw или annotated |
У live-кадра заголовок X-Snapshot-Variant равен raw, а заголовки
X-Detection-Run-Id и X-Snapshot-Captured-At отсутствуют.
Ошибки
| Код | Тип | Описание |
|---|---|---|
| 401 | Unauthorized | Токен отсутствует, невалиден или истёк. |
| 403 | Forbidden | Нет разрешения cameras.view. |
| 404 | Not Found | Камера, выбранная детекция, metadata или объект S3 не найдены. |
| 422 | Unprocessable Entity | detection_run_id меньше 1 или параметр имеет неверный тип. |
| 502 | Bad Gateway | Контейнер повреждён, подменён или не соответствует камере/варианту. |
| 503 | Service Unavailable | S3, ключ или конфигурация хранилища недоступны. |
Пример ответа (404)
{
"error_description": "Camera snapshot not available"
}
4.13 Требования к другим разделам API
Если эндпоинт другого раздела работает с камерой как с ресурсом партнёра, рекомендуется использовать поля:
camera_id(integer)partner_id(integer | null)created_by_user_id(integer | null)
Если эндпоинт другого раздела ссылается на снапшоты или результаты распознавания по камере, рекомендуется использовать camera_id как основной внешний ключ.