Видеопоток (тег типа 6)

Как завести камеру в конфигураторе, описано в Подключение видеокамер. Ниже — контракт чтения, архива и управления.

Тег с prsValueTypeCode = 6 — окно в камеру, а не ряд чисел. Кадры не пишутся в RabbitMQ, VictoriaMetrics и PostgreSQL. Камера заводится как коннектор с адресом потока. Каталог архива, срок хранения, ширина и частота кадров задаются у хранилища данных типа 3 (archivePath, retentionHours, archiveWidth, archiveFps). Тег привязывают и к коннектору, и к этому хранилищу. Новых атрибутов LDAP для этого нет.

Пример конфигурации коннектора:

{
  "source": {
    "rtspUrl": "rtsp://192.168.1.72:554/live/ch00_0",
    "rtspTransport": "tcp",
    "onvifPort": 8899,
    "onvifUser": "",
    "onvifPassword": ""
  },
  "log": {
    "level": "INFO",
    "fileName": "logs/prs_connector_{id}.log",
    "maxBytes": 10485760,
    "backupCount": 10,
    "sendToPlatform": false
  }
}

Зона распознавания человека задаётся на этом же экране, поверх картинки. Переход на тег другого типа сразу убирает блок видео, не дожидаясь ответа сервера. Сейчас на экране одна зона. Кнопка Зона включает правку и остаётся подсвеченной, пока правка включена. Остальные кнопки блока на мгновение загораются жёлтым, чтобы было видно нажатие. Углы ставятся кликами по картинке. Клик по жёлтой линии добавляет вершину на эту линию. Шаг назад снимает последнюю точку. Удалить зону убирает многоугольник. Контур не выходит за границы картинки. Справа снизу у блока видео есть ручка: потяните её, чтобы изменить размер. Записать сохраняет зону в prsJsonConfigString тега ключом zones. Каждая координата — доля кадра от 0 до 1, ноль слева и сверху. zones — массив многоугольников, каждый многоугольник сам массив точек [x, y]:

{"zones": [[[0.1, 0.2], [0.4, 0.2], [0.4, 0.8]]]}

Сейчас в массиве одна зона. Пустой zones означает весь кадр. Весь кадр как раз записывает пустой список. Если раньше было записано несколько многоугольников, на экране показывается первый, и следующая запись оставляет только его. Старый конверт {"zone": {"zones": [...]}} при чтении ещё понимается, а запись уже кладёт массив в корень. Человек ищется в прямоугольнике вокруг зоны: он равномерно увеличивается до входа сети 640×640, пропорции фигуры сохраняются. Края прямоугольника чуть шире многоугольника, чтобы человека на границе не разрезало. Человек считается в зоне, если его рамка пересекает этот многоугольник. Пустая зона по-прежнему означает весь кадр. Метод «Человек в зоне» попадает в каталог «Методы» командой «Загрузка описания» из methodDescription.yaml проекта видеометодов. На теге хранится ссылка на него. Многоугольники метод не хранит: параметр zone — запрос к платформе prsTag.api_crud.read.* за атрибутом prsJsonConfigString этого тега, а JSONata $eval(data[0].attributes.prsJsonConfigString[0]).zones отдаёт в метод массив zones.

source.rtspUrl обязателен и должен быть адресом rtsp:// или rtsps://. Наличие этого ключа и делает коннектор камерой. Тот же ключ в корне JSON тоже принимается. Ключ log совпадает с журналом коннектора: level, fileName, maxBytes, backupCount и флажок sendToPlatform. Архив пишется в <archivePath хранилища>/<id коннектора>. Каталог вне тома /var/lib/peresvet/video пропадёт вместе с контейнером. retentionHours у хранилища — срок хранения в часах; если ключ опущен, берётся 24. archiveWidth и archiveFps тоже у хранилища: ширина архива в пикселях (чётное число, высота подстраивается) и частота кадров. null или пустое значение означает ширину и частоту камеры, поток копируется. Пустые onvifUser и onvifPassword означают, что камера логин не требует. Транспорт по умолчанию TCP. Его можно задать ключом rtspTransport со значением tcp или udp. Управление камерой идёт по OnVif, отдельным запросом, без второго подключения RTSP. Хост берётся из rtspUrl, порт по умолчанию 8899 (ключ onvifPort). Если камера требует логин, его задают ключами onvifUser и onvifPassword. В блоке «Данные тега» кнопки стоят слева от картинки. Стрелки двигают камеру, пока их держат. «Ближе» и «Дальше» делают один шаг зума. «Стоп» останавливает поворот и наклон сразу. «Домой» возвращает камеру по уже сделанным с этой страницы движениям и снимает накопленный зум: сама камера положение не запоминает. Под стрелками камера показывает параметры изображения, которые сама отдаёт по OnVif: яркость, контраст, насыщенность, резкость, засветка, динамический диапазон, ИК-фильтр (день, ночь, авто) и баланс белого. Ползунок или список отправляет на камеру только изменённый параметр. Камера может ответить успехом и при этом не изменить кадр: прошивка принимает зум и параметры изображения, сохраняет числа и не применяет их к объективу. Поворот и наклон при этом могут работать.

Снаружи конфигуратора то же управление — POST /v1/ptz/, тело JSON. Обязателен tagId тега видеопотока. Поле action:

  • status — профиль, pan, tilt, zoom и список пресетов. То же делает GET /v1/ptz/?tagId=<id>.

  • move — непрерывный поворот, пока не придёт stop или не истекут 8 секунд таймаута команды. pan, tilt и zoom от −1 до 1. Если камера объявляет свою шкалу скорости выше 1, перед первым поворотом платформа поднимает её скорость до этого максимума: иначе команда доходит сразу, а объектив ползёт. Повтор той же команды, пока поворот уже идёт, на камеру не отправляется.

  • step — один относительный сдвиг с теми же тремя полями.

  • pulse — шаг зума. Знак zoom задаёт направление, на камеру уходит ±1.

  • stop — остановить текущий поворот и наклон.

  • home — пройти назад движения, посланные с момента старта сервиса подхвата, затем вызвать домашнее положение камеры.

  • preset — перейти к пресету, поле token или preset.

  • image — текущие параметры изображения и допустимые значения.

  • image_set — записать параметры: brightness, contrast, saturation, sharpness (числа), backlight и wideDynamicRange (ON или OFF), irCut (ON, OFF, AUTO), whiteBalance (AUTO или MANUAL).

Ошибка управления приходит тем же JSON error.code и error.message. Чужой тег — 422, выключенный тег — 409, тег без камеры — 424, камера отклонила команду — 502.

Тег типа «Видеопоток» привязывается к этому коннектору на вкладке коннектора и не кладётся в хранилище истории.

Чтение

GET /v1/data/?tagId=<id> для такого тега отдаёт не JSON-точки, а медиа:

  • без start и finish — живой поток WebRTC (один H.264 на всех зрителей);

  • только finish — снимок image/jpeg на этот момент; если фрагмента на это время нет, ответ 404 без картинки;

  • start и finish — фрагмент video/mp4 за период.

Ключи timeStep и count для видеопотока пока не поддерживаются. На одну камеру держится один сеанс RTSP в контейнере подхвата prs-video-capture. Этот сеанс копирует поток камеры в архив, если хранилище не задало ширину или частоту, и один раз кодирует H.264 для живого просмотра. Смотрящих может быть несколько, картинка при этом не кодируется заново: платформа проверяет право чтения тега, по RPC находит сервис отдачи и проксирует запрос по HTTP. Отдача так же находит подхват и забирает у него байты. Клиент к камере не подключается. В RabbitMQ уходит только идентификатор и базовый URL процесса, не кадры. Распознавание берёт последний кадр этого сеанса (/internal/video/<id>/frame), а не открывает запись отдельным ffmpeg. Снимок на прошлый момент и фрагмент за период собираются из архива и могут занять время.

В конфигураторе живой поток открывается в <video> через POST /v1/video/webrtc. Снимок по-прежнему image/jpeg, фрагмент — <video> с video/mp4. Кадры WebRTC идут в nginx на порт 8189 (UDP и TCP) того же хоста, с которого открыта страница.

Метки start и finish — микросекунды или дата ISO 8601. Для видеотега отсутствие обеих меток означает живой поток. У остальных тегов отсутствие метки по-прежнему означает «сейчас».

Ошибки чтения

Тело ошибки — JSON {"error": {"code": ..., "message": "..."}}, не картинка.

  • 404 «Нет записи камеры на этот момент.» — на finish нет фрагмента.

  • 404 «Нет записи камеры за этот период.» — между start и finish нет файлов.

  • 409 «Тег видеопотока выключен.»

  • 422 «Видеопоток читается по одному тегу за запрос.»

  • 422 «Для видеопотока ключи timeStep и count пока не поддерживаются.»

  • 422 «В периоде видеопотока finish должен быть позже start.»

  • 422 «Слишком длинный видеофрагмент.» Период длиннее VIDEO_MAX_FRAGMENT_SECONDS, по умолчанию 7200 секунд.

  • 424 «Тег видеопотока не привязан к активному коннектору-камере.»

  • 502 — камера или ffmpeg не отдали кадр или фрагмент.

  • 503 «Не найден ffmpeg.»

  • 503 «Сервис отдачи видео не запущен.» — нет ответа на video.delivery.locate.<id тега>.

  • 503 «Сервис подхвата видео не запущен.» — отдача не нашла процесс, который держит камеру.

Фрагмент

start и finish не отдают готовый файл с диска. Сервис подхвата находит покрывающие период фрагменты MPEG-TS, склеивает их и перекодирует в один video/mp4 (H.264, preset ultrafast). На сборку даётся 180 секунд. Пока она идёт, страница ждёт тело ответа: в конфигураторе это надпись «Загрузка…».

Своя HTML-страница

Если tagId известен заранее, живой поток — это WebRTC. Браузер шлёт SDP на POST /v1/video/webrtc и принимает ответ в RTCPeerConnection. Снимок по finish по-прежнему можно поставить в img.

<video id="cam" autoplay muted playsinline></video>
<audio autoplay
       src="/v1/data/?tagId=67bde18c-5431-1041-925c-470278307e33&amp;audio=1">
</audio>

audio=1 — звук того же сеанса, audio/mpeg. Если в потоке камеры звука нет, ответ 404 и тег audio молчит. Картинка по-прежнему без звука.

Страницу открывают с того же хоста, что и платформу: путь /v1/data/ уже проксируется nginx. В атрибут src нельзя подставить заголовок авторизации. При включённой авторизации браузер отправит только куку этого хоста, поэтому страница должна быть на нём же. Если перед картинкой стоит свой прокси, для /v1/data/ отключают буферизацию ответа. Иначе живой поток приходит одним куском или с большой задержкой.

Остановить просмотр в браузере — убрать адрес. Запись архива при этом продолжается:

document.querySelector("#cam").removeAttribute("src");

Снимок на момент — тот же адрес с finish. Знак плюс в часовом поясе кодируется как %2B:

<img alt="Снимок"
     src="/v1/data/?tagId=67bde18c-5431-1041-925c-470278307e33&amp;finish=2026-10-05T18:36:20%2B03:00">

Если записи нет, ответ — JSON, и тег img остаётся пустым. Текст ошибки тогда читают отдельным fetch по тому же адресу.

Фрагмент за период — тег video. Картинка появится после сборки mp4:

<video controls
       src="/v1/data/?tagId=67bde18c-5431-1041-925c-470278307e33&amp;start=2026-10-05T18:36:00%2B03:00&amp;finish=2026-10-05T18:37:00%2B03:00">
</video>

fetch живого потока без чтения тела по частям не завершается. Клиент вроде Advanced REST Client остаётся на строке «получены заголовки».

Просмотр в конфигураторе

У тега типа 6 блок «Данные тега» открыт и без привязки к хранилищу истории. Кнопка с иконкой воспроизведения читает тег по тем же правилам, что и GET /v1/data/:

  • пустые start и finish — живой поток;

  • только finish — снимок;

  • start и finish — фрагмент за период.

Пока картинка или поток открыты, та же кнопка меняет иконку на паузу и закрывает просмотр в браузере. Запись архива при этом не останавливается: сеанс с камерой держит сервис подхвата, а не открытая страница. Выключенный тег останавливает сам сеанс. Включение тега запускает его снова. Несколько тегов видеопотока на одной камере держат сеанс, пока включён хотя бы один.

Частота кадров просмотра

8 кадров в секунду — это не частота камеры. Для картинки в браузере подхват из того же сеанса отдаёт 8 кадров в секунду и ширину 960 пикселей (MJPEG). Этот поток один на камеру: его получают и зрители, и распознавание.

В архив по умолчанию пишется копия потока камеры, без второго кодирования. Уменьшить архив можно ключами archiveWidth и archiveFps у хранилища видео: тогда архив кодируется отдельно. Битрейт архива от сюжета и от этих ключей зависит и заранее не фиксируется: его нужно померить по уже записанным файлам.

Просмотр на диск не пишется. Один зритель при текущих настройках забирает по сети порядка 8 Мбит/с. Кодирование одно на камеру, сколько бы страниц ни было открыто.

Диск и мощность подхвата

Объём архива одной камеры:

ГБ ≈ часы_хранения × битрейт_Мбит/с × 0,44

Множитель 0,44 — это перевод Мбит/с в гигабайты за час: 3600 / 8 / 1024. На несколько камер результат умножается на их число.

По хранилищу с ключом archiveWidth 640 измеренный поток около 1,15 Мбит/с, то есть около 492 МБ в час. При retentionHours 24 это около 12 ГБ на эту камеру. Пока сеанс перезапускается, в часе бывают пустые промежутки, и фактический объём чуть меньше расчётного.

Мощность подхвата считается по числу камер, не по числу зрителей. На каждую камеру один процесс: приём RTSP, запись архива и подготовка просмотра. Сеть подхвата растёт отдельно, примерно на 8 Мбит/с на каждого зрителя живого потока. Перед добавлением следующей камеры смотрите загрузку контейнера prs-video-capture. Если память подходит к пределу, камер должно быть меньше либо картинка архива ещё уже.

Архив на диске

Архив — это фрагменты MPEG-TS. Каталог берётся из archivePath хранилища. У камеры с путём /var/lib/peresvet/video/rear-cam файлы лежат там, внутри тома контейнера подхвата. Имя файла — время начала фрагмента в секундах Unix, например 1791117621.ts.

Номинальная длина фрагмента — VIDEO_SEGMENT_SECONDS, по умолчанию 10 секунд. Если сеанс с камерой перезапускался, промежуток между соседними файлами бывает длиннее. Файлы старше retentionHours удаляет сам открытый сеанс. Новый срок, путь, ширина и частота приходят сообщением prsDataStorage.model.updated.<id> и применяются сразу.

Список фрагментов:

docker exec prs-video-capture ls -lh /var/lib/peresvet/video/rear-cam

Посмотреть запись из конфигуратора: у тега задайте start и finish и нажмите кнопку воспроизведения. Плеер получит один mp4 за этот период, склеенный из покрывающих его фрагментов.

Изменение коннектора

Сохранение коннектора в конфигураторе публикует в брокер сообщение prsConnector.model.updated.<id>. Сервис подхвата подписан на создание, изменение и удаление коннектора так же, как остальные сервисы этой сущности. Список камер задаётся полем nodes его конфигурации: пустой список — все камеры, иначе только перечисленные id. Голос на саму правку он отдаёт разрешающий: сохранить коннектор можно и пока подхват применяет новую конфигурацию. По сообщению сервер заново читает prsJsonConfigString. Новый rtspUrl или rtspTransport открывают сеанс заново. archivePath, retentionHours, archiveWidth и archiveFps читаются у хранилища видео. Их изменение публикует prsDataStorage.model.updated.<id>. Новый путь, ширина или частота открывают сеанс заново. Новый retentionHours применяется сразу: лишние фрагменты удаляются, а поток с камеры не переоткрывается.

Индикатор в дереве

Кружок у коннектора зелёный, пока на брокере есть MQTT-сессия с ClientId, равным id этого коннектора. Для камеры такую сессию открывает сервис подхвата на время живого сеанса RTSP и закрывает её, когда сеанс останавливается. В теги камеры при этом не пишутся коды качества 100 и 101, и полная конфигурация коннектора на камеру не отправляется.