Перейти к основному содержимому

4. Cameras

Раздел описывает камеры наблюдения, используемые системой как источник точных данных о занятости парковочных зон.

4.1 Совместимость с текущей реализацией

Раздел сохраняет существующие основные эндпоинты:

  • GET /cameras
  • POST /cameras/new
  • GET /cameras/<camera_id>
  • PUT /cameras/<camera_id>
  • DELETE /cameras/<camera_id>
  • GET /cameras/next
  • GET /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.

Общие ошибки

КодТипОписание
400Bad RequestНевалидный JSON или неверные типы полей.
401UnauthorizedТокен отсутствует, невалиден, истёк или сессия завершена.
403ForbiddenУ пользователя недостаточно прав для выполнения действия.
404Not FoundКамера не найдена.
409ConflictКонфликт данных, например камера с таким title уже существует.
415Unsupported Media TypeНеверный Content-Type. Используй application/json.
422Unprocessable EntityОшибка валидации.
500Internal Server ErrorНеобработанная ошибка сервера.
502Bad GatewayСохранённый объект повреждён или не соответствует детекции.
503Service 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
}
]

Ошибки

КодТипОписание
401UnauthorizedТокен отсутствует, невалиден или истёк.
403ForbiddenНедостаточно прав для просмотра камер.
422Unprocessable 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
}

Ошибки

КодТипОписание
400Bad RequestНевалидное тело запроса.
401UnauthorizedТокен отсутствует, невалиден или истёк.
403ForbiddenНедостаточно прав для создания камеры.
404Not FoundУказанный partner_id не найден.
409ConflictКамера с таким title уже существует.
415Unsupported Media TypeНеверный Content-Type.
422Unprocessable 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"
}

Ошибки

КодТипОписание
401UnauthorizedТокен отсутствует, невалиден или истёк.
403ForbiddenНедостаточно прав для просмотра камеры.
404Not 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"
}

Ошибки

КодТипОписание
400Bad RequestНевалидное тело запроса.
401UnauthorizedТокен отсутствует, невалиден или истёк.
403ForbiddenНедостаточно прав для изменения камеры.
404Not FoundКамера не найдена.
422Unprocessable 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>

Ошибки

КодТипОписание
401UnauthorizedТокен отсутствует, невалиден или истёк.
403ForbiddenНедостаточно прав для удаления камеры.
404Not FoundКамера не найдена.
409ConflictУдаление невозможно из-за активных зависимостей, если сервер не выполняет каскадное удаление.

Пример ответа (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
}

Ошибки

КодТипОписание
401UnauthorizedТокен отсутствует или невалиден.
403ForbiddenНедостаточно прав.
404Not 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, default false) — вернуть сохранённый кадр с разметкой.
  • last_detection (boolean, default false) — при annotated=false вернуть последний сохранённый raw-кадр детекции вместо live-кадра.
  • fallback_to_raw (boolean, default false) — при annotated=true разрешить возврат raw, если annotated для выбранной детекции отсутствует.
  • detection_run_id (integer >= 1) — выбрать конкретное распознавание этой камеры вместо последнего. При annotated=false возвращается raw, при annotated=trueannotated либо raw, если разрешён fallback.

Приоритет выбора:

  1. Если указан detection_run_id, выбирается только это распознавание.
  2. Иначе при annotated=true выбирается последнее распознавание с подходящим кадром.
  3. Иначе при last_detection=true выбирается последний сохранённый raw.
  4. Во всех остальных случаях возвращается новый 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-Dispositioninline; filename="raw.jpg" или inline; filename="annotated.jpg"
Cache-Controlprivate, 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 отсутствуют.

Ошибки

КодТипОписание
401UnauthorizedТокен отсутствует, невалиден или истёк.
403ForbiddenНет разрешения cameras.view.
404Not FoundКамера, выбранная детекция, metadata или объект S3 не найдены.
422Unprocessable Entitydetection_run_id меньше 1 или параметр имеет неверный тип.
502Bad GatewayКонтейнер повреждён, подменён или не соответствует камере/варианту.
503Service UnavailableS3, ключ или конфигурация хранилища недоступны.

Пример ответа (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 как основной внешний ключ.