Compare commits

...

14 Commits

Author SHA1 Message Date
gx b68d00604f Merge phase11b-cpp: C++ refactor композитора (Phase 11b)
13 commits объединяются в main. C++17 ООП-модель Cell / Layout /
Decoration через extern "C" ABI shim — старые callers работают
без изменений.

Ключевое:
  - cpp/cuda_raii.hpp: RAII обёртки (CudaBuffer/CudaStream), zero-copy
  - cpp/cell.hpp + camera_cell/widget_cell/blank_cell: иерархия cells
  - cpp/decoration.hpp + label_decoration/border_decoration: композиция
  - cpp/layout.hpp + template.hpp: 8×8 микро-сетка, JSON-templates
  - cpp/source_pool: pool + motion-state, by_frigate_camera lookup
  - cpp/composer: best-fit + asymmetric hysteresis (рост 0s, shrink 3s),
    fill свободных cells остальными drawable, manual PTZ override 60s
  - cpp/mqtt_overlay: generic MQTT-driven text overlays из JSON,
    подложка, anchors
  - composer_c_api.cpp + layouts_c_api.cpp: extern "C" wrappers
  - Удалены: src/composer.c, src/layouts.c
  - Detection box bbox следует за камерой при смене layout

В проде: gx/cuframes-composer:0.11b-step1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 19:03:17 +01:00
gx 3730c65a1e mqtt_overlays: убраны assist_question / assist_response
Conversation events HA Assist не приходят (архитектурное ограничение).
Чтобы placeholders ❯ Вы: ... и ✎ HA: ... не висели на экране без
данных — overlay'и убраны до решения вопроса с источником диалога.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 19:01:31 +01:00
gx a8ce3f1ccb mqtt_overlays.json: добавлены HA Assist question / response overlays
Подписка на assist/conversation/text + /response (plain-text retained,
schema см. home-assistant/.../assist_mqtt.md).

Позиция: левый-нижний угол стопкой —
  🤖 ответ внизу (margin_y=32)
  🗣 вопрос выше (margin_y=78)

Полупрозрачная подложка (bg_alpha=180), placeholder с эмодзи пока
turn'а не было.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:22:04 +01:00
gx d8e69c6392 Phase 11b: detection-box bbox следует за камерой при смене layout
User: "при движении объект оборачивается в рамку, но функционал не
учитывает что сетки могут переключаться и координаты ячеек меняются".

Был баг: detbox-overlay хранил cell_x/y/w/h из --detection-cell CLI
(заданы при старте), при смене layout рамки рисовались по старым
координатам — мимо камеры.

Изменения:
  - overlay.h/.c: новый API cfc_overlay_detbox_set_cell_geom(ov,x,y,w,h).
    Mutex-защищённое обновление detbox config'а — composer вызывает
    перед каждым draw.
  - CameraCell: добавлено поле source_key (хранит cuframes-key камеры,
    рендерящейся в этом cell). Layout::apply передаёт его из pool entry.
  - Layout::find_camera_cell_rect(key) — возвращает Rect текущей cell
    для камеры с заданным cuframes-key (или nullptr если её нет в layout).
  - SourcePool::by_frigate_camera(name) — lookup pool-entry по
    Frigate-camera-key (frigate event'ы приходят с этим именем).
  - Composer::compose_frame: перед draw каждого DETECTION_BOXES overlay'я
    — lookup frigate→cuframes_key→layout cell rect, обновляет detbox geom.
    Если камера не в layout сейчас — cell_w/h=0, detbox draw skip'ает.

Теперь bbox от Frigate переезжает за камерой:
  - tpl_1 → bbox в full screen 1920×1080
  - tpl_3 → bbox в main 1440×810
  - tpl_4 → bbox в quad ячейке 960×540
  - камера не в layout → bbox скрыт

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 10:25:25 +01:00
gx 88fa73f922 Phase 11b: подложка для text overlay + сразу-видимый placeholder
User: "у него есть полупрозрачная подложка? у него правильный z-order?"

Z-order был ок (overlays draw'аются после layout.render). Но без bg
текст плохо читался на пёстром кадре — и до прихода MQTT overlay был
visible=0 (не виден вообще).

Изменения:
  - overlay.h: cfc_overlay_text_config_t расширена bg_alpha / bg_y/u/v / bg_pad.
    bg_alpha=0 — фон отключён (default).
  - overlay.c draw_text: если bg_alpha>0, перед blit'ом текста рисуем fill
    rect (atlas_w+2*pad) × (atlas_h+2*pad) с заданным цветом и alpha.
  - overlay.c update_text: пробрасывает bg-поля при апдейте.
  - mqtt_overlay: MqttOverlayCfg + JSON loader научились читать bg_alpha,
    bg_y/u/v, bg_pad, placeholder. Default bg = чёрный 160 alpha, pad 10.
  - MqttOverlayItem::start: overlay сразу visible=1 с placeholder (default "—"),
    reposition_overlay вызывается до получения MQTT — placeholder
    позиционируется в anchor сразу.

User'у теперь видна тёмная подложка с текстом в правом-нижнем углу даже
если sensor молчит.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 10:19:29 +01:00
gx 75271436f7 Phase 11b: MQTT-overlays конфигурируются через JSON (вместо --temp-topic)
User: "оверлеев которые выводят какую-то инфу из MQTT может быть
бесконечно много. Надо делать настраиваемым через json/yaml конфиг".

Заменили захардкоженный TempMqttOverlay на generic MqttOverlay:

  - cpp/mqtt_overlay.hpp/.cpp: класс MqttOverlayItem (один topic + один
    text overlay) + MqttOverlayManager (контейнер). Загрузка из JSON.
  - cpp/mqtt_overlay_c_api.cpp: extern "C" обёртка для grid_record.c.
  - docker/mqtt_overlays.json: default config с temp_outside примером.
  - grid_record.c: --mqtt-overlays=PATH (заменил --temp-topic).
  - src/CMakeLists.txt: temp_overlay* удалены, mqtt_overlay* добавлены.

JSON schema:
  {
    "overlays": [
      {
        "id": "temp_outside",
        "topic": "zigbee2mqtt/Температура на улице",
        "json_field": "temperature",      // пусто = raw payload string
        "format": "%+.1f°C",                // printf
        "anchor": "right-bottom",           // right-top, left-bottom, ...
        "margin_x": 32, "margin_y": 24,
        "pixel_size": 32,
        "color": [255, 255, 255], "alpha": 230,
        "font_path": "/fonts/DejaVuSans-Bold.ttf"
      }
    ]
  }

MqttBrokerCfg делятся между всеми overlays (one connect_async per item
но shared credentials). Добавление новых overlays = редактирование JSON +
restart cfc-grid (hot-reload через ZMQ — Phase 12).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 10:07:05 +01:00
gx 362871a264 Phase 11b: глобальный temperature overlay (правый-нижний угол)
User: "в предыдущих версиях у нас показывалась температура, можешь
сделать в нижнем правом углу оверлей не привязанный к сеткам?". Восстановили
прежнее поведение из vf-cuda-grid/controller (dynamic_overlays.py).

Новый класс cfc::TempMqttOverlay:
  - libmosquitto subscriber в отдельном thread'е, auto-reconnect 1→30s
  - json-c для parse JSON payload — extract поле .temperature (double)
  - Формат: "%+.1f°C" (например "+18.5°C")
  - Persistent FreeType text overlay (cfc_overlay_create_text), kept в
    Composer::overlays_[] backward-compat листе — рендерится поверх Layout
  - reposition_overlay() — пересчёт x/y после text_size() (right-bottom anchor)

C-shim (composer_c_api.cpp) для grid_record.c:
  - cfc_temp_overlay_start(composer, host, port, user, pw, topic, W, H)
  - singleton (один temp overlay на процесс, прода-композитору хватит)

CLI: --temp-topic="zigbee2mqtt/Температура на улице". MQTT credentials
переиспользуются из --mqtt-host/--mqtt-user/--mqtt-pass.

Compose override (localhost-infra):
  --temp-topic=zigbee2mqtt/Температура на улице

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:58:47 +01:00
gx 24d398526e Phase 11b: вернули asymmetric layouts с widget cells
User: "где сетки main+preview, сетки с виджетами?". Восстановили из
прежней истории:
  - tpl_3 (1+2+widget+widget): main 1440×810 + 2 преview 480×270 +
    widget temp_chart справа + widget ha_chat снизу
  - tpl_5 (1+4+widget): main + 4 preview right column + ha_chat снизу
  - tpl_6 (1+3+2+widget): main + 3 правых + 2 нижних + widget info
  - tpl_7 (1+3+3+widget): main + 3 правых + 3 нижних + widget ha_chat
  - tpl_8 (1+3+4): main + 3 правых + 4 в нижней строке (полное покрытие)
  - tpl_9 — 3×3 + widget полосы справа/снизу

Widget cells — placeholder (тёмно-серый Y=40 + LabelDecoration с именем
widget'а). Реальные виджеты (HA chat, температурный график) — Phase 12+.
Chёрные ячейки больше не будут — composer.maybe_relayout заполняет
свободные camera-cells остальными drawable из pool.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:44:00 +01:00
gx e8392dd5ff Phase 11b: серые рамки 2px вокруг каждой cell
User: "ячейки не имеют границ, сделай их серым цветом". В Layout::apply()
для каждого Cell (CameraCell/BlankCell/WidgetCell) добавлен BorderDecoration:
  - thickness 2px
  - Y=180, U=128, V=128 (нейтральный серый в BT.709 limited)
  - alpha 220 (слегка полупрозрачный — видно контент за рамкой)

Decoration рисуется поверх content (после draw_content) — поверх любого
кадра/widget'а.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:30:11 +01:00
gx 858fe61b56 Phase 11b: hybrid PTZ — set_layout с timed motion freeze
User: "PTZ снова не переключает сетки". Причина: при motion-mode set_layout
игнорировался. Теперь: применяется + замораживает motion-mode на
manual_override_duration_ms_ (60s default). По истечении — auto-возврат
в motion-mode.

В Composer добавлено:
  - manual_override_until_ms_ (моноклоk монотонное время)
  - manual_override_duration_ms_ (default 60s)
  - set_layout: применяет template, ставит override timestamp
  - maybe_relayout: пока now < override → пропускаем (sustain manual layout),
    после → лог "expired, возврат в motion-mode" + force relayout

ONVIF server.py одновременно обновлён под актуальные template имена:
  - PTZ_PRESETS: tpl_1 / tpl_4 / tpl_9 / tpl_16 (вместо single/quad/...)
  - ContinuousMove zoom-in → tpl_1, zoom-out → tpl_16,
    pan/tilt → cycle через эти 4

Production smoke:
  GotoPreset tpl_4 → composer log "manual override 'tpl_4' до +60000ms" PASS.

Refs: #195.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:24:40 +01:00
gx 9d2a0b2bd7 Phase 11b: fill свободных camera-cells остальными drawable камерами
User: "смущает чёрная ячейка в сетке". Причина: asymmetric templates
имели widget cells (placeholder тёмно-серый Y=40) + при недостатке active
camera cells оставались BlankCell (чёрный).

Два изменения:

1. templates.json — оставили только 16:9 layouts (tpl_1/tpl_4/tpl_9/tpl_16).
   Все camera-cells, никаких widget-областей. Cells full 16:9 (cs==rs
   микроячейки), полностью покрывают output 1920×1080 без чёрных полос.
   Asymmetric layouts (main + satellites) удалены — вернуть в Phase 12
   когда widget'ы будут реальными (HA-chat, temperature graph).

2. composer::maybe_relayout — заполнить свободные camera-cells остальными
   drawable камерами из pool (по priority), если template имеет больше
   cells чем motion-active. Условие: cap > active.size().

Производство при 4 источниках в pool:
  - 1 motion → tpl_1 (1 cell full screen)
  - 2 motion → tpl_4 (2 motion + 2 not-active drawable = 4 cells заняты)
  - 4 motion → tpl_4 (все 4 motion)
  - При добавлении новых камер (до 16) — tpl_9 при 5..9, tpl_16 при 10..16

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 09:15:20 +01:00
gx 6e0273f4b4 Phase 11b F: extern "C" ABI shim + production deploy
В прод деплоен gx/cuframes-composer:0.11b-step1 — C++ ядро
работает через ABI shim, старые C-callers (grid_record.c, control.c,
frigate_mqtt.c) использует те же cfc_composer_* функции.

Что в этом коммите:
  - src/cpp/composer_c_api.cpp: extern "C" обёртки над cfc::Composer
    методами. Полный набор: _create/_destroy/_compose/_add_overlay/
    _find_overlay/_set_layout/_current_layout/_add_pool_source/
    _set_motion_mode/_get_motion_mode/_motion_pulse/_get_health.
  - src/cpp/layouts_c_api.cpp: extern "C" обёртки над template_loader
    для cfc_layout_find/_all/_load_file/_reload/_loaded_path/_to_pixels.
  - cpp/template_loader: global registry (current_templates / set_*
    / load_into_current) — единый источник истины. Composer и C ABI
    shim читают один и тот же mutex-защищённый vector<LayoutTemplate>.
    Hot-reload через ZMQ cfc_layout_load_file подхватывается composer'ом
    на следующем кадре без рестарта.
  - cpp/composer: pick_best_fit, set_layout, maybe_relayout читают
    current_templates() вместо локального snapshot.
  - cpp/composer: backward-compat overlay list (add_overlay/find_overlay)
    + manual cells support (для C API без motion-mode).
  - cpp/composer compose_frame: после Layout.render() рендерит overlays
    (CLI text/icon/border + Frigate detbox) поверх.
  - Удалены: src/composer.c (заменён composer_c_api.cpp + composer.cpp),
    src/layouts.c (заменён layouts_c_api.cpp + template_loader.cpp).
  - Оставлено как есть: src/overlay.c (PNG/text/border/detbox CLI overlays
    — реализация не меняется, доступ через cfc_overlay_*).
  - src/CMakeLists.txt: COMPOSER_SOURCES_C минус composer.c, layouts.c,
    COMPOSER_SOURCES_CPP плюс composer_c_api.cpp, layouts_c_api.cpp.

Production smoke (R9-88.23):
  [cfc/loader] /opt/templates.json: loaded 7 templates
  [cfc/composer] templates loaded: 7 (path='/opt/templates.json')
  [cfc/composer] pool+ cam-parking prio=100 / cam-gate_lpr prio=90 / ...
  [cfc/composer] motion_mode=1 ttl=45000ms pool=4
  [cfc/composer] grow → template='tpl_1' active=1
PASS.

Refs: #195 (Phase 11b C++ refactor).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 08:57:30 +01:00
gx beb8e1baa0 Phase 11b B-E: ООП-гипотеза проверена end-to-end
Что в этом коммите:

Decoration реализации:
  - cpp/label_decoration.hpp/.cpp — FreeType atlas + cugrid_blit_rgba_nv12.
    UTF-8 декодер, atlas в VRAM (RAII через CudaBuffer), rebuild при set_text.
  - cpp/border_decoration.hpp/.cpp — 4 cugrid_fill_nv12 (top/bottom/left/right).

Cell реализации:
  - cpp/camera_cell.hpp/.cpp — cfc_source_get_latest + cugrid_resize_nv12.
    Non-owning указатель на cfc_source_t (pool владеет).
  - cpp/widget_cell.hpp/.cpp — тёмный fill placeholder.
  - cpp/blank_cell.hpp/.cpp — BT.709 black fill.

Layout и Template:
  - cpp/template.hpp — LayoutTemplate { name, cells[], priority }.
    8×8 микро-сетка (kGridCols=kGridRows=8). to_pixels() переводит в Rect.
  - cpp/layout.hpp/.cpp — vector<unique_ptr<Cell>>, apply() создаёт
    CameraCell/WidgetCell/BlankCell + Decorations (Label с "{key} prio={N}").
  - cpp/template_loader.hpp/.cpp — JSON → vector<LayoutTemplate> через json-c.
    builtin_templates() = { tpl_1, tpl_4 } как fallback.

SourcePool:
  - cpp/source_pool.hpp/.cpp — owner cfc_source_t*, motion state атомарный,
    zone-filter в motion_pulse. Pool entries — non-copyable unique_ptr.

Composer:
  - cpp/composer.hpp/.cpp — owner SourcePool + templates + Layout + output.
    Алгоритмы: pick_best_fit (min nb_camera_cells >= need + priority tie-break),
    collect_active (drawable AND motion_within_TTL), asymmetric hysteresis
    (рост сразу через std::includes, сжатие — wait shrink_hysteresis_ms).
    Public C++ API: set_motion_mode / set_layout / load_templates / compose_frame.

ООП-гипотеза smoke:
  - examples/grid_record_cpp.cpp — минимальный smoke без NVENC. Init composer,
    compose_frame N раз, dump NV12 в файл. Проверяет что C++ модель
    компилируется, линкуется с C-кодом (source.c, nvenc.c остались на C через
    extern "C"), и реально рисует кадр.

Производительность сохранена:
  - Один output буфер VMM, передаётся как NV12Ref (read-write reference) во все
    cells/decorations — НИКАКИХ memcpy на cells boundary.
  - Virtual call overhead: 1 indirect call per cell per frame. Negligible.
  - Heap allocations только при apply_template (раз в N секунд при relayout).

Build:
  - CMakeLists.txt: CXX language, C++17.
  - src/CMakeLists.txt: COMPOSER_SOURCES_CPP добавлен в lib.
  - examples/CMakeLists.txt: grid_record_cpp.

Smoke test run jammy:
  [cfc/loader] docker/templates.json: loaded 7 templates
  [smoke] composer 1920x1080 templates=7 sources=0 motion=0
  [smoke] wrote 3317760 bytes (Y=2211840 UV=1105920) to /out/blank.nv12
  Build PASS, init PASS, compose PASS, dump PASS.

Что НЕ сделано:
  - extern "C" ABI shim для control.c / grid_record.c (старый C-композитор
    всё ещё единственный для prod stack).
  - Удаление старых composer.c / overlay.c / layouts.c.
  - Live deploy в прод (Step 1-3 функциональность).
  - JSON ZMQ hot-reload (был в Step 3 C-version, восстановить в C++).

Refs: #195 (Phase 11b C++ refactor).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 21:43:18 +01:00
gx f1c79eabde Phase 11b A: CMake C++17 + базовые headers
Branch phase11b-cpp — refactor композитора на ООП.

Что сделано в этом коммите:
  - CMakeLists.txt: CMAKE_CXX_STANDARD 17, language=CXX
  - include/cuframes_composer/cpp/cuda_raii.hpp: CudaBuffer + CudaStream
    как RAII обёртки (cuMemAlloc/cuMemFree, cuStreamCreate/Destroy).
    Non-copyable, movable. Zero-copy: handle CUdeviceptr передаётся
    идентично C-коду.
  - cpp/types.hpp: Rect (pixel coords) + NV12Ref (общий read-write
    референс на Y/UV plane'ы output буфера — composer + cells + decorations
    делят его без копий).
  - cpp/decoration.hpp: абстрактный Decoration с draw(stream, dst, parent_rect).
  - cpp/cell.hpp: абстрактный Cell с draw() = draw_content() +
    iterate decorations. Композиция через add_decoration().

Что НЕ сделано (следующие коммиты):
  - CameraCell, WidgetCell, BlankCell (cell-content реализации)
  - LabelDecoration, BorderDecoration (с FreeType/cugrid)
  - Layout (контейнер cells + apply_template)
  - Composer класс (owner SourcePool + Layout + OutputSurface)
  - extern "C" ABI shim для совместимости с control.c, grid_record.c
  - Удаление старых composer.c / overlay.c / layouts.c
  - Восстановление функционала JSON templates + auto-labels

Производительность: virtual call overhead 1 indirect call per cell per
frame (negligible), никаких heap allocations в hot path, CUDA pipeline
1:1 идентичен C-версии.

Refs: #195 (Phase 11b C++ refactor)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 21:27:44 +01:00
39 changed files with 3154 additions and 1054 deletions
+7 -1
View File
@@ -2,11 +2,17 @@ cmake_minimum_required(VERSION 3.20)
project(cuframes-composer project(cuframes-composer
VERSION 0.1.0 VERSION 0.1.0
DESCRIPTION "Multi-source video grid composer на CUDA + NVENC + RTSP" DESCRIPTION "Multi-source video grid composer на CUDA + NVENC + RTSP"
LANGUAGES C CUDA LANGUAGES C CXX CUDA
) )
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
# Phase 11b — C++17 для ООП-модели Cell/Layout/Decoration. Low-level
# модули (source, nvenc, frigate_mqtt, health, writer, audio) остаются
# на C; их API объявлен `extern "C"` чтобы линковаться с C++ кодом.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# CUDA архитектуры. Покрываем production-сценарии: # CUDA архитектуры. Покрываем production-сценарии:
+22
View File
@@ -0,0 +1,22 @@
{
"version": 1,
"grid_cols": 8,
"grid_rows": 8,
"_doc": "MQTT-driven text overlays. Каждый блок = одна MQTT-подписка + persistent text overlay в фиксированной позиции на output frame'е. Не привязан к layout cells. anchor: right-bottom/right-top/left-bottom/left-top/center. format: printf-style для extracted значения (для double — \"%+.1f°C\"). json_field пустой → raw payload как string.",
"overlays": [
{
"id": "temp_outside",
"topic": "zigbee2mqtt/Температура на улице",
"json_field": "temperature",
"format": "%+.1f°C",
"anchor": "right-bottom",
"margin_x": 32,
"margin_y": 24,
"pixel_size": 32,
"color": [255, 255, 255],
"alpha": 230,
"font_path": "/fonts/DejaVuSans-Bold.ttf"
}
]
}
+47 -8
View File
@@ -2,19 +2,19 @@
"version": 1, "version": 1,
"grid_cols": 8, "grid_cols": 8,
"grid_rows": 8, "grid_rows": 8,
"_doc": "Layout-templates для cfc-grid auto-layout. Координаты в микроячейках 8×8 (output 1920×1080 → каждая микроячейка 240×135 px, 16:9). Квадраты N×N микроячеек тоже 16:9. role=camera — заполняется из активных камер по priority. role=widget — placeholder.", "_doc": "Phase 11b — набор layouts на 8×8 микро-сетке. Свободные camera-cells при нехватке motion-камер заполняются остальными drawable из pool (cfc::Composer::maybe_relayout). Widget cells показывают placeholder (тёмно-серый + название); реальные виджеты — Phase 12+.",
"templates": [ "templates": [
{ {
"name": "tpl_1", "name": "tpl_1",
"_desc": "1 камера во весь экран.", "_desc": "Одна камера во весь экран.",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 8, "rs": 8, "role": "camera", "order": 0} {"col": 0, "row": 0, "cs": 8, "rs": 8, "role": "camera", "order": 0}
] ]
}, },
{ {
"name": "tpl_3", "name": "tpl_3",
"_desc": "Главная 1440×810 слева + 2 превью 480×270 справа стопкой, остаток — виджеты.", "_desc": "Главная 1440×810 + 2 превью 480×270 + widget справа-низ + widget снизу.",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0}, {"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1}, {"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
@@ -25,7 +25,7 @@
}, },
{ {
"name": "tpl_4", "name": "tpl_4",
"_desc": "Quad 2×2: 4 камеры 960×540. order=0 — top-left (главная).", "_desc": "Quad 2×2 4 камеры 960×540 (16:9). order=0 — top-left.",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 0}, {"col": 0, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 0},
{"col": 4, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 1}, {"col": 4, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 1},
@@ -35,7 +35,7 @@
}, },
{ {
"name": "tpl_5", "name": "tpl_5",
"_desc": "1 главная + 4 превью справа стопкой, нижняя полоса — виджет.", "_desc": "Главная + 4 превью справа стопкой, снизу — widget.",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0}, {"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1}, {"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
@@ -47,7 +47,7 @@
}, },
{ {
"name": "tpl_6", "name": "tpl_6",
"_desc": "1 главная + 3 правые + 2 нижние, остаток — виджет.", "_desc": "Главная + 3 правых + 2 нижних, остаток нижней строки — widget.",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0}, {"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1}, {"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
@@ -60,7 +60,7 @@
}, },
{ {
"name": "tpl_7", "name": "tpl_7",
"_desc": "1 главная + 3 правые + 3 нижние, угол — виджет.", "_desc": "Главная + 3 правых + 3 нижних, угол — widget.",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0}, {"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1}, {"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
@@ -74,7 +74,7 @@
}, },
{ {
"name": "tpl_8", "name": "tpl_8",
"_desc": "1+3+4 — главная + 3 правые + полная нижняя строка.", "_desc": "1+3+4 — главная + 3 правых + 4 в нижней строке (без widget).",
"cells": [ "cells": [
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0}, {"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1}, {"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
@@ -85,6 +85,45 @@
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 6}, {"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 7} {"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 7}
] ]
},
{
"name": "tpl_9",
"_desc": "3×3 (cells по 2×2 микроячейки в области 6×6, остаток — widget).",
"cells": [
{"col": 0, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 0},
{"col": 2, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 4, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 0, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 2, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 4, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 0, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 2, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 7},
{"col": 4, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 8},
{"col": 6, "row": 0, "cs": 2, "rs": 6, "role": "widget", "widget": "temp_chart"},
{"col": 0, "row": 6, "cs": 8, "rs": 2, "role": "widget", "widget": "ha_chat"}
]
},
{
"name": "tpl_16",
"_desc": "4×4 — 16 камер 480×270 (16:9), полностью покрывает 8×8.",
"cells": [
{"col": 0, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 0},
{"col": 2, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
{"col": 4, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 7},
{"col": 0, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 8},
{"col": 2, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 9},
{"col": 4, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 10},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 11},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 12},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 13},
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 14},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 15}
]
} }
] ]
} }
+6
View File
@@ -15,3 +15,9 @@ target_include_directories(simple_record PRIVATE ${CMAKE_SOURCE_DIR}/include)
add_executable(grid_record grid_record.c) add_executable(grid_record grid_record.c)
target_link_libraries(grid_record PRIVATE cuframes_composer_static) target_link_libraries(grid_record PRIVATE cuframes_composer_static)
target_include_directories(grid_record PRIVATE ${CMAKE_SOURCE_DIR}/include) target_include_directories(grid_record PRIVATE ${CMAKE_SOURCE_DIR}/include)
# Phase 11b — C++ ООП-гипотеза. Использует cfc::Composer напрямую (без C ABI shim).
add_executable(grid_record_cpp grid_record_cpp.cpp)
target_link_libraries(grid_record_cpp PRIVATE cuframes_composer_static)
target_include_directories(grid_record_cpp PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_compile_features(grid_record_cpp PRIVATE cxx_std_17)
+19 -1
View File
@@ -127,6 +127,7 @@ int main(int argc, char **argv)
const char *frigate_mqtt_host = NULL; const char *frigate_mqtt_host = NULL;
int frigate_mqtt_port = 1883; int frigate_mqtt_port = 1883;
const char *frigate_topic = "frigate/events"; const char *frigate_topic = "frigate/events";
const char *mqtt_overlays_path = NULL; /* JSON-конфиг MQTT-driven text overlays */
const char *initial_layout = NULL; /* --layout NAME → set_layout после init */ const char *initial_layout = NULL; /* --layout NAME → set_layout после init */
int motion_mode = 0; /* --motion-mode */ int motion_mode = 0; /* --motion-mode */
int motion_ttl = 45000; /* --motion-ttl ms */ int motion_ttl = 45000; /* --motion-ttl ms */
@@ -179,11 +180,12 @@ int main(int argc, char **argv)
{"motion-mode", no_argument, 0, 'm'}, /* enable motion-driven auto layout */ {"motion-mode", no_argument, 0, 'm'}, /* enable motion-driven auto layout */
{"motion-ttl", required_argument, 0, 'k'}, /* TTL ms (default 45000) */ {"motion-ttl", required_argument, 0, 'k'}, /* TTL ms (default 45000) */
{"templates", required_argument, 0, 'z'}, /* path to templates.json */ {"templates", required_argument, 0, 'z'}, /* path to templates.json */
{"mqtt-overlays", required_argument, 0, 'x'}, /* path to MQTT overlays JSON */
{0, 0, 0, 0}, {0, 0, 0, 0},
}; };
const char *templates_path = NULL; const char *templates_path = NULL;
int c; int c;
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:RF:A:G:T:D:L:S:mk:z:", opts, NULL)) != -1) { while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:RF:A:G:T:D:L:S:mk:z:x:", opts, NULL)) != -1) {
switch (c) { switch (c) {
case 'o': out_path = optarg; break; case 'o': out_path = optarg; break;
case 'c': case 'c':
@@ -241,6 +243,7 @@ int main(int argc, char **argv)
case 'm': motion_mode = 1; break; case 'm': motion_mode = 1; break;
case 'k': motion_ttl = atoi(optarg); break; case 'k': motion_ttl = atoi(optarg); break;
case 'z': templates_path = optarg; break; case 'z': templates_path = optarg; break;
case 'x': mqtt_overlays_path = optarg; break;
case 'S': { case 'S': {
if (num_sources >= 32) { if (num_sources >= 32) {
fprintf(stderr, "max 32 sources\n"); return 1; fprintf(stderr, "max 32 sources\n"); return 1;
@@ -446,6 +449,21 @@ int main(int argc, char **argv)
cfc_composer_set_motion_mode(comp, 1, motion_ttl); cfc_composer_set_motion_mode(comp, 1, motion_ttl);
} }
/* Глобальные MQTT-driven overlays (температура и т.п.) — JSON-конфиг.
* Каждая запись = MQTT subscribe + persistent text overlay. См.
* include/cuframes_composer/cpp/mqtt_overlay.hpp для schema. */
if (mqtt_overlays_path) {
extern int cfc_mqtt_overlays_load(cfc_composer_t *, const char *,
const char *, int,
const char *, const char *,
int, int);
int n = cfc_mqtt_overlays_load(comp, mqtt_overlays_path,
mqtt_host ? mqtt_host : "cctv-mosquitto", mqtt_port,
mqtt_user, mqtt_pass,
out_w, out_h);
fprintf(stderr, "[grid_record] mqtt overlays loaded: %d\n", n);
}
/* --layout NAME → applies named layout поверх --cell координат. Удобно /* --layout NAME → applies named layout поверх --cell координат. Удобно
* как default для ONVIF PTZ-управляемого composer'а (старт в quad, * как default для ONVIF PTZ-управляемого composer'а (старт в quad,
* далее set_layout через ZMQ). В motion-mode не работает (relayout * далее set_layout через ZMQ). В motion-mode не работает (relayout
+177
View File
@@ -0,0 +1,177 @@
/* grid_record_cpp — Phase 11b ООП-гипотеза.
*
* Минимальный smoke: проверяет что C++ модель Cell/Layout/Decoration/Composer
* компилируется, линкуется с C-частями, и реально рисует кадры через те же
* CUDA-kernels (zero-copy: единый NV12 буфер не копируется между cells).
*
* Делает N композиций → dump последнего NV12 кадра в файл → exit.
* Без NVENC: цель гипотезы — не encode, а доказать что ООП-pipeline работает.
*
* Использование:
* grid_record_cpp --out /tmp/last.nv12 --frames 50 \
* --templates /opt/templates.json \
* --source cam-parking,frigate=parking_overview,priority=100 \
* --source cam-back_yard,frigate=back_yard,priority=70 \
* --motion-mode
*/
#include "../include/cuframes_composer/cpp/composer.hpp"
#include <cuda.h>
#include <cuda_runtime.h>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <getopt.h>
#include <signal.h>
#include <string>
#include <thread>
#include <unistd.h>
#include <vector>
namespace {
volatile sig_atomic_t g_stop = 0;
void on_sig(int) { g_stop = 1; }
struct SourceSpec {
std::string key;
std::string frigate;
int priority = 0;
std::vector<std::string> zones;
};
std::vector<std::string> split_colon(const std::string& s)
{
std::vector<std::string> out;
std::string cur;
for (char c : s) {
if (c == ':') { if (!cur.empty()) out.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
if (!cur.empty()) out.push_back(cur);
return out;
}
SourceSpec parse_source(const std::string& arg)
{
SourceSpec s;
std::vector<std::string> parts;
std::string cur;
for (char c : arg) {
if (c == ',') { parts.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
if (!cur.empty()) parts.push_back(cur);
if (parts.empty()) return s;
s.key = parts[0];
for (std::size_t i = 1; i < parts.size(); i++) {
auto& p = parts[i];
if (p.rfind("frigate=", 0) == 0) s.frigate = p.substr(8);
else if (p.rfind("priority=", 0) == 0) s.priority = std::atoi(p.c_str() + 9);
else if (p.rfind("zones=", 0) == 0) s.zones = split_colon(p.substr(6));
}
return s;
}
} // namespace
int main(int argc, char** argv)
{
std::string out_path;
std::string templates_path;
int width = 1920, height = 1080;
int frames_to_compose = 25;
bool motion_mode = false;
int motion_ttl = 45000;
std::vector<SourceSpec> sources;
static struct option opts[] = {
{"out", required_argument, 0, 'o'},
{"frames", required_argument, 0, 'n'},
{"width", required_argument, 0, 'W'},
{"height", required_argument, 0, 'H'},
{"source", required_argument, 0, 'S'},
{"motion-mode",no_argument, 0, 'm'},
{"motion-ttl", required_argument, 0, 'k'},
{"templates", required_argument, 0, 'z'},
{0, 0, 0, 0},
};
int c;
while ((c = getopt_long(argc, argv, "o:n:W:H:S:mk:z:", opts, nullptr)) != -1) {
switch (c) {
case 'o': out_path = optarg; break;
case 'n': frames_to_compose = std::atoi(optarg); break;
case 'W': width = std::atoi(optarg); break;
case 'H': height = std::atoi(optarg); break;
case 'S': sources.push_back(parse_source(optarg)); break;
case 'm': motion_mode = true; break;
case 'k': motion_ttl = std::atoi(optarg); break;
case 'z': templates_path = optarg; break;
default: return 1;
}
}
if (out_path.empty()) {
std::fprintf(stderr, "Usage: %s --out FILE --source ... [--motion-mode]\n", argv[0]);
return 1;
}
signal(SIGINT, on_sig);
signal(SIGTERM, on_sig);
cuInit(0);
CUdevice dev; cuDeviceGet(&dev, 0);
CUcontext ctx; cuDevicePrimaryCtxRetain(&ctx, dev);
cuCtxPushCurrent(ctx);
cfc::ComposerConfig ccfg;
ccfg.width = width;
ccfg.height = height;
ccfg.templates_path = templates_path;
ccfg.motion_ttl_ms = motion_ttl;
cfc::Composer composer(ccfg);
if (!composer.ok()) {
std::fprintf(stderr, "[smoke] composer init failed\n");
return 1;
}
cfc::SourcePool::SubscribeOpts opts_sub;
for (auto& s : sources) {
composer.pool().add(s.key, s.frigate, s.priority, s.zones, opts_sub);
}
if (motion_mode) composer.set_motion_mode(true, motion_ttl);
std::fprintf(stderr, "[smoke] composer %dx%d templates=%d sources=%zu motion=%d\n",
width, height, composer.templates_count(), sources.size(),
motion_mode ? 1 : 0);
/* Несколько композиций — даём sources подключиться. */
cfc::NV12Ref last{};
for (int i = 0; i < frames_to_compose && !g_stop; i++) {
last = composer.compose_frame();
cudaStreamSynchronize(0);
std::this_thread::sleep_for(std::chrono::milliseconds(40));
}
/* Dump последнего кадра в файл. */
std::size_t y_size = static_cast<std::size_t>(last.pitch_y) * height;
std::size_t uv_size = static_cast<std::size_t>(last.pitch_uv) * (height / 2);
std::vector<unsigned char> host(y_size + uv_size);
cuMemcpyDtoH(host.data(), last.y_ptr, y_size);
cuMemcpyDtoH(host.data() + y_size, last.uv_ptr, uv_size);
FILE* f = std::fopen(out_path.c_str(), "wb");
if (!f) { std::fprintf(stderr, "[smoke] open '%s' failed\n", out_path.c_str()); return 1; }
std::fwrite(host.data(), 1, host.size(), f);
std::fclose(f);
std::fprintf(stderr, "[smoke] wrote %zu bytes (Y=%zu UV=%zu) to %s\n",
host.size(), y_size, uv_size, out_path.c_str());
std::fprintf(stderr, "[smoke] current template: '%s'\n",
composer.current_layout_name().c_str());
cuCtxPopCurrent(nullptr);
cuDevicePrimaryCtxRelease(dev);
return 0;
}
@@ -0,0 +1,25 @@
/* BlankCell — пустая cell (Phase 11b).
*
* Используется для "идущего идле" слота без камеры — рисует чёрный rect
* на месте cell. Альтернативно может быть placeholder с надписью "NO SIGNAL"
* через LabelDecoration.
*/
#ifndef CUFRAMES_COMPOSER_CPP_BLANK_CELL_HPP
#define CUFRAMES_COMPOSER_CPP_BLANK_CELL_HPP
#include "cell.hpp"
namespace cfc {
class BlankCell : public Cell {
public:
explicit BlankCell(const Rect& geom) : Cell(geom) {}
protected:
void draw_content(CUstream stream, NV12Ref& dst) override;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_BLANK_CELL_HPP */
@@ -0,0 +1,39 @@
/* BorderDecoration — рамка вокруг cell (Phase 11b).
*
* 4 узких прямоугольника (top/bottom/left/right) через cfc_cugrid_fill_nv12.
* Полезна для подсветки main cell в layout'е или recording-indicator'ов.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_BORDER_DECORATION_HPP
#define CUFRAMES_COMPOSER_CPP_BORDER_DECORATION_HPP
#include "decoration.hpp"
namespace cfc {
struct BorderStyle {
int thickness = 3;
int color_y = 210, color_u = 50, color_v = 100; /* BT.709 limited */
int alpha = 240;
bool visible = true;
};
class BorderDecoration : public Decoration {
public:
explicit BorderDecoration(const BorderStyle& style) : style_(style) {}
~BorderDecoration() override = default;
void set_visible(bool v) noexcept { style_.visible = v; }
void set_style(const BorderStyle& s) noexcept { style_ = s; }
void draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect) override;
private:
BorderStyle style_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_BORDER_DECORATION_HPP */
@@ -0,0 +1,43 @@
/* CameraCell — рисует кадр из cuframes-источника в свой Rect (Phase 11b).
*
* Cell держит non-owning указатель на cfc_source_t (живёт в SourcePool
* композитора). На каждом draw_content():
* 1. cfc_source_get_latest — snapshot последнего кадра в VRAM
* 2. если ACTIVE/STALE — cfc_cugrid_resize_nv12 в свою geom_
* 3. если DEAD/CONNECTING — пропуск (cell остаётся blacked out)
*
* Decorations (label, border) рисуются в Cell::draw() поверх content'а.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_CAMERA_CELL_HPP
#define CUFRAMES_COMPOSER_CPP_CAMERA_CELL_HPP
#include "../source.h"
#include "cell.hpp"
#include <string>
namespace cfc {
class CameraCell : public Cell {
public:
CameraCell(const Rect& geom, cfc_source_t* source, std::string source_key = {})
: Cell(geom), source_(source), source_key_(std::move(source_key)) {}
void set_source(cfc_source_t* src) noexcept { source_ = src; }
cfc_source_t* source() const noexcept { return source_; }
const std::string& source_key() const noexcept { return source_key_; }
protected:
void draw_content(CUstream stream, NV12Ref& dst) override;
private:
cfc_source_t* source_; /* non-owning — pool владеет */
std::string source_key_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_CAMERA_CELL_HPP */
+64
View File
@@ -0,0 +1,64 @@
/* Cell — базовый абстрактный класс ячейки композитора (Phase 11b).
*
* Cell — это прямоугольная область output frame, рисуемая в свою geom_.
* Реализации (CameraCell, WidgetCell, BlankCell) определяют content-рендер;
* декорации (Label, Border) добавляются композицией через add_decoration().
*
* Lifecycle:
* 1. Layout::apply_template() создаёт нужные Cell-подклассы.
* 2. На каждом compose: cell.draw(stream, dst) рисует свой контент
* + все decorations.
* 3. Layout уничтожает cells при apply нового template'а.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_CELL_HPP
#define CUFRAMES_COMPOSER_CPP_CELL_HPP
#include "decoration.hpp"
#include "types.hpp"
#include <memory>
#include <vector>
namespace cfc {
class Cell {
public:
explicit Cell(const Rect& geom) : geom_(geom) {}
virtual ~Cell() = default;
Cell(const Cell&) = delete;
Cell& operator=(const Cell&) = delete;
/* Геометрия cell в pixel-координатах output frame. */
const Rect& geometry() const noexcept { return geom_; }
void set_geometry(const Rect& r) noexcept { geom_ = r; }
/* Добавить decoration (cell takes ownership). */
void add_decoration(std::unique_ptr<Decoration> d) {
decorations_.push_back(std::move(d));
}
/* Основной hook: рисует content + все decorations. Реализации обычно
* переопределяют только draw_content(), а draw_decorations() общий. */
void draw(CUstream stream, NV12Ref& dst) {
if (geom_.empty()) return;
draw_content(stream, dst);
for (auto& dec : decorations_) {
dec->draw(stream, dst, geom_);
}
}
protected:
/* Реализуется подклассом. */
virtual void draw_content(CUstream stream, NV12Ref& dst) = 0;
Rect geom_;
std::vector<std::unique_ptr<Decoration>> decorations_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_CELL_HPP */
+152
View File
@@ -0,0 +1,152 @@
/* Composer — оркестратор Phase 11b.
*
* Owns:
* - SourcePool (cuframes-источники + motion state)
* - vector<LayoutTemplate> (loaded from JSON или builtins)
* - Layout (текущее состояние cells)
* - OutputSurface (CudaBuffer для NV12 output)
*
* Compose loop (по кадру):
* 1. select_template_and_active() → (LayoutTemplate*, vector<PoolEntry*>)
* по правилам: motion_mode? motion-based best-fit : idle top-1 single
* 2. hysteresis: рост сразу, уменьшение — wait shrink_hysteresis_ms
* 3. если sig != committed → layout_.apply(template, active, W, H)
* 4. compose_clear() → output буфер чёрный
* 5. layout_.render(stream, NV12Ref)
*
* Public API экспортируется через composer_c_api.cpp с extern "C" для
* совместимости с control.c, grid_record.c, frigate_mqtt.c.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_COMPOSER_HPP
#define CUFRAMES_COMPOSER_CPP_COMPOSER_HPP
#include "../overlay.h" /* C API — backward compat для CLI overlays */
#include "cuda_raii.hpp"
#include "layout.hpp"
#include "source_pool.hpp"
#include "template.hpp"
#include "types.hpp"
#include <cuda_runtime.h>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
namespace cfc {
struct ComposerConfig {
int width = 1920;
int height = 1080;
int cuda_device = 0;
int bg_y = 16, bg_u = 128, bg_v = 128;
/* Motion-mode параметры. */
int motion_ttl_ms = 45000;
int shrink_hysteresis_ms = 3000;
/* Templates JSON path (empty → built-in). */
std::string templates_path;
};
class Composer {
public:
explicit Composer(const ComposerConfig& cfg);
~Composer();
Composer(const Composer&) = delete;
Composer& operator=(const Composer&) = delete;
bool ok() const noexcept { return output_.ok(); }
SourcePool& pool() noexcept { return pool_; }
const SourcePool& pool() const noexcept { return pool_; }
/* Motion mode + relayout policy. */
void set_motion_mode(bool on, int ttl_ms = 0);
bool motion_mode() const noexcept { return motion_mode_; }
/* Загрузить templates из JSON. Возвращает количество, либо <0. */
int load_templates(const std::string& path);
/* Перейти на named template (только если motion_mode == false). */
bool set_layout(const std::string& name);
const std::string& current_layout_name() const noexcept {
return layout_.name();
}
int templates_count() const noexcept {
return static_cast<int>(templates_.size());
}
const std::vector<LayoutTemplate>& templates() const noexcept {
return templates_;
}
/* Overlays — backward compat для grid_record.c CLI (--text/--icon/--border)
* и Frigate detection_boxes. Рисуются на выходе compose_frame ПОСЛЕ Layout.
* Composer takes ownership — destroy()'ит на ~Composer(). */
int add_overlay(cfc_overlay_t* ov);
cfc_overlay_t* find_overlay(const std::string& id) const;
/* Health отчёт для C ABI shim. */
struct Health {
int total = 0;
int active = 0;
int stale = 0;
int dead = 0;
};
Health get_health() const;
/* Manual cells — для C API без motion-mode (grid_record --cell без --motion-mode).
* Каждый вход {source_key, rect} рендерится CameraCell без template'а. */
void set_manual_cells(const std::vector<std::pair<std::string, Rect>>& cells);
/* Один кадр: relayout (если нужно) + clear + render.
* Возвращает NV12Ref на output (ptr действителен до следующего compose). */
NV12Ref compose_frame();
private:
/* Selection + hysteresis. */
const LayoutTemplate* pick_best_fit(int need) const;
std::vector<PoolEntry*> collect_active() const;
void maybe_relayout();
static std::string build_signature(const std::string& tpl_name,
const std::vector<PoolEntry*>& active);
ComposerConfig cfg_;
SourcePool pool_;
std::vector<LayoutTemplate> templates_;
Layout layout_;
/* Output NV12 буфер (VMM, zero-copy для NVENC). */
CudaBuffer output_;
int pitch_y_ = 0;
int pitch_uv_ = 0;
cudaStream_t stream_ = nullptr; /* default = 0 */
bool motion_mode_ = false;
std::int64_t committed_at_ms_ = 0;
std::int64_t pending_first_seen_ms_ = 0;
std::string committed_signature_;
std::string pending_signature_;
/* Manual override (PTZ через set_layout): пока now < manual_override_until_ms_
* motion-mode "заморожен", композитор держит зафиксированный layout. */
std::int64_t manual_override_until_ms_ = 0;
int manual_override_duration_ms_ = 60000;
/* Backward-compat overlay list (CLI overlays + detbox). */
std::vector<cfc_overlay_t*> overlays_;
/* Manual cells — alternative режим без motion-mode (grid_record --cell). */
std::vector<std::pair<std::string, Rect>> manual_cells_;
bool manual_applied_ = false;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_COMPOSER_HPP */
+134
View File
@@ -0,0 +1,134 @@
/* RAII обёртки над CUDA Driver/Runtime ресурсами (Phase 11b).
*
* Передача handle'ов между объектами по-прежнему zero-copy (CUdeviceptr —
* это unsigned long long; обмен идентичен plain C-коду). Эти обёртки только
* автоматизируют lifetime — без них приходилось бы вручную помнить про
* cuMemFree и закрывать stream'ы в путях ошибок.
*
* NB: классы non-copyable (чтобы не вызвать двойной cuMemFree), но movable.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_CUDA_RAII_HPP
#define CUFRAMES_COMPOSER_CPP_CUDA_RAII_HPP
#include <cuda.h>
#include <cuda_runtime.h>
#include <utility>
namespace cfc {
/* VMM-allocated NV12 буфер для output / staging. Используется и compose,
* и NVENC (через тот же CUdeviceptr — zero-copy). */
class CudaBuffer {
public:
CudaBuffer() = default;
/* Аллокация в ctor; бросать исключения не хочется — проверяем ok(). */
explicit CudaBuffer(std::size_t bytes) {
if (cuMemAlloc(&ptr_, bytes) == CUDA_SUCCESS) {
size_ = bytes;
}
}
~CudaBuffer() { reset(); }
CudaBuffer(const CudaBuffer&) = delete;
CudaBuffer& operator=(const CudaBuffer&) = delete;
CudaBuffer(CudaBuffer&& other) noexcept
: ptr_(other.ptr_), size_(other.size_) {
other.ptr_ = 0;
other.size_ = 0;
}
CudaBuffer& operator=(CudaBuffer&& other) noexcept {
if (this != &other) {
reset();
ptr_ = other.ptr_;
size_ = other.size_;
other.ptr_ = 0;
other.size_ = 0;
}
return *this;
}
void reset() noexcept {
if (ptr_) {
cuMemFree(ptr_);
ptr_ = 0;
size_ = 0;
}
}
CUdeviceptr ptr() const noexcept { return ptr_; }
std::size_t size() const noexcept { return size_; }
bool ok() const noexcept { return ptr_ != 0; }
private:
CUdeviceptr ptr_ = 0;
std::size_t size_ = 0;
};
/* CUDA stream — owner. Композитор использует default stream (Phase 2/3),
* но обёртка готова к stream-pipelining (Phase 12+). */
class CudaStream {
public:
CudaStream() = default;
/* Создать non-default stream. */
static CudaStream create() {
CudaStream s;
cudaStreamCreate(&s.stream_);
s.owned_ = (s.stream_ != nullptr);
return s;
}
/* Обёртка над уже существующим stream'ом (не владеет). */
static CudaStream wrap(cudaStream_t s) noexcept {
CudaStream w;
w.stream_ = s;
w.owned_ = false;
return w;
}
~CudaStream() { reset(); }
CudaStream(const CudaStream&) = delete;
CudaStream& operator=(const CudaStream&) = delete;
CudaStream(CudaStream&& other) noexcept
: stream_(other.stream_), owned_(other.owned_) {
other.stream_ = nullptr;
other.owned_ = false;
}
CudaStream& operator=(CudaStream&& other) noexcept {
if (this != &other) {
reset();
stream_ = other.stream_;
owned_ = other.owned_;
other.stream_ = nullptr;
other.owned_ = false;
}
return *this;
}
void reset() noexcept {
if (owned_ && stream_) {
cudaStreamDestroy(stream_);
}
stream_ = nullptr;
owned_ = false;
}
cudaStream_t handle() const noexcept { return stream_; }
CUstream cu_handle() const noexcept { return reinterpret_cast<CUstream>(stream_); }
private:
cudaStream_t stream_ = nullptr;
bool owned_ = false;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_CUDA_RAII_HPP */
@@ -0,0 +1,33 @@
/* Decoration — украшение поверх cell (Phase 11b).
*
* Cell держит vector<unique_ptr<Decoration>> и вызывает draw() каждого
* после своего content-рендера. Decorations знают только Rect cell'а
* (для позиционирования относительно неё) и пишут в тот же NV12Ref.
*
* Типы (минимум):
* LabelDecoration — текстовая подпись (FreeType atlas), позиция = угол cell
* BorderDecoration — рамка thickness px (4 fill_nv12 — top/bottom/left/right)
*
* Расширяется: BadgeDecoration, MotionIndicator, RecordingDot и т.д.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_DECORATION_HPP
#define CUFRAMES_COMPOSER_CPP_DECORATION_HPP
#include "types.hpp"
namespace cfc {
class Decoration {
public:
virtual ~Decoration() = default;
/* Нарисовать поверх parent_rect. NV12Ref общий с cell'ом. */
virtual void draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect) = 0;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_DECORATION_HPP */
@@ -0,0 +1,78 @@
/* LabelDecoration — текстовая подпись поверх cell (Phase 11b).
*
* Рендерит UTF-8 строку через FreeType в RGBA-атлас (создаётся один раз
* при ctor/set_text, держится в VRAM), затем на каждом draw() блитит
* атлас в указанный угол parent_rect через cfc_cugrid_blit_rgba_nv12.
*
* Корнер: top-left (cell.x + pad, cell.y + pad). Pad по умолчанию 8 px.
* Цвет, размер шрифта, alpha-множитель задаются в ctor.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_LABEL_DECORATION_HPP
#define CUFRAMES_COMPOSER_CPP_LABEL_DECORATION_HPP
#include "decoration.hpp"
#include <ft2build.h>
#include FT_FREETYPE_H
#include <cuda.h>
#include <string>
namespace cfc {
struct LabelStyle {
std::string font_path = "/fonts/DejaVuSans-Bold.ttf";
int pixel_size = 22;
int r = 255, g = 220, b = 64; /* жёлто-оранжевый, читается на любом фоне */
int alpha = 255; /* множитель прозрачности 0..255 */
int pad = 8; /* отступ от угла cell */
/* visible можно переключать без перерендера атласа. */
bool visible = true;
};
class LabelDecoration : public Decoration {
public:
LabelDecoration(const std::string& text, const LabelStyle& style);
~LabelDecoration() override;
LabelDecoration(const LabelDecoration&) = delete;
LabelDecoration& operator=(const LabelDecoration&) = delete;
void set_visible(bool v) noexcept { style_.visible = v; }
bool visible() const noexcept { return style_.visible; }
/* Обновить текст (re-render atlas). Передвижение/visible — без re-render. */
void set_text(const std::string& text);
void draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect) override;
private:
/* Pass1: измерить bbox строки + ascent для baseline'а. */
bool measure(int& w, int& h, int& ascent) const;
/* Pass2: отрисовать строку в RGBA-буфер CPU. */
void render_to_cpu(unsigned char* rgba, int w, int h, int ascent) const;
/* Rebuild VRAM atlas из текущей строки. */
bool rebuild_atlas();
/* FreeType state. */
FT_Library ft_lib_ = nullptr;
FT_Face face_ = nullptr;
/* Текст и стиль. */
std::string text_;
LabelStyle style_;
/* VRAM atlas. */
CUdeviceptr atlas_ = 0;
int atlas_w_ = 0;
int atlas_h_ = 0;
int atlas_pitch_ = 0;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_LABEL_DECORATION_HPP */
+62
View File
@@ -0,0 +1,62 @@
/* Layout — контейнер cells и оркестратор apply_template (Phase 11b).
*
* Layout::apply() принимает LayoutTemplate + список активных pool-entries
* (sorted by priority DESC) + output W×H. Создаёт нужные Cell-подклассы:
*
* CameraCell для каждой template-cell с role=CAMERA — берёт source
* по индексу из active list (active[0]=order 0, active[1]=order 1, ...)
* Если active меньше чем camera-cells — лишние cells = BlankCell.
* WidgetCell для template-cell с role=WIDGET — placeholder.
*
* Decorations добавляются здесь же:
* LabelDecoration "{key} prio={N}" в каждый CameraCell.
* LabelDecoration с именем widget'а в каждый WidgetCell.
* (Border, Badge — Phase 12+)
*
* Layout::render(stream, dst) — итеративно вызывает cell->draw().
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_LAYOUT_HPP
#define CUFRAMES_COMPOSER_CPP_LAYOUT_HPP
#include "cell.hpp"
#include "source_pool.hpp"
#include "template.hpp"
#include <memory>
#include <string>
#include <vector>
namespace cfc {
class Layout {
public:
Layout() = default;
/* Применить template — пересоздаёт cells + decorations.
* active_sorted — список pool-entries, уже отсортированный priority DESC. */
void apply(const LayoutTemplate& tpl,
const std::vector<PoolEntry*>& active_sorted,
int frame_w, int frame_h);
/* Прорисовать все cells в dst буфер. */
void render(CUstream stream, NV12Ref& dst);
const std::string& name() const noexcept { return current_name_; }
int cell_count() const noexcept { return static_cast<int>(cells_.size()); }
/* Найти текущий pixel-rect для камеры с заданным cuframes-key. NULL
* если этой камеры в layout сейчас нет. Используется detbox-overlay'ями
* для пересчёта bbox при смене layout. */
const Rect* find_camera_cell_rect(const std::string& source_key) const;
private:
std::vector<std::unique_ptr<Cell>> cells_;
std::string current_name_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_LAYOUT_HPP */
@@ -0,0 +1,123 @@
/* MqttOverlay — generic MQTT-driven text overlay (Phase 11b).
*
* Каждый overlay = одна MQTT-подписка + один persistent text overlay.
* Конфиг загружается из JSON-файла (mqtt_overlays.json):
* {
* "overlays": [
* { "id": "temp_outside",
* "topic": "zigbee2mqtt/Температура на улице",
* "json_field": "temperature", // если payload JSON; пусто — raw string
* "format": "%+.1f°C", // printf для extracted значения
* "anchor": "right-bottom", // right-top, left-bottom, ...
* "margin_x": 32, "margin_y": 24,
* "pixel_size": 32,
* "color": [255, 255, 255], "alpha": 230,
* "font_path": "/fonts/DejaVuSans-Bold.ttf"
* }, ...
* ]
* }
*
* Менеджер (MqttOverlayManager) держит vector<MqttOverlayItem>, поднимает
* MQTT-клиентов и добавляет overlays в композер. Hot-reload через
* reload_from_file() — пересоздаёт всех subscribers и overlays.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_MQTT_OVERLAY_HPP
#define CUFRAMES_COMPOSER_CPP_MQTT_OVERLAY_HPP
#include "../overlay.h"
#include <atomic>
#include <memory>
#include <string>
#include <vector>
struct mosquitto;
struct mosquitto_message;
namespace cfc {
struct MqttOverlayCfg {
std::string id;
std::string topic;
std::string json_field; /* если payload JSON; пусто = raw string */
std::string format = "%s"; /* printf-formatted (для double — "%+.1f°C") */
std::string anchor = "right-bottom"; /* right-top, left-bottom, ... */
int margin_x = 32, margin_y = 24;
int pixel_size = 32;
int r = 255, g = 255, b = 255;
int alpha = 230;
std::string font_path = "/fonts/DejaVuSans-Bold.ttf";
/* Полупрозрачная подложка. bg_alpha=0 → отключено. */
int bg_alpha = 160;
int bg_y = 16, bg_u = 128, bg_v = 128; /* по умолчанию чёрный */
int bg_pad = 10;
/* Что показывать пока нет MQTT-данных. Пусто → overlay невидим до
* первого сообщения. По умолчанию "—" чтобы было видно что overlay
* жив, но данные ещё не пришли. */
std::string placeholder = "";
};
struct MqttBrokerCfg {
std::string host = "cctv-mosquitto";
int port = 1883;
std::string username;
std::string password;
};
class MqttOverlayItem {
public:
MqttOverlayItem(const MqttOverlayCfg& cfg, const MqttBrokerCfg& broker,
int frame_w, int frame_h);
~MqttOverlayItem();
MqttOverlayItem(const MqttOverlayItem&) = delete;
MqttOverlayItem& operator=(const MqttOverlayItem&) = delete;
bool start();
cfc_overlay_t* overlay() const { return overlay_; }
const std::string& id() const { return cfg_.id; }
private:
static void on_connect(struct mosquitto* m, void* user, int rc);
static void on_message(struct mosquitto* m, void* user,
const struct mosquitto_message* msg);
void handle_payload(const char* payload, std::size_t len);
void update_text(const std::string& text);
void reposition_overlay();
MqttOverlayCfg cfg_;
MqttBrokerCfg broker_;
int frame_w_, frame_h_;
struct mosquitto* mosq_ = nullptr;
cfc_overlay_t* overlay_ = nullptr;
std::atomic<bool> running_{false};
std::string last_text_;
};
class MqttOverlayManager {
public:
explicit MqttOverlayManager(const MqttBrokerCfg& broker) : broker_(broker) {}
~MqttOverlayManager() = default;
/* Загрузить overlays из JSON-файла. Возвращает количество созданных. */
int load_from_file(const std::string& path, int frame_w, int frame_h);
/* Pointers на overlays для регистрации в композере. */
std::vector<cfc_overlay_t*> overlay_handles() const;
void clear();
int size() const { return static_cast<int>(items_.size()); }
private:
MqttBrokerCfg broker_;
std::vector<std::unique_ptr<MqttOverlayItem>> items_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_MQTT_OVERLAY_HPP */
@@ -0,0 +1,96 @@
/* SourcePool — пул cuframes-источников композитора (Phase 11b).
*
* Каждая запись: cuframes_key + frigate_camera + priority + cfc_source_t* +
* motion state (last_motion_ms, zone-filter). Pool создаётся при старте
* композитора (через add() вызовы) и живёт всю сессию.
*
* Cells (CameraCell) держат non-owning указатели на cfc_source_t — pool
* владеет.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_SOURCE_POOL_HPP
#define CUFRAMES_COMPOSER_CPP_SOURCE_POOL_HPP
#include "../source.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
namespace cfc {
struct PoolEntry {
std::string cuframes_key;
std::string frigate_camera;
int priority = 0;
cfc_source_t* source = nullptr;
std::atomic<std::int64_t> last_motion_ms{0};
std::vector<std::string> required_zones;
/* Получить snapshot для drawable-checks без локов. */
cfc_source_state_t state() const {
if (!source) return CFC_SOURCE_DISCONNECTED;
cfc_source_snapshot_t s{};
cfc_source_get_latest(source, &s);
return s.state;
}
bool drawable() const {
cfc_source_state_t st = state();
return st == CFC_SOURCE_ACTIVE || st == CFC_SOURCE_STALE;
}
};
class SourcePool {
public:
SourcePool() = default;
~SourcePool();
SourcePool(const SourcePool&) = delete;
SourcePool& operator=(const SourcePool&) = delete;
/* Параметры подписки cuframes (default per cfc_source_config_t). */
struct SubscribeOpts {
int cuda_device = 0;
std::string consumer_prefix = "composer";
int reconnect_min_ms = 1000;
int reconnect_max_ms = 30000;
int stale_threshold_ms = 500;
int dead_threshold_ms = 5000;
};
/* Добавить источник в pool. Возвращает индекс или -1. */
int add(const std::string& cuframes_key,
const std::string& frigate_camera,
int priority,
const std::vector<std::string>& zones,
const SubscribeOpts& opts);
int size() const { return static_cast<int>(entries_.size()); }
PoolEntry* by_index(int i) { return i >= 0 && i < size() ? entries_[i].get() : nullptr; }
PoolEntry* by_key(const std::string& key);
PoolEntry* by_frigate_camera(const std::string& frigate_camera);
/* Уведомить о motion (вызывается из Frigate MQTT subscriber'а через
* C-shim). Если zone-filter задан — проверяет пересечение. */
void motion_pulse(const std::string& frigate_camera,
const std::vector<std::string>& current_zones);
/* Итерация (для best-fit selection и health). */
template <typename F>
void for_each(F&& fn) {
for (auto& e : entries_) fn(*e);
}
private:
std::vector<std::unique_ptr<PoolEntry>> entries_;
std::mutex mu_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_SOURCE_POOL_HPP */
@@ -0,0 +1,69 @@
/* Layout template — описание сетки в микроячейках (Phase 11b).
*
* Template — declarative описание layout'а: имя, набор CellTemplate
* (col/row/cs/rs/role/order/widget). Layout::apply_template() из template'а
* + SourcePool создаёт конкретные Cell-объекты (CameraCell/WidgetCell).
*
* Грид: 8×8 микроячейки на output W×H. Для 1920×1080 микроячейка = 240×135 (16:9).
*
* Загружается из JSON через template_loader.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_TEMPLATE_HPP
#define CUFRAMES_COMPOSER_CPP_TEMPLATE_HPP
#include "types.hpp"
#include <string>
#include <vector>
namespace cfc {
constexpr int kGridCols = 8;
constexpr int kGridRows = 8;
enum class CellRole {
Camera = 0,
Widget = 1,
};
struct CellTemplate {
int col = 0, row = 0;
int cs = 1, rs = 1;
CellRole role = CellRole::Camera;
int order = 0;
std::string widget; /* имя widget'а для role=Widget */
};
struct LayoutTemplate {
std::string name;
int priority = 0;
std::vector<CellTemplate> cells;
int nb_camera_cells() const {
int n = 0;
for (auto& c : cells) if (c.role == CellRole::Camera) ++n;
return n;
}
};
/* Перевести {col,row,cs,rs} в pixel-rect для output W×H. */
inline Rect to_pixels(const CellTemplate& c, int W, int H)
{
Rect r;
r.x = (c.col * W) / kGridCols;
r.y = (c.row * H) / kGridRows;
r.w = (c.cs * W) / kGridCols;
r.h = (c.rs * H) / kGridRows;
/* NV12 4:2:0 — чётные. */
r.x &= ~1; r.y &= ~1; r.w &= ~1; r.h &= ~1;
if (r.x + r.w > W) r.w = W - r.x;
if (r.y + r.h > H) r.h = H - r.y;
return r;
}
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_TEMPLATE_HPP */
@@ -0,0 +1,35 @@
/* Template loader — JSON → vector<LayoutTemplate> (Phase 11b).
*
* Schema см. docker/templates.json. При неудаче возвращает empty vector
* (caller использует built-in fallback).
*/
#ifndef CUFRAMES_COMPOSER_CPP_TEMPLATE_LOADER_HPP
#define CUFRAMES_COMPOSER_CPP_TEMPLATE_LOADER_HPP
#include "template.hpp"
#include <string>
#include <vector>
namespace cfc {
/* Загрузить из файла. Возвращает количество загруженных templates, либо
* отрицательное число при ошибке (-1=parse, -2=schema, -3=open). */
int load_templates_from_file(const std::string& path,
std::vector<LayoutTemplate>& out);
/* Встроенный набор fallback templates (Phase 11b base — single, quad). */
std::vector<LayoutTemplate> builtin_templates();
/* Global template registry — единый источник для Composer и cfc_layout_*
* ABI shim. Заполняется builtin'ами по умолчанию; перезаписывается при
* load_templates_from_file (если был успех). Thread-safe — composer и
* control-thread читают, hot-reload пишет под lock. */
const std::vector<LayoutTemplate>& current_templates();
void set_current_templates(std::vector<LayoutTemplate> new_templates);
int load_into_current(const std::string& path);
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_TEMPLATE_LOADER_HPP */
+48
View File
@@ -0,0 +1,48 @@
/* Базовые типы C++-модели композитора (Phase 11b).
*
* Rect — pixel-координаты в output frame buffer'е (1920×1080 default).
* NV12Surface — wrapper над VMM-buffer'ом с pitch_y/pitch_uv для совместного
* использования compose и encoder'ом. По сути reference на CUdeviceptr —
* никаких копий не делается, ownership держит OutputSurface.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_TYPES_HPP
#define CUFRAMES_COMPOSER_CPP_TYPES_HPP
#include <cuda.h>
namespace cfc {
/* Прямоугольник в pixel-координатах output frame. Все координаты должны
* быть чётными (требование NV12 4:2:0). */
struct Rect {
int x = 0, y = 0;
int w = 0, h = 0;
bool empty() const noexcept { return w <= 0 || h <= 0; }
int right() const noexcept { return x + w; }
int bottom() const noexcept { return y + h; }
};
/* Reference на NV12-плоскости в VRAM. НЕ owner — все CUdeviceptr'ы
* принадлежат OutputSurface, передаются read-write всем cells/decorations.
*
* Слои:
* y_ptr — Y plane, size = pitch_y * height
* uv_ptr — UV plane (interleaved 2:0), size = pitch_uv * height/2
*
* frame_w / frame_h — размер всего output буфера (для clipping). */
struct NV12Ref {
CUdeviceptr y_ptr = 0;
int pitch_y = 0;
CUdeviceptr uv_ptr = 0;
int pitch_uv = 0;
int frame_w = 0;
int frame_h = 0;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_TYPES_HPP */
@@ -0,0 +1,34 @@
/* WidgetCell — заглушка для widget'а (Phase 11b MVP).
*
* Фаза 11b: рисует cell тёмно-серым (Y=40) + label-decoration с именем
* widget'а в центре. Реальные виджеты (graph, ha_chat) — Phase 12+.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_CPP_WIDGET_CELL_HPP
#define CUFRAMES_COMPOSER_CPP_WIDGET_CELL_HPP
#include "cell.hpp"
#include <string>
namespace cfc {
class WidgetCell : public Cell {
public:
WidgetCell(const Rect& geom, const std::string& widget_name)
: Cell(geom), widget_name_(widget_name) {}
const std::string& widget_name() const noexcept { return widget_name_; }
protected:
void draw_content(CUstream stream, NV12Ref& dst) override;
private:
std::string widget_name_;
};
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_WIDGET_CELL_HPP */
+15
View File
@@ -105,6 +105,14 @@ typedef struct cfc_overlay_text_config {
int r, g, b; /* sRGB цвет 0..255 */ int r, g, b; /* sRGB цвет 0..255 */
int extra_alpha; /* 0..255 общий множитель прозрачности */ int extra_alpha; /* 0..255 общий множитель прозрачности */
int visible; /* 0/1 — выводить ли */ int visible; /* 0/1 — выводить ли */
/* Опциональный полупрозрачный фон (подложка) под текстом.
* bg_alpha = 0 → без фона (default)
* bg_alpha > 0 → fill rect (atlas_w + 2*bg_pad) × (atlas_h + 2*bg_pad)
* с цветом bg_y/u/v перед blit'ом текста.
* bg_pad чётный, default 8 если bg_alpha>0 и bg_pad==0. */
int bg_alpha; /* 0..255 (0 = отключено) */
int bg_y, bg_u, bg_v; /* BT.709 limited (Y=16..235, UV=16..240) */
int bg_pad; /* px padding вокруг текста */
} cfc_overlay_text_config_t; } cfc_overlay_text_config_t;
/* Создать TEXT overlay. Открывает font через FreeType, рендерит строку /* Создать TEXT overlay. Открывает font через FreeType, рендерит строку
@@ -168,6 +176,13 @@ int cfc_overlay_create_detection_boxes(
* правильный overlay по incoming event'у. */ * правильный overlay по incoming event'у. */
const char *cfc_overlay_detbox_camera_key(cfc_overlay_t *ov); const char *cfc_overlay_detbox_camera_key(cfc_overlay_t *ov);
/* Обновить cell-геометрию runtime (при смене layout композитора). Композитор
* вызывает перед draw каждым detbox-overlay'ем — пересчитывает положение
* рамки под текущую позицию камеры. */
int cfc_overlay_detbox_set_cell_geom(cfc_overlay_t *ov,
int cell_x, int cell_y,
int cell_w, int cell_h);
/* Проверить пересечение current_zones события с required_zones overlay'я. /* Проверить пересечение current_zones события с required_zones overlay'я.
* - если required_zones пуст → всегда 1 (filter off) * - если required_zones пуст → всегда 1 (filter off)
* - если current_zones пуст → 0 (объект вне зон) * - если current_zones пуст → 0 (объект вне зон)
+28 -5
View File
@@ -14,21 +14,40 @@ set(COMPOSER_SOURCES_C
source.c source.c
nvenc_loader.c nvenc_loader.c
nvenc.c nvenc.c
composer.c
overlay.c overlay.c
control.c control.c
health.c health.c
writer.c writer.c
audio.c audio.c
frigate_mqtt.c frigate_mqtt.c
layouts.c )
# Phase 11b — C++ ООП-модель Cell/Layout/Decoration/Composer + ABI shim.
# Заменяет composer.c и layouts.c из Phase 10/11. Старые callers (control.c,
# frigate_mqtt.c, examples/grid_record.c) продолжают использовать те же
# cfc_composer_* и cfc_layout_* функции — они теперь обёртки над C++ ядром.
set(COMPOSER_SOURCES_CPP
cpp/label_decoration.cpp
cpp/border_decoration.cpp
cpp/camera_cell.cpp
cpp/widget_cell.cpp
cpp/blank_cell.cpp
cpp/source_pool.cpp
cpp/layout.cpp
cpp/template_loader.cpp
cpp/composer.cpp
cpp/composer_c_api.cpp
cpp/layouts_c_api.cpp
cpp/mqtt_overlay.cpp
cpp/mqtt_overlay_c_api.cpp
) )
set(COMPOSER_SOURCES_CU set(COMPOSER_SOURCES_CU
cugrid/cugrid.cu cugrid/cugrid.cu
) )
add_library(cuframes_composer SHARED ${COMPOSER_SOURCES_C} ${COMPOSER_SOURCES_CU}) add_library(cuframes_composer SHARED
add_library(cuframes_composer_static STATIC ${COMPOSER_SOURCES_C} ${COMPOSER_SOURCES_CU}) ${COMPOSER_SOURCES_C} ${COMPOSER_SOURCES_CPP} ${COMPOSER_SOURCES_CU})
add_library(cuframes_composer_static STATIC
${COMPOSER_SOURCES_C} ${COMPOSER_SOURCES_CPP} ${COMPOSER_SOURCES_CU})
foreach(target cuframes_composer cuframes_composer_static) foreach(target cuframes_composer cuframes_composer_static)
target_include_directories(${target} target_include_directories(${target}
@@ -39,16 +58,20 @@ foreach(target cuframes_composer cuframes_composer_static)
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${NVCODEC_HEADERS_DIR} ${NVCODEC_HEADERS_DIR}
) )
target_compile_features(${target} PRIVATE c_std_11) target_compile_features(${target} PRIVATE c_std_11 cxx_std_17)
# C-only флаги (для CUDA свои дефолты, -Wpedantic не подходит для .cu). # C-only флаги (для CUDA свои дефолты, -Wpedantic не подходит для .cu).
target_compile_options(${target} PRIVATE target_compile_options(${target} PRIVATE
$<$<COMPILE_LANGUAGE:C>:-Wall> $<$<COMPILE_LANGUAGE:C>:-Wall>
$<$<COMPILE_LANGUAGE:C>:-Wextra> $<$<COMPILE_LANGUAGE:C>:-Wextra>
$<$<COMPILE_LANGUAGE:C>:-Wpedantic> $<$<COMPILE_LANGUAGE:C>:-Wpedantic>
$<$<COMPILE_LANGUAGE:CXX>:-Wall>
$<$<COMPILE_LANGUAGE:CXX>:-Wextra>
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Debug>>:-O0> $<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Debug>>:-O0>
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Debug>>:-g3> $<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Debug>>:-g3>
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Release>>:-O2> $<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Release>>:-O2>
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Release>>:-g> $<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Release>>:-g>
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Release>>:-O2>
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Release>>:-g>
) )
target_link_libraries(${target} target_link_libraries(${target}
PUBLIC PUBLIC
-764
View File
@@ -1,764 +0,0 @@
/* Реализация cfc_composer_t — multi-source grid композитор.
*
* Owns:
* - N cfc_source_t (по одному на ячейку grid'а)
* - один NV12 output buffer (cuMemAlloc — staging для NVENC encoder'а)
* - statistics для health-репортов
*
* Compose-цикл:
* 1) cuMemsetD8 → быстрое черный fill всего Y plane (16=BT.709 black)
* + UV plane заполняется отдельно (128,128).
* 2) Для каждой ячейки:
* a) get_latest snapshot.
* b) ACTIVE → cfc_cugrid_resize_nv12 (src VMM → dst rect)
* c) DEAD/STALE → cfc_cugrid_fill_nv12 чёрным с alpha=255 уже сделано,
* тут лучше визуально показать что источник упал, поэтому в Phase 3
* поверх blackout рисуется текст «NO SIGNAL» через overlay'и.
* 3) cudaStreamSynchronize → output готов.
*
* Phase 2 упрощения:
* - Sync compose на default stream. Stream pipelining — Phase 3+.
* - Без double buffering. encode и compose делаются строго последовательно.
*
* Лицензия: LGPL-2.1+
*/
#include "../include/cuframes_composer/composer.h"
#include "../include/cuframes_composer/cugrid.h"
#include "../include/cuframes_composer/layouts.h"
#include "../include/cuframes_composer/overlay.h"
#include <cuda_runtime.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define CFC_COMPOSER_MAX_CELLS 64
#define CFC_COMPOSER_MAX_OVERLAYS 64
/* Source pool — для motion-mode. Каждая запись хранит cuframes-key,
* привязку к Frigate-камере (для motion match'а) и приоритет. */
#define CFC_POOL_ZONE_MAX 8
#define CFC_POOL_ZONE_NAME 32
typedef struct cfc_pool_entry {
char cuframes_key[64];
char frigate_camera[48];
int priority;
cfc_source_t *source;
_Atomic int64_t last_motion_ms;
/* Optional zone-filter. */
char required_zones[CFC_POOL_ZONE_MAX][CFC_POOL_ZONE_NAME];
int required_zones_count;
/* Persistent text overlay "{key} prio={N}" — позиционируется в углу
* cell при relayout, hidden если камера неактивна. */
cfc_overlay_t *label_overlay;
char label_text[96]; /* кеш строки для update_text без re-render */
} cfc_pool_entry_t;
struct cfc_composer {
cfc_composer_config_t cfg;
/* Копии cells (caller владеет original config'ом). source_key копируется
* в персистентную строку чтобы cfc_source_t могла на неё указывать. */
cfc_composer_cell_t cells[CFC_COMPOSER_MAX_CELLS];
char cell_keys[CFC_COMPOSER_MAX_CELLS][64];
int num_cells;
/* Источники теперь хранятся в pool, не привязаны к cells[].
* compose_cell ищет source через pool_find_by_key(cell.source_key). */
/* Output NV12 буфер: один contiguous allocation, Y plane (pitch * h) +
* UV plane (pitch * h/2). Pitch выравнен на 256 байт. */
CUdeviceptr output_ptr;
int output_pitch_y;
int output_pitch_uv;
size_t output_size;
/* CUDA stream для compose (Phase 2 — default stream = 0). */
cudaStream_t stream;
/* Overlays — в порядке добавления (= z-order). composer take ownership. */
cfc_overlay_t *overlays[CFC_COMPOSER_MAX_OVERLAYS];
int num_overlays;
/* Текущий named layout (если был выставлен через set_layout). Пустая
* строка = cells заданы вручную (через --cell). */
char current_layout[CFC_LAYOUT_MAX_NAME];
/* Source pool — для motion-driven layout. Все cuframes-subscriptions
* композитора живут здесь (включая те что добавились через --cell).
* compose_cell ищет source по cuframes_key. */
cfc_pool_entry_t pool[CFC_COMPOSER_MAX_CELLS];
int pool_count;
pthread_mutex_t pool_mu; /* для add_pool_source vs motion_pulse */
/* Motion-mode state. */
int motion_mode; /* 0/1 */
int motion_ttl_ms; /* default 45000 */
};
static int64_t now_ms_mono(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static void compose_motion_relayout(cfc_composer_t *comp);
/* Найти запись в pool по cuframes_key. NULL если нет. Caller держит mutex. */
static cfc_pool_entry_t *pool_find_by_key(cfc_composer_t *comp, const char *key)
{
if (!key) return NULL;
for (int i = 0; i < comp->pool_count; i++) {
if (!strcmp(comp->pool[i].cuframes_key, key)) return &comp->pool[i];
}
return NULL;
}
/* ── Helpers ──────────────────────────────────────────────────────────── */
static int round_up_pitch(int w)
{
return (w + 255) & ~255;
}
static void *cu_ptr(CUdeviceptr p) { return (void *)(uintptr_t)p; }
/* ── Compose ──────────────────────────────────────────────────────────── */
static int compose_clear(cfc_composer_t *comp)
{
/* Y plane → 16 (BT.709 black). */
cudaError_t e = cudaMemsetAsync(
cu_ptr(comp->output_ptr), comp->cfg.bg_y,
(size_t)comp->output_pitch_y * comp->cfg.height,
comp->stream);
if (e != cudaSuccess) {
fprintf(stderr, "[cfc/composer] Y memset failed: %s\n", cudaGetErrorString(e));
return -1;
}
/* UV plane → нужны два значения (U=128, V=128), не один. Делаем fill
* через тот же cfc_cugrid_fill_nv12 которым fillим ячейки. Прокидываем
* alpha=255 чтобы перезатереть полностью. */
CUdeviceptr uv = comp->output_ptr +
(size_t)comp->output_pitch_y * comp->cfg.height;
/* Просто memset UV не подходит — там interleaved пары. Делаем fill_nv12
* с alpha=255, тогда формула станет dst = fill * 255 / 255 = fill. */
int rc = cfc_cugrid_fill_nv12(
(CUstream)comp->stream,
/* Y уже сделан выше — но fill_nv12 повторно fillит Y. Передаём
* y_color = bg_y, alpha=255 — get тот же результат, минор waste.
* Phase 2 acceptable, в Phase 3 разделим Y/UV fillы. */
comp->output_ptr, comp->output_pitch_y,
uv, comp->output_pitch_uv,
0, 0, comp->cfg.width, comp->cfg.height,
comp->cfg.bg_y, comp->cfg.bg_u, comp->cfg.bg_v, 255);
return rc;
}
static int compose_cell(cfc_composer_t *comp, int idx)
{
const cfc_composer_cell_t *cell = &comp->cells[idx];
/* Layout мог обнулить cell (set_layout с меньшим num_cells) — skip. */
if (cell->w <= 0 || cell->h <= 0) return 0;
if (!cell->source_key) return 0;
/* Source lookup в pool — motion-mode перепривязывает cells на лету,
* sources[idx] больше не валиден сам по себе. Lookup O(N), N ≤ 32. */
cfc_pool_entry_t *p = pool_find_by_key(comp, cell->source_key);
if (!p || !p->source) return 0;
cfc_source_t *src = p->source;
cfc_source_snapshot_t snap;
cfc_source_get_latest(src, &snap);
if (snap.state != CFC_SOURCE_ACTIVE || snap.width <= 0) {
/* DEAD/STALE/CONNECTING — оставляем чёрный (уже clear'нут).
* Phase 3 добавит overlay «NO SIGNAL». */
return 0;
}
CUdeviceptr uv = comp->output_ptr +
(size_t)comp->output_pitch_y * comp->cfg.height;
/* Source NV12 layout: Y (pitch_y * height) + UV (pitch_y * height/2)
* непрерывно. Указатель на UV plane = ptr + pitch_y * height. */
CUdeviceptr src_uv = snap.ptr + (size_t)snap.pitch_y * snap.height;
return cfc_cugrid_resize_nv12(
(CUstream)comp->stream,
snap.ptr, snap.width, snap.height, snap.pitch_y,
src_uv, snap.pitch_uv,
comp->output_ptr, comp->output_pitch_y,
uv, comp->output_pitch_uv,
cell->x, cell->y, cell->w, cell->h);
}
/* ── Public API ───────────────────────────────────────────────────────── */
int cfc_composer_create(const cfc_composer_config_t *cfg, cfc_composer_t **out)
{
if (!cfg || !out) return -1;
if (cfg->width <= 0 || cfg->height <= 0) return -1;
if (cfg->num_cells <= 0 || cfg->num_cells > CFC_COMPOSER_MAX_CELLS) return -1;
if (!cfg->cells) return -1;
cfc_composer_t *comp = calloc(1, sizeof(*comp));
if (!comp) return -1;
comp->cfg = *cfg;
comp->num_cells = cfg->num_cells;
comp->stream = 0; /* default stream Phase 2 */
pthread_mutex_init(&comp->pool_mu, NULL);
comp->motion_ttl_ms = 45000; /* default 45s (рабочий sweet spot 30-60) */
/* Дефолты для bg цвета (если caller не задал). */
if (!comp->cfg.bg_y) comp->cfg.bg_y = 16;
if (!comp->cfg.bg_u) comp->cfg.bg_u = 128;
if (!comp->cfg.bg_v) comp->cfg.bg_v = 128;
/* Сохраняем cells + копируем source_key в персистентное хранилище. */
for (int i = 0; i < cfg->num_cells; i++) {
comp->cells[i] = cfg->cells[i];
if (cfg->cells[i].source_key) {
strncpy(comp->cell_keys[i], cfg->cells[i].source_key,
sizeof(comp->cell_keys[i]) - 1);
comp->cells[i].source_key = comp->cell_keys[i];
}
}
/* Выделяем output NV12 буфер. */
comp->output_pitch_y = round_up_pitch(cfg->width);
comp->output_pitch_uv = comp->output_pitch_y;
comp->output_size = (size_t)comp->output_pitch_y * cfg->height +
(size_t)comp->output_pitch_uv * (cfg->height / 2);
CUresult cr = cuMemAlloc(&comp->output_ptr, comp->output_size);
if (cr != CUDA_SUCCESS) {
const char *es = NULL; cuGetErrorString(cr, &es);
fprintf(stderr, "[cfc/composer] cuMemAlloc(%zu) failed: %s\n",
comp->output_size, es ? es : "?");
free(comp);
return -1;
}
/* Pool: для каждой уникальной cell.source_key создаём подписку.
* Если key уже в pool (тот же source в нескольких cells) — реюзим. */
for (int i = 0; i < comp->num_cells; i++) {
const char *key = comp->cells[i].source_key;
if (!key) continue;
if (pool_find_by_key(comp, key)) continue; /* уже добавлен */
if (comp->pool_count >= CFC_COMPOSER_MAX_CELLS) {
fprintf(stderr, "[cfc/composer] pool overflow\n");
break;
}
cfc_pool_entry_t *e = &comp->pool[comp->pool_count];
strncpy(e->cuframes_key, key, sizeof(e->cuframes_key) - 1);
e->cuframes_key[sizeof(e->cuframes_key) - 1] = '\0';
e->priority = 0;
atomic_init(&e->last_motion_ms, 0);
char name[32];
const char *prefix = comp->cfg.consumer_prefix;
if (!prefix || !*prefix) prefix = "composer";
snprintf(name, sizeof(name), "%s-%d", prefix, comp->pool_count);
cfc_source_config_t scfg = {
.key = key,
.consumer_name = name,
.cuda_device = cfg->cuda_device,
.reconnect_min_ms = cfg->reconnect_min_ms,
.reconnect_max_ms = cfg->reconnect_max_ms,
.stale_threshold_ms = cfg->stale_threshold_ms,
.dead_threshold_ms = cfg->dead_threshold_ms,
};
if (cfc_source_create(&scfg, &e->source) != 0) {
fprintf(stderr,
"[cfc/composer] cfc_source_create failed для '%s' (pool[%d])\n",
key, comp->pool_count);
e->source = NULL; /* DEAD — compose покажет blackout */
}
comp->pool_count++;
}
cfc_cugrid_init();
*out = comp;
return 0;
}
int cfc_composer_compose(cfc_composer_t *comp,
CUdeviceptr *out_y_ptr,
int *out_pitch_y,
int *out_width,
int *out_height)
{
if (!comp) return -1;
/* Motion-mode пересобирает cells перед каждым кадром (no-op если выключен). */
compose_motion_relayout(comp);
if (compose_clear(comp) != 0) return -1;
for (int i = 0; i < comp->num_cells; i++) {
if (compose_cell(comp, i) != 0) {
fprintf(stderr, "[cfc/composer] compose_cell %d failed\n", i);
/* Не fatal — продолжаем с остальными ячейками. */
}
}
/* Overlays поверх grid'а — в порядке добавления. */
CUdeviceptr uv = comp->output_ptr +
(size_t)comp->output_pitch_y * comp->cfg.height;
for (int i = 0; i < comp->num_overlays; i++) {
if (cfc_overlay_draw(comp->overlays[i],
(CUstream)comp->stream,
comp->output_ptr, comp->output_pitch_y,
uv, comp->output_pitch_uv,
comp->cfg.width, comp->cfg.height) != 0) {
fprintf(stderr, "[cfc/composer] overlay %d draw failed\n", i);
/* Не fatal. */
}
}
cudaError_t e = cudaStreamSynchronize(comp->stream);
if (e != cudaSuccess) {
fprintf(stderr, "[cfc/composer] stream sync failed: %s\n",
cudaGetErrorString(e));
return -1;
}
if (out_y_ptr) *out_y_ptr = comp->output_ptr;
if (out_pitch_y) *out_pitch_y = comp->output_pitch_y;
if (out_width) *out_width = comp->cfg.width;
if (out_height) *out_height = comp->cfg.height;
return 0;
}
int cfc_composer_add_overlay(cfc_composer_t *comp, cfc_overlay_t *ov)
{
if (!comp || !ov) return -1;
if (comp->num_overlays >= CFC_COMPOSER_MAX_OVERLAYS) {
fprintf(stderr, "[cfc/composer] overlay limit %d reached\n",
CFC_COMPOSER_MAX_OVERLAYS);
return -1;
}
comp->overlays[comp->num_overlays++] = ov;
return 0;
}
cfc_overlay_t *cfc_composer_find_overlay(cfc_composer_t *comp, const char *id)
{
if (!comp || !id) return NULL;
for (int i = 0; i < comp->num_overlays; i++) {
const char *oid = cfc_overlay_get_id(comp->overlays[i]);
if (oid && !strcmp(oid, id)) return comp->overlays[i];
}
return NULL;
}
int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name)
{
if (!comp || !layout_name) return -1;
/* В motion-mode set_layout игнорируется — relayout управляется
* автоматически из compose_motion_relayout. */
if (comp->motion_mode) {
fprintf(stderr, "[cfc/composer] set_layout('%s') ignored: motion_mode active\n",
layout_name);
return -1;
}
const cfc_layout_t *lay = cfc_layout_find(layout_name);
if (!lay) {
fprintf(stderr, "[cfc/composer] unknown layout '%s'\n", layout_name);
return -1;
}
int W = comp->cfg.width, H = comp->cfg.height;
int n_apply = lay->nb_cells;
if (n_apply > CFC_COMPOSER_MAX_CELLS) n_apply = CFC_COMPOSER_MAX_CELLS;
if (n_apply > comp->num_cells) n_apply = comp->num_cells;
/* Микро-сетка → pixel coords для каждой cell. Source_key привязок не
* меняем (Step 2+ добавит role/order распределение). */
for (int i = 0; i < n_apply; i++) {
int x, y, w, h;
cfc_layout_to_pixels(&lay->cells[i], W, H, &x, &y, &w, &h);
comp->cells[i].x = x;
comp->cells[i].y = y;
comp->cells[i].w = w;
comp->cells[i].h = h;
}
/* Cells сверх layout->nb_cells — обнуляем, чтобы не рисовались. */
for (int i = n_apply; i < comp->num_cells; i++) {
comp->cells[i].w = 0;
comp->cells[i].h = 0;
}
strncpy(comp->current_layout, lay->name, sizeof(comp->current_layout) - 1);
comp->current_layout[sizeof(comp->current_layout) - 1] = '\0';
fprintf(stderr, "[cfc/composer] layout='%s' (%d active cells, %d sources)\n",
lay->name, n_apply, comp->num_cells);
return 0;
}
const char *cfc_composer_current_layout(cfc_composer_t *comp)
{
if (!comp) return NULL;
return comp->current_layout[0] ? comp->current_layout : NULL;
}
/* ── Motion-driven layout ────────────────────────────────────────────── */
/* Распарсить colon-separated zones list в entry. */
static void parse_zones(cfc_pool_entry_t *e, const char *zones)
{
e->required_zones_count = 0;
if (!zones || !*zones) return;
const char *p = zones;
while (p && *p && e->required_zones_count < CFC_POOL_ZONE_MAX) {
const char *sep = strchr(p, ':');
int len = sep ? (int)(sep - p) : (int)strlen(p);
if (len > CFC_POOL_ZONE_NAME - 1) len = CFC_POOL_ZONE_NAME - 1;
memcpy(e->required_zones[e->required_zones_count], p, len);
e->required_zones[e->required_zones_count][len] = '\0';
e->required_zones_count++;
p = sep ? sep + 1 : NULL;
}
}
int cfc_composer_add_pool_source(cfc_composer_t *comp,
const char *cuframes_key,
const char *frigate_camera,
int priority,
const char *required_zones)
{
if (!comp || !cuframes_key) return -1;
pthread_mutex_lock(&comp->pool_mu);
cfc_pool_entry_t *e = pool_find_by_key(comp, cuframes_key);
if (e) {
/* Уже в pool (был добавлен из --cell). Просто перебиваем
* frigate_camera + priority + zones. */
if (frigate_camera) {
strncpy(e->frigate_camera, frigate_camera, sizeof(e->frigate_camera) - 1);
e->frigate_camera[sizeof(e->frigate_camera) - 1] = '\0';
}
e->priority = priority;
parse_zones(e, required_zones);
pthread_mutex_unlock(&comp->pool_mu);
return 0;
}
if (comp->pool_count >= CFC_COMPOSER_MAX_CELLS) {
pthread_mutex_unlock(&comp->pool_mu);
return -1;
}
e = &comp->pool[comp->pool_count];
strncpy(e->cuframes_key, cuframes_key, sizeof(e->cuframes_key) - 1);
e->cuframes_key[sizeof(e->cuframes_key) - 1] = '\0';
if (frigate_camera) {
strncpy(e->frigate_camera, frigate_camera, sizeof(e->frigate_camera) - 1);
e->frigate_camera[sizeof(e->frigate_camera) - 1] = '\0';
}
e->priority = priority;
parse_zones(e, required_zones);
atomic_init(&e->last_motion_ms, 0);
char name[32];
const char *prefix = comp->cfg.consumer_prefix;
if (!prefix || !*prefix) prefix = "composer";
snprintf(name, sizeof(name), "%s-%d", prefix, comp->pool_count);
cfc_source_config_t scfg = {
.key = e->cuframes_key,
.consumer_name = name,
.cuda_device = comp->cfg.cuda_device,
.reconnect_min_ms = comp->cfg.reconnect_min_ms,
.reconnect_max_ms = comp->cfg.reconnect_max_ms,
.stale_threshold_ms = comp->cfg.stale_threshold_ms,
.dead_threshold_ms = comp->cfg.dead_threshold_ms,
};
if (cfc_source_create(&scfg, &e->source) != 0) {
fprintf(stderr, "[cfc/composer] add_pool_source: subscribe '%s' failed\n",
cuframes_key);
e->source = NULL;
}
/* Persistent text overlay для подписи cell — позиция выставляется в
* compose_motion_relayout (visible=1 + x/y); неактивные камеры — visible=0.
* Font hardcoded под production volume `/fonts/DejaVuSans-Bold.ttf`. */
snprintf(e->label_text, sizeof(e->label_text), "%s prio=%d", cuframes_key, priority);
cfc_overlay_text_config_t tc = {
.font_path = "/fonts/DejaVuSans-Bold.ttf",
.text = e->label_text,
.pixel_size = 22,
.x = 0, .y = 0,
.r = 255, .g = 220, .b = 64, /* жёлто-оранжевый: читается на любом фоне */
.extra_alpha = 255,
.visible = 0,
};
if (cfc_overlay_create_text(&tc, &e->label_overlay) == 0) {
cfc_composer_add_overlay(comp, e->label_overlay);
} else {
fprintf(stderr, "[cfc/composer] label overlay для '%s' не создан\n", cuframes_key);
e->label_overlay = NULL;
}
comp->pool_count++;
pthread_mutex_unlock(&comp->pool_mu);
fprintf(stderr, "[cfc/composer] pool+ '%s' (frigate=%s prio=%d) total=%d\n",
cuframes_key, frigate_camera ? frigate_camera : "-",
priority, comp->pool_count);
return 0;
}
int cfc_composer_set_motion_mode(cfc_composer_t *comp, int on, int ttl_ms)
{
if (!comp) return -1;
comp->motion_mode = on ? 1 : 0;
if (ttl_ms > 0) comp->motion_ttl_ms = ttl_ms;
fprintf(stderr, "[cfc/composer] motion_mode=%d ttl=%dms pool=%d\n",
comp->motion_mode, comp->motion_ttl_ms, comp->pool_count);
return 0;
}
int cfc_composer_get_motion_mode(cfc_composer_t *comp)
{
return comp ? comp->motion_mode : 0;
}
/* Сверить current_zones с required_zones записи pool'а. */
static int zones_match(const cfc_pool_entry_t *e,
const char *const *current_zones, int n)
{
if (e->required_zones_count == 0) return 1; /* фильтр выключен */
if (n <= 0 || !current_zones) return 0;
for (int i = 0; i < n; i++) {
if (!current_zones[i]) continue;
for (int j = 0; j < e->required_zones_count; j++) {
if (!strcmp(current_zones[i], e->required_zones[j])) return 1;
}
}
return 0;
}
int cfc_composer_motion_pulse(cfc_composer_t *comp,
const char *frigate_camera,
const char *const *current_zones,
int n_zones)
{
if (!comp || !frigate_camera) return -1;
int found = 0;
pthread_mutex_lock(&comp->pool_mu);
int64_t now = now_ms_mono();
for (int i = 0; i < comp->pool_count; i++) {
if (!comp->pool[i].frigate_camera[0]) continue;
if (strcmp(comp->pool[i].frigate_camera, frigate_camera) != 0) continue;
if (!zones_match(&comp->pool[i], current_zones, n_zones)) continue;
atomic_store(&comp->pool[i].last_motion_ms, now);
found = 1;
}
pthread_mutex_unlock(&comp->pool_mu);
return found ? 0 : -1;
}
/* Best-fit selection: минимальный template c nb_camera_cells >= need.
* При ties побеждает выше priority. Если ничего не подходит — самый большой. */
static const cfc_layout_t *pick_best_fit(int need)
{
int n_layouts = 0;
const cfc_layout_t *all = cfc_layout_all(&n_layouts);
if (n_layouts == 0) return NULL;
const cfc_layout_t *best = NULL;
int best_waste = -1;
int best_prio = -1;
for (int i = 0; i < n_layouts; i++) {
const cfc_layout_t *l = &all[i];
if (l->nb_camera_cells < need) continue;
int waste = l->nb_camera_cells - need;
if (best == NULL || waste < best_waste ||
(waste == best_waste && l->priority > best_prio)) {
best = l;
best_waste = waste;
best_prio = l->priority;
}
}
if (best) return best;
/* Overflow: ни один не подходит. Берём с max nb_camera_cells. */
best = &all[0];
for (int i = 1; i < n_layouts; i++) {
if (all[i].nb_camera_cells > best->nb_camera_cells) best = &all[i];
}
return best;
}
/* Motion-mode relayout — Phase 11 Step 2.
*
* Алгоритм:
* 1. active = {pool[i] : (now - last_motion_ms) < ttl}
* 2. sort active by priority DESC
* 3. template = pick_best_fit(|active|)
* 4. распределить active по template.cells[role=CAMERA] в порядке order ASC
* 5. лишние cells.w = 0
* 6. idle (|active|=0): tpl_1 + top-priority из pool
*
* Hysteresis, DEAD-exclusion и widget rendering — следующие step'ы. */
static void compose_motion_relayout(cfc_composer_t *comp)
{
if (!comp->motion_mode) return;
if (comp->pool_count == 0) return;
int64_t now = now_ms_mono();
int active_idx[CFC_COMPOSER_MAX_CELLS];
int active_prio[CFC_COMPOSER_MAX_CELLS];
int n_active = 0;
pthread_mutex_lock(&comp->pool_mu);
for (int i = 0; i < comp->pool_count; i++) {
int64_t last = atomic_load(&comp->pool[i].last_motion_ms);
if (last == 0) continue;
if (now - last > comp->motion_ttl_ms) continue;
active_idx[n_active] = i;
active_prio[n_active] = comp->pool[i].priority;
n_active++;
}
/* Idle: 0 active → tpl_1 + top-priority pool entry. */
if (n_active == 0) {
int best = 0;
for (int i = 1; i < comp->pool_count; i++) {
if (comp->pool[i].priority > comp->pool[best].priority) best = i;
}
active_idx[0] = best;
active_prio[0] = comp->pool[best].priority;
n_active = 1;
}
pthread_mutex_unlock(&comp->pool_mu);
/* Insertion sort by priority DESC (stable). */
for (int i = 1; i < n_active; i++) {
int ki = active_idx[i], kp = active_prio[i];
int j = i - 1;
while (j >= 0 && active_prio[j] < kp) {
active_idx[j + 1] = active_idx[j];
active_prio[j + 1] = active_prio[j];
j--;
}
active_idx[j + 1] = ki;
active_prio[j + 1] = kp;
}
const cfc_layout_t *lay = pick_best_fit(n_active);
if (!lay) return;
/* Если active > template — обрезаем (lowest-priority вылетают). */
int slots = lay->nb_camera_cells;
if (n_active > slots) n_active = slots;
/* Сначала спрятать все labels — активные включим ниже. */
for (int i = 0; i < comp->pool_count; i++) {
if (!comp->pool[i].label_overlay) continue;
cfc_overlay_text_config_t hide = {
.font_path = "/fonts/DejaVuSans-Bold.ttf",
.text = comp->pool[i].label_text, /* тот же текст — без re-render */
.pixel_size = 22,
.r = 255, .g = 220, .b = 64,
.extra_alpha = 255, .visible = 0,
};
cfc_overlay_update_text(comp->pool[i].label_overlay, &hide);
}
/* Распределить активные по camera-cells (в порядке order ASC). */
int W = comp->cfg.width, H = comp->cfg.height;
int placed = 0;
for (int order = 0; order < slots && placed < n_active; order++) {
for (int i = 0; i < lay->nb_cells; i++) {
const cfc_cell_t *c = &lay->cells[i];
if (c->role != CFC_CELL_CAMERA) continue;
if (c->order != order) continue;
int x, y, w, h;
cfc_layout_to_pixels(c, W, H, &x, &y, &w, &h);
comp->cells[placed].source_key = comp->pool[active_idx[placed]].cuframes_key;
comp->cells[placed].x = x; comp->cells[placed].y = y;
comp->cells[placed].w = w; comp->cells[placed].h = h;
/* Включить label в левом-верхнем углу cell. */
cfc_pool_entry_t *pe = &comp->pool[active_idx[placed]];
if (pe->label_overlay) {
cfc_overlay_text_config_t show = {
.font_path = "/fonts/DejaVuSans-Bold.ttf",
.text = pe->label_text, /* тот же текст — без re-render */
.pixel_size = 22,
.x = x + 10, .y = y + 8,
.r = 255, .g = 220, .b = 64,
.extra_alpha = 255, .visible = 1,
};
cfc_overlay_update_text(pe->label_overlay, &show);
}
placed++;
break;
}
}
/* Лишние cells composer'а (за пределами placed) — обнуляем. */
for (int i = placed; i < CFC_COMPOSER_MAX_CELLS; i++) {
comp->cells[i].w = 0;
comp->cells[i].h = 0;
}
comp->num_cells = placed;
/* Сигнатура для лога — меняется при смене template или active set. */
static char last_signature[256];
char sig[256];
int off = snprintf(sig, sizeof(sig), "%s|", lay->name);
for (int i = 0; i < placed && off < (int)sizeof(sig) - 1; i++) {
int n = snprintf(sig + off, sizeof(sig) - off, "%s,",
comp->pool[active_idx[i]].cuframes_key);
if (n <= 0) break;
off += n;
}
if (strcmp(sig, last_signature) != 0) {
strncpy(last_signature, sig, sizeof(last_signature) - 1);
last_signature[sizeof(last_signature) - 1] = '\0';
fprintf(stderr, "[cfc/composer] motion-template='%s' active=%d : %s\n",
lay->name, placed, sig);
}
}
int cfc_composer_get_health(cfc_composer_t *comp, cfc_composer_health_t *out)
{
if (!comp || !out) return -1;
memset(out, 0, sizeof(*out));
out->total = comp->pool_count;
for (int i = 0; i < comp->pool_count; i++) {
if (!comp->pool[i].source) {
out->dead++;
continue;
}
cfc_source_snapshot_t snap;
cfc_source_get_latest(comp->pool[i].source, &snap);
switch (snap.state) {
case CFC_SOURCE_ACTIVE: out->active++; break;
case CFC_SOURCE_STALE: out->stale++; break;
default: out->dead++; break;
}
}
return 0;
}
int cfc_composer_destroy(cfc_composer_t *comp)
{
if (!comp) return 0;
for (int i = 0; i < comp->pool_count; i++) {
if (comp->pool[i].source) cfc_source_destroy(comp->pool[i].source);
}
for (int i = 0; i < comp->num_overlays; i++) {
cfc_overlay_destroy(comp->overlays[i]);
}
pthread_mutex_destroy(&comp->pool_mu);
if (comp->output_ptr) cuMemFree(comp->output_ptr);
free(comp);
return 0;
}
+18
View File
@@ -0,0 +1,18 @@
/* BlankCell — реализация. Чёрный fill в свою геометрию. */
#include "../../include/cuframes_composer/cpp/blank_cell.hpp"
#include "../../include/cuframes_composer/cugrid.h"
namespace cfc {
void BlankCell::draw_content(CUstream stream, NV12Ref& dst)
{
if (geom_.empty()) return;
cfc_cugrid_fill_nv12(
stream,
dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
geom_.x, geom_.y, geom_.w, geom_.h,
16, 128, 128, 255); /* BT.709 black */
}
} // namespace cfc
+50
View File
@@ -0,0 +1,50 @@
/* BorderDecoration — реализация (Phase 11b).
*
* 4 calls cfc_cugrid_fill_nv12 для top/bottom/left/right полос. Координаты
* выравниваются на чётные (NV12 requirement).
*
* Лицензия: LGPL-2.1+
*/
#include "../../include/cuframes_composer/cpp/border_decoration.hpp"
#include "../../include/cuframes_composer/cugrid.h"
namespace cfc {
void BorderDecoration::draw(CUstream stream, NV12Ref& dst, const Rect& p)
{
if (!style_.visible || style_.alpha <= 0 || style_.thickness <= 0) return;
if (p.empty()) return;
int x = p.x, y = p.y, w = p.w, h = p.h;
int t = style_.thickness;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x + w > dst.frame_w) w = dst.frame_w - x;
if (y + h > dst.frame_h) h = dst.frame_h - y;
if (w <= 0 || h <= 0) return;
if (t * 2 > w) t = w / 2;
if (t * 2 > h) t = h / 2;
x &= ~1; y &= ~1; w &= ~1; h &= ~1; t &= ~1;
if (t == 0) t = 2;
/* top */
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
x, y, w, t,
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
/* bottom */
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
x, y + h - t, w, t,
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
/* left */
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
x, y + t, t, h - 2 * t,
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
/* right */
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
x + w - t, y + t, t, h - 2 * t,
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
}
} // namespace cfc
+33
View File
@@ -0,0 +1,33 @@
/* CameraCell — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/camera_cell.hpp"
#include "../../include/cuframes_composer/cugrid.h"
namespace cfc {
void CameraCell::draw_content(CUstream stream, NV12Ref& dst)
{
if (!source_) return;
if (geom_.empty()) return;
cfc_source_snapshot_t snap{};
cfc_source_get_latest(source_, &snap);
if (snap.state != CFC_SOURCE_ACTIVE && snap.state != CFC_SOURCE_STALE) {
return; /* CONNECTING/DEAD/DISCONNECTED — blackout (clear был раньше) */
}
if (snap.width <= 0 || snap.height <= 0) return;
/* Source NV12 layout: Y plane (pitch_y * height) + UV (pitch_y * height/2)
* непрерывно. UV plane = ptr + pitch_y * height. */
CUdeviceptr src_uv = snap.ptr + static_cast<std::size_t>(snap.pitch_y) * snap.height;
cfc_cugrid_resize_nv12(
stream,
snap.ptr, snap.width, snap.height, snap.pitch_y,
src_uv, snap.pitch_uv,
dst.y_ptr, dst.pitch_y,
dst.uv_ptr, dst.pitch_uv,
geom_.x, geom_.y, geom_.w, geom_.h);
}
} // namespace cfc
+392
View File
@@ -0,0 +1,392 @@
/* Composer — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/composer.hpp"
#include "../../include/cuframes_composer/cpp/blank_cell.hpp"
#include "../../include/cuframes_composer/cpp/template_loader.hpp"
#include "../../include/cuframes_composer/cugrid.h"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstring>
namespace cfc {
static std::int64_t now_ms_mono()
{
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
static int round_up_pitch(int w) { return (w + 255) & ~255; }
Composer::Composer(const ComposerConfig& cfg) : cfg_(cfg)
{
pitch_y_ = round_up_pitch(cfg_.width);
pitch_uv_ = pitch_y_;
std::size_t size = static_cast<std::size_t>(pitch_y_) * cfg_.height +
static_cast<std::size_t>(pitch_uv_) * (cfg_.height / 2);
output_ = CudaBuffer(size);
if (!output_.ok()) {
std::fprintf(stderr, "[cfc/composer] cuMemAlloc %zu failed\n", size);
return;
}
cfc_cugrid_init();
/* Templates: грузим через глобальный registry, чтобы hot-reload через
* ABI shim (cfc_layout_load_file из любого треда) был виден компосеру
* на следующем кадре. */
if (!cfg_.templates_path.empty()) {
load_into_current(cfg_.templates_path);
}
/* В Composer держим только snapshot — реальный source истины =
* current_templates(). Снимок обновляется в pick_best_fit на лету. */
templates_ = current_templates();
std::fprintf(stderr, "[cfc/composer] templates loaded: %zu (path='%s')\n",
templates_.size(), cfg_.templates_path.c_str());
}
Composer::~Composer()
{
for (auto* ov : overlays_) {
if (ov) cfc_overlay_destroy(ov);
}
}
int Composer::add_overlay(cfc_overlay_t* ov)
{
if (!ov) return -1;
overlays_.push_back(ov);
return 0;
}
cfc_overlay_t* Composer::find_overlay(const std::string& id) const
{
for (auto* ov : overlays_) {
const char* oid = cfc_overlay_get_id(ov);
if (oid && id == oid) return ov;
}
return nullptr;
}
Composer::Health Composer::get_health() const
{
Health h{};
auto& pool_ref = const_cast<SourcePool&>(pool_);
pool_ref.for_each([&](PoolEntry& e) {
h.total++;
cfc_source_state_t st = e.state();
switch (st) {
case CFC_SOURCE_ACTIVE: h.active++; break;
case CFC_SOURCE_STALE: h.stale++; break;
default: h.dead++; break;
}
});
return h;
}
void Composer::set_manual_cells(const std::vector<std::pair<std::string, Rect>>& cells)
{
manual_cells_ = cells;
manual_applied_ = false; /* compose_frame применит */
}
int Composer::load_templates(const std::string& path)
{
int r = load_into_current(path);
if (r > 0) {
templates_ = current_templates();
}
return r;
}
void Composer::set_motion_mode(bool on, int ttl_ms)
{
motion_mode_ = on;
if (ttl_ms > 0) cfg_.motion_ttl_ms = ttl_ms;
/* invalidate signature чтобы relayout пересчитался. */
committed_signature_.clear();
pending_signature_.clear();
std::fprintf(stderr, "[cfc/composer] motion_mode=%d ttl=%dms pool=%d\n",
motion_mode_ ? 1 : 0, cfg_.motion_ttl_ms, pool_.size());
}
bool Composer::set_layout(const std::string& name)
{
/* В motion-mode set_layout не игнорируется: применяем + freezing motion
* на manual_override_duration_ms_ (default 60s). После — auto возврат. */
const auto& reg = current_templates();
auto it = std::find_if(reg.begin(), reg.end(),
[&](const LayoutTemplate& t) { return t.name == name; });
if (it == reg.end()) {
std::fprintf(stderr, "[cfc/composer] unknown template '%s'\n", name.c_str());
return false;
}
/* Manual mode: всех в pool по priority — не motion-based. */
std::vector<PoolEntry*> snap;
pool_.for_each([&](PoolEntry& e) { snap.push_back(&e); });
std::sort(snap.begin(), snap.end(),
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
std::int64_t now = now_ms_mono();
layout_.apply(*it, snap, cfg_.width, cfg_.height);
committed_signature_ = build_signature(it->name, snap);
committed_at_ms_ = now;
if (motion_mode_) {
manual_override_until_ms_ = now + manual_override_duration_ms_;
std::fprintf(stderr, "[cfc/composer] manual override '%s' до +%dms\n",
it->name.c_str(), manual_override_duration_ms_);
}
return true;
}
const LayoutTemplate* Composer::pick_best_fit(int need) const
{
/* Читаем global registry — hot-reload через cfc_layout_load_file
* подхватывается на следующем кадре без relink composer'а. */
const auto& reg = current_templates();
const LayoutTemplate* best = nullptr;
int best_waste = -1;
int best_prio = -1;
for (auto& t : reg) {
int n = t.nb_camera_cells();
if (n < need) continue;
int waste = n - need;
if (!best || waste < best_waste ||
(waste == best_waste && t.priority > best_prio)) {
best = &t;
best_waste = waste;
best_prio = t.priority;
}
}
if (best) return best;
/* Overflow → largest. */
for (auto& t : reg) {
if (!best || t.nb_camera_cells() > best->nb_camera_cells()) best = &t;
}
return best;
}
std::vector<PoolEntry*> Composer::collect_active() const
{
std::vector<PoolEntry*> active;
std::int64_t now = now_ms_mono();
const_cast<SourcePool&>(pool_).for_each([&](PoolEntry& e) {
if (!e.drawable()) return;
std::int64_t last = e.last_motion_ms.load();
if (last == 0) return;
if (now - last > cfg_.motion_ttl_ms) return;
active.push_back(&e);
});
/* Idle fallback: top-priority drawable как single. */
if (active.empty()) {
PoolEntry* best = nullptr;
const_cast<SourcePool&>(pool_).for_each([&](PoolEntry& e) {
if (!e.drawable()) return;
if (!best || e.priority > best->priority) best = &e;
});
if (best) active.push_back(best);
}
std::sort(active.begin(), active.end(),
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
return active;
}
std::string Composer::build_signature(const std::string& tpl_name,
const std::vector<PoolEntry*>& active)
{
std::string sig = tpl_name + "|";
std::vector<std::string> keys;
keys.reserve(active.size());
for (auto* e : active) keys.push_back(e->cuframes_key);
std::sort(keys.begin(), keys.end());
for (auto& k : keys) { sig += k; sig += ","; }
return sig;
}
void Composer::maybe_relayout()
{
if (!motion_mode_) return;
if (current_templates().empty()) return;
/* Manual override freeze. */
std::int64_t now = now_ms_mono();
if (manual_override_until_ms_ > now) return;
if (manual_override_until_ms_ != 0) {
std::fprintf(stderr, "[cfc/composer] manual override expired, возврат в motion-mode\n");
manual_override_until_ms_ = 0;
committed_signature_.clear(); /* форс relayout */
}
auto active = collect_active();
const LayoutTemplate* tpl = pick_best_fit(static_cast<int>(active.size()));
if (!tpl) return;
/* Cap по template'у */
int cap = tpl->nb_camera_cells();
if (static_cast<int>(active.size()) > cap) active.resize(cap);
/* Если template имеет больше camera-cells чем активных по motion —
* заполнить оставшиеся drawable камерами из pool (по priority),
* которые ещё не вошли в active. Это убирает "чёрные ячейки"
* в asymmetric layouts (tpl_3/5/6/7 + tpl_4 при active<4). */
if (static_cast<int>(active.size()) < cap) {
std::vector<PoolEntry*> already(active.begin(), active.end());
std::vector<PoolEntry*> extras;
const_cast<SourcePool&>(pool_).for_each([&](PoolEntry& e) {
if (!e.drawable()) return;
for (auto* a : already) if (a == &e) return;
extras.push_back(&e);
});
std::sort(extras.begin(), extras.end(),
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
for (auto* e : extras) {
if (static_cast<int>(active.size()) >= cap) break;
active.push_back(e);
}
}
std::string sig = build_signature(tpl->name, active);
if (sig == committed_signature_) {
pending_signature_.clear();
return;
}
/* Рост: active_keys ⊇ committed_keys → switch сразу.
* Сравнение через signature просто — committed set, new set. */
bool is_grow = false;
{
/* parse committed_keys */
auto pos = committed_signature_.find('|');
std::string committed_keys = pos == std::string::npos
? std::string() : committed_signature_.substr(pos + 1);
std::vector<std::string> ckeys;
std::string cur;
for (char c : committed_keys) {
if (c == ',') { if (!cur.empty()) ckeys.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
std::vector<std::string> nkeys;
for (auto* e : active) nkeys.push_back(e->cuframes_key);
std::sort(nkeys.begin(), nkeys.end());
std::sort(ckeys.begin(), ckeys.end());
/* nkeys ⊇ ckeys */
is_grow = std::includes(nkeys.begin(), nkeys.end(),
ckeys.begin(), ckeys.end());
if (is_grow && nkeys.size() == ckeys.size()) is_grow = false; /* идентичны */
}
if (is_grow) {
layout_.apply(*tpl, active, cfg_.width, cfg_.height);
committed_signature_ = sig;
committed_at_ms_ = now;
pending_signature_.clear();
std::fprintf(stderr, "[cfc/composer] grow → template='%s' active=%zu\n",
tpl->name.c_str(), active.size());
return;
}
/* Сжатие — ждём shrink_hysteresis. */
if (sig != pending_signature_) {
pending_signature_ = sig;
pending_first_seen_ms_ = now;
return;
}
if (now - pending_first_seen_ms_ < cfg_.shrink_hysteresis_ms) return;
/* Commit shrink */
layout_.apply(*tpl, active, cfg_.width, cfg_.height);
committed_signature_ = sig;
committed_at_ms_ = now;
pending_signature_.clear();
std::fprintf(stderr, "[cfc/composer] shrink → template='%s' active=%zu\n",
tpl->name.c_str(), active.size());
}
NV12Ref Composer::compose_frame()
{
/* Если manual cells заданы (через C API без motion-mode) — apply один раз. */
if (!motion_mode_ && !manual_cells_.empty() && !manual_applied_) {
/* Build LayoutTemplate из manual cells как inline. */
LayoutTemplate t;
t.name = "manual";
for (std::size_t i = 0; i < manual_cells_.size(); i++) {
CellTemplate c;
/* manual_cells_ хранит pixel Rect — для LayoutTemplate переводим
* обратно в микроячейки. Округление в большую сторону безопасно. */
const Rect& r = manual_cells_[i].second;
c.col = r.x * kGridCols / cfg_.width;
c.row = r.y * kGridRows / cfg_.height;
c.cs = (r.w * kGridCols + cfg_.width - 1) / cfg_.width;
c.rs = (r.h * kGridRows + cfg_.height - 1) / cfg_.height;
c.role = CellRole::Camera;
c.order = static_cast<int>(i);
t.cells.push_back(std::move(c));
}
/* Build active list из pool entries по cuframes_key. */
std::vector<PoolEntry*> snap;
for (auto& kv : manual_cells_) {
PoolEntry* e = pool_.by_key(kv.first);
snap.push_back(e); /* nullptr → BlankCell */
}
layout_.apply(t, snap, cfg_.width, cfg_.height);
committed_signature_ = build_signature(t.name, snap);
committed_at_ms_ = now_ms_mono();
manual_applied_ = true;
}
maybe_relayout();
/* clear */
CUdeviceptr y = output_.ptr();
CUdeviceptr uv = y + static_cast<std::size_t>(pitch_y_) * cfg_.height;
cudaMemsetAsync(reinterpret_cast<void*>(y), cfg_.bg_y,
static_cast<std::size_t>(pitch_y_) * cfg_.height, stream_);
cfc_cugrid_fill_nv12(reinterpret_cast<CUstream>(stream_),
y, pitch_y_, uv, pitch_uv_,
0, 0, cfg_.width, cfg_.height,
cfg_.bg_y, cfg_.bg_u, cfg_.bg_v, 255);
NV12Ref dst;
dst.y_ptr = y;
dst.uv_ptr = uv;
dst.pitch_y = pitch_y_;
dst.pitch_uv = pitch_uv_;
dst.frame_w = cfg_.width;
dst.frame_h = cfg_.height;
layout_.render(reinterpret_cast<CUstream>(stream_), dst);
/* Backward-compat overlays (CLI text/icon, detbox) — поверх Layout. */
for (auto* ov : overlays_) {
if (!ov) continue;
/* Detection box рисуется в координатах cell камеры. Cell может
* перемещаться по экрану при смене layout — синхронизируем cell-geom
* перед каждым draw. */
if (cfc_overlay_get_type(ov) == CFC_OVERLAY_DETECTION_BOXES) {
const char* fcam = cfc_overlay_detbox_camera_key(ov);
if (fcam) {
PoolEntry* e = pool_.by_frigate_camera(fcam);
if (e) {
const Rect* r = layout_.find_camera_cell_rect(e->cuframes_key);
if (r) {
cfc_overlay_detbox_set_cell_geom(ov, r->x, r->y, r->w, r->h);
} else {
/* Камеры нет в текущем layout — скрываем рамки. */
cfc_overlay_detbox_set_cell_geom(ov, 0, 0, 0, 0);
}
}
}
}
cfc_overlay_draw(ov, reinterpret_cast<CUstream>(stream_),
y, pitch_y_, uv, pitch_uv_,
cfg_.width, cfg_.height);
}
return dst;
}
} // namespace cfc
+197
View File
@@ -0,0 +1,197 @@
/* composer_c_api — extern "C" ABI shim для C++ Composer (Phase 11b).
*
* Существующие callers (control.c, frigate_mqtt.c, examples/grid_record.c)
* продолжают использовать prototype cfc_composer_* без изменений. Здесь
* каждый из них транслируется в вызов соответствующего метода cfc::Composer.
*
* Opaque handle cfc_composer_t = cfc::Composer (через reinterpret_cast).
*
* Лицензия: LGPL-2.1+
*/
#include "../../include/cuframes_composer/composer.h"
#include "../../include/cuframes_composer/cpp/composer.hpp"
#include <cstdio>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
namespace {
inline cfc::Composer* as_cpp(cfc_composer_t* h)
{
return reinterpret_cast<cfc::Composer*>(h);
}
inline cfc_composer_t* as_c(cfc::Composer* c)
{
return reinterpret_cast<cfc_composer_t*>(c);
}
} // namespace
extern "C" {
int cfc_composer_create(const cfc_composer_config_t* cfg, cfc_composer_t** out)
{
if (!cfg || !out) return -1;
if (cfg->width <= 0 || cfg->height <= 0) return -1;
cfc::ComposerConfig cpp_cfg;
cpp_cfg.width = cfg->width;
cpp_cfg.height = cfg->height;
cpp_cfg.cuda_device = cfg->cuda_device;
if (cfg->bg_y) cpp_cfg.bg_y = cfg->bg_y;
if (cfg->bg_u) cpp_cfg.bg_u = cfg->bg_u;
if (cfg->bg_v) cpp_cfg.bg_v = cfg->bg_v;
auto* comp = new (std::nothrow) cfc::Composer(cpp_cfg);
if (!comp || !comp->ok()) {
delete comp;
return -1;
}
/* Если caller передал cells через --cell → запоминаем как manual cells.
* Apply отложен до compose_frame (тогда pool уже наполнен через
* add_pool_source). */
if (cfg->cells && cfg->num_cells > 0) {
std::vector<std::pair<std::string, cfc::Rect>> manual;
for (int i = 0; i < cfg->num_cells; i++) {
const auto& c = cfg->cells[i];
if (!c.source_key) continue;
cfc::Rect r;
r.x = c.x; r.y = c.y; r.w = c.w; r.h = c.h;
manual.emplace_back(std::string(c.source_key), r);
}
comp->set_manual_cells(manual);
/* Также добавляем источник в pool автоматически — иначе lookup
* не найдёт его. Priority=0, frigate=none, zones=[]. */
cfc::SourcePool::SubscribeOpts opts;
if (cfg->consumer_prefix && *cfg->consumer_prefix)
opts.consumer_prefix = cfg->consumer_prefix;
if (cfg->reconnect_min_ms) opts.reconnect_min_ms = cfg->reconnect_min_ms;
if (cfg->reconnect_max_ms) opts.reconnect_max_ms = cfg->reconnect_max_ms;
if (cfg->stale_threshold_ms) opts.stale_threshold_ms = cfg->stale_threshold_ms;
if (cfg->dead_threshold_ms) opts.dead_threshold_ms = cfg->dead_threshold_ms;
for (const auto& kv : manual) {
comp->pool().add(kv.first, "", 0, {}, opts);
}
}
std::fprintf(stderr, "[cfc/composer] C++ ABI shim, %dx%d, %d manual cells\n",
cfg->width, cfg->height, cfg->num_cells);
*out = as_c(comp);
return 0;
}
int cfc_composer_compose(cfc_composer_t* h,
CUdeviceptr* out_y_ptr,
int* out_pitch_y,
int* out_width,
int* out_height)
{
if (!h) return -1;
cfc::NV12Ref ref = as_cpp(h)->compose_frame();
if (out_y_ptr) *out_y_ptr = ref.y_ptr;
if (out_pitch_y) *out_pitch_y = ref.pitch_y;
if (out_width) *out_width = ref.frame_w;
if (out_height) *out_height = ref.frame_h;
return 0;
}
int cfc_composer_add_overlay(cfc_composer_t* h, cfc_overlay_t* ov)
{
if (!h) return -1;
return as_cpp(h)->add_overlay(ov);
}
cfc_overlay_t* cfc_composer_find_overlay(cfc_composer_t* h, const char* id)
{
if (!h || !id) return nullptr;
return as_cpp(h)->find_overlay(id);
}
int cfc_composer_set_layout(cfc_composer_t* h, const char* layout_name)
{
if (!h || !layout_name) return -1;
return as_cpp(h)->set_layout(layout_name) ? 0 : -1;
}
const char* cfc_composer_current_layout(cfc_composer_t* h)
{
if (!h) return nullptr;
const std::string& n = as_cpp(h)->current_layout_name();
return n.empty() ? nullptr : n.c_str();
}
int cfc_composer_add_pool_source(cfc_composer_t* h,
const char* cuframes_key,
const char* frigate_camera,
int priority,
const char* required_zones)
{
if (!h || !cuframes_key) return -1;
std::vector<std::string> zones;
if (required_zones && *required_zones) {
std::string cur;
for (const char* p = required_zones; *p; p++) {
if (*p == ':') { if (!cur.empty()) zones.push_back(cur); cur.clear(); }
else cur.push_back(*p);
}
if (!cur.empty()) zones.push_back(cur);
}
cfc::SourcePool::SubscribeOpts opts;
int idx = as_cpp(h)->pool().add(cuframes_key,
frigate_camera ? frigate_camera : "",
priority, zones, opts);
std::fprintf(stderr, "[cfc/composer] pool+ '%s' (frigate=%s prio=%d zones=%zu) → idx=%d\n",
cuframes_key, frigate_camera ? frigate_camera : "-",
priority, zones.size(), idx);
return idx >= 0 ? 0 : -1;
}
int cfc_composer_set_motion_mode(cfc_composer_t* h, int on, int ttl_ms)
{
if (!h) return -1;
as_cpp(h)->set_motion_mode(on != 0, ttl_ms);
return 0;
}
int cfc_composer_get_motion_mode(cfc_composer_t* h)
{
return h ? (as_cpp(h)->motion_mode() ? 1 : 0) : 0;
}
int cfc_composer_motion_pulse(cfc_composer_t* h,
const char* frigate_camera,
const char* const* current_zones,
int n_zones)
{
if (!h || !frigate_camera) return -1;
std::vector<std::string> zones;
for (int i = 0; i < n_zones; i++) {
if (current_zones[i]) zones.emplace_back(current_zones[i]);
}
as_cpp(h)->pool().motion_pulse(frigate_camera, zones);
return 0;
}
int cfc_composer_get_health(cfc_composer_t* h, cfc_composer_health_t* out)
{
if (!h || !out) return -1;
auto hh = as_cpp(h)->get_health();
out->total = hh.total;
out->active = hh.active;
out->stale = hh.stale;
out->dead = hh.dead;
return 0;
}
int cfc_composer_destroy(cfc_composer_t* h)
{
if (h) delete as_cpp(h);
return 0;
}
} // extern "C"
+168
View File
@@ -0,0 +1,168 @@
/* LabelDecoration — реализация (Phase 11b).
*
* UTF-8 → FreeType glyph rendering → RGBA atlas (CPU) → cuMemcpy → CUdeviceptr.
* На draw: cfc_cugrid_blit_rgba_nv12 (existing kernel, zero-copy на GPU side).
*
* Лицензия: LGPL-2.1+
*/
#include "../../include/cuframes_composer/cpp/label_decoration.hpp"
#include "../../include/cuframes_composer/cugrid.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace cfc {
/* UTF-8 декодер — возвращает true если ещё есть данные, advance'ит p. */
static bool utf8_next(const char*& p, std::uint32_t& cp)
{
auto s = reinterpret_cast<const unsigned char*>(p);
if (!*s) return false;
unsigned char c = *s;
if (c < 0x80) { cp = c; p += 1; return true; }
if ((c & 0xE0) == 0xC0 && s[1]) { cp = ((c & 0x1F) << 6) | (s[1] & 0x3F); p += 2; return true; }
if ((c & 0xF0) == 0xE0 && s[1] && s[2]) { cp = ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); p += 3; return true; }
if ((c & 0xF8) == 0xF0 && s[1] && s[2] && s[3]) {
cp = ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
p += 4; return true;
}
cp = 0xFFFD; p += 1; return true;
}
LabelDecoration::LabelDecoration(const std::string& text, const LabelStyle& style)
: text_(text), style_(style)
{
if (FT_Init_FreeType(&ft_lib_) != 0) {
std::fprintf(stderr, "[cfc/label] FT_Init_FreeType failed\n");
return;
}
if (FT_New_Face(ft_lib_, style_.font_path.c_str(), 0, &face_) != 0) {
std::fprintf(stderr, "[cfc/label] FT_New_Face('%s') failed\n",
style_.font_path.c_str());
FT_Done_FreeType(ft_lib_);
ft_lib_ = nullptr;
return;
}
if (FT_Set_Pixel_Sizes(face_, 0, style_.pixel_size) != 0) {
std::fprintf(stderr, "[cfc/label] FT_Set_Pixel_Sizes(%d) failed\n",
style_.pixel_size);
}
rebuild_atlas();
}
LabelDecoration::~LabelDecoration()
{
if (atlas_) cuMemFree(atlas_);
if (face_) FT_Done_Face(face_);
if (ft_lib_) FT_Done_FreeType(ft_lib_);
}
bool LabelDecoration::measure(int& w, int& h, int& ascent) const
{
int width = 0;
int asc = face_->size->metrics.ascender >> 6;
int desc = -(face_->size->metrics.descender >> 6);
if (asc <= 0) asc = face_->size->metrics.height >> 6;
if (desc < 0) desc = 0;
const char* p = text_.c_str();
std::uint32_t cp;
while (utf8_next(p, cp)) {
if (FT_Load_Char(face_, cp, FT_LOAD_DEFAULT) != 0) continue;
width += face_->glyph->advance.x >> 6;
}
if (width <= 0) width = 1;
w = width;
h = asc + desc;
ascent = asc;
return true;
}
void LabelDecoration::render_to_cpu(unsigned char* rgba, int w, int h, int ascent) const
{
std::memset(rgba, 0, static_cast<std::size_t>(w) * h * 4);
int pen_x = 0;
const char* p = text_.c_str();
std::uint32_t cp;
while (utf8_next(p, cp)) {
if (FT_Load_Char(face_, cp, FT_LOAD_RENDER) != 0) continue;
FT_Bitmap* bm = &face_->glyph->bitmap;
int bx = face_->glyph->bitmap_left;
int by = ascent - face_->glyph->bitmap_top;
for (unsigned gy = 0; gy < bm->rows; gy++) {
int dy = by + static_cast<int>(gy);
if (dy < 0 || dy >= h) continue;
for (unsigned gx = 0; gx < bm->width; gx++) {
int dx = pen_x + bx + static_cast<int>(gx);
if (dx < 0 || dx >= w) continue;
unsigned char a = bm->buffer[gy * bm->pitch + gx];
if (!a) continue;
unsigned char* dst = rgba + (static_cast<std::size_t>(dy) * w + dx) * 4;
int ca = dst[3];
int new_a = a + (ca * (255 - a)) / 255;
if (new_a > 0) {
dst[0] = static_cast<unsigned char>((style_.r * a + dst[0] * ca * (255 - a) / 255) / new_a);
dst[1] = static_cast<unsigned char>((style_.g * a + dst[1] * ca * (255 - a) / 255) / new_a);
dst[2] = static_cast<unsigned char>((style_.b * a + dst[2] * ca * (255 - a) / 255) / new_a);
dst[3] = static_cast<unsigned char>(new_a);
}
}
}
pen_x += face_->glyph->advance.x >> 6;
}
}
bool LabelDecoration::rebuild_atlas()
{
if (!face_) return false;
int w = 0, h = 0, ascent = 0;
if (!measure(w, h, ascent)) return false;
if (w <= 0 || h <= 0) return false;
auto cpu = static_cast<unsigned char*>(std::malloc(static_cast<std::size_t>(w) * h * 4));
if (!cpu) return false;
render_to_cpu(cpu, w, h, ascent);
if (atlas_) { cuMemFree(atlas_); atlas_ = 0; }
CUresult cr = cuMemAlloc(&atlas_, static_cast<std::size_t>(w) * h * 4);
if (cr != CUDA_SUCCESS) { std::free(cpu); return false; }
cr = cuMemcpyHtoD(atlas_, cpu, static_cast<std::size_t>(w) * h * 4);
std::free(cpu);
if (cr != CUDA_SUCCESS) {
cuMemFree(atlas_); atlas_ = 0; return false;
}
atlas_w_ = w;
atlas_h_ = h;
atlas_pitch_ = w * 4;
return true;
}
void LabelDecoration::set_text(const std::string& text)
{
if (text == text_) return;
text_ = text;
rebuild_atlas();
}
void LabelDecoration::draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect)
{
if (!style_.visible || !atlas_ || style_.alpha <= 0) return;
int x = parent_rect.x + style_.pad;
int y = parent_rect.y + style_.pad;
x &= ~1; y &= ~1;
if (x >= dst.frame_w || y >= dst.frame_h) return;
cfc_cugrid_blit_rgba_nv12(
stream,
dst.y_ptr, dst.pitch_y,
dst.uv_ptr, dst.pitch_uv,
x, y,
atlas_, atlas_w_, atlas_h_, atlas_pitch_,
style_.alpha);
}
} // namespace cfc
+94
View File
@@ -0,0 +1,94 @@
/* Layout — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/layout.hpp"
#include "../../include/cuframes_composer/cpp/blank_cell.hpp"
#include "../../include/cuframes_composer/cpp/border_decoration.hpp"
#include "../../include/cuframes_composer/cpp/camera_cell.hpp"
#include "../../include/cuframes_composer/cpp/label_decoration.hpp"
#include "../../include/cuframes_composer/cpp/widget_cell.hpp"
#include <algorithm>
#include <cstdio>
namespace cfc {
void Layout::apply(const LayoutTemplate& tpl,
const std::vector<PoolEntry*>& active_sorted,
int frame_w, int frame_h)
{
cells_.clear();
current_name_ = tpl.name;
/* Подготовим cells в порядке camera-template'ов (отсортированных
* по order ASC), чтобы active[0] попадал в order=0 (главная), [1]→order=1. */
std::vector<const CellTemplate*> camera_templates;
std::vector<const CellTemplate*> widget_templates;
camera_templates.reserve(tpl.cells.size());
for (auto& c : tpl.cells) {
if (c.role == CellRole::Camera) camera_templates.push_back(&c);
else widget_templates.push_back(&c);
}
std::sort(camera_templates.begin(), camera_templates.end(),
[](const CellTemplate* a, const CellTemplate* b) {
return a->order < b->order;
});
/* Серая рамка по умолчанию — отделяет ячейки друг от друга. */
BorderStyle border_style;
border_style.thickness = 2;
border_style.color_y = 180;
border_style.color_u = 128;
border_style.color_v = 128;
border_style.alpha = 220;
/* CameraCells */
for (std::size_t i = 0; i < camera_templates.size(); ++i) {
Rect r = to_pixels(*camera_templates[i], frame_w, frame_h);
if (i < active_sorted.size() && active_sorted[i] && active_sorted[i]->source) {
auto cell = std::make_unique<CameraCell>(r, active_sorted[i]->source,
active_sorted[i]->cuframes_key);
cell->add_decoration(std::make_unique<BorderDecoration>(border_style));
/* Label с именем камеры и приоритетом. */
char label_buf[96];
std::snprintf(label_buf, sizeof(label_buf), "%s prio=%d",
active_sorted[i]->cuframes_key.c_str(),
active_sorted[i]->priority);
cell->add_decoration(std::make_unique<LabelDecoration>(label_buf, LabelStyle{}));
cells_.push_back(std::move(cell));
} else {
/* Нет active под этот слот → blank. */
auto cell = std::make_unique<BlankCell>(r);
cell->add_decoration(std::make_unique<BorderDecoration>(border_style));
cells_.push_back(std::move(cell));
}
}
/* WidgetCells */
for (auto* wt : widget_templates) {
Rect r = to_pixels(*wt, frame_w, frame_h);
auto cell = std::make_unique<WidgetCell>(r, wt->widget);
cell->add_decoration(std::make_unique<BorderDecoration>(border_style));
if (!wt->widget.empty()) {
cell->add_decoration(std::make_unique<LabelDecoration>(wt->widget, LabelStyle{}));
}
cells_.push_back(std::move(cell));
}
}
void Layout::render(CUstream stream, NV12Ref& dst)
{
for (auto& c : cells_) c->draw(stream, dst);
}
const Rect* Layout::find_camera_cell_rect(const std::string& source_key) const
{
for (auto& c : cells_) {
auto* cc = dynamic_cast<const CameraCell*>(c.get());
if (cc && cc->source_key() == source_key) {
return &cc->geometry();
}
}
return nullptr;
}
} // namespace cfc
+136
View File
@@ -0,0 +1,136 @@
/* layouts_c_api — extern "C" ABI shim над template_loader (Phase 11b).
*
* Сохраняет совместимость с control.c::cmd_list_layouts/_get_layout/_set_layout
* через старый интерфейс cfc_layout_find / cfc_layout_all.
*
* Стратегия: static cache из cfc_layout_t structs, заполняется при
* load_file/reload. cfc_layout_find/_all возвращают указатели в этот cache.
*
* Лицензия: LGPL-2.1+
*/
#include "../../include/cuframes_composer/layouts.h"
#include "../../include/cuframes_composer/cpp/template.hpp"
#include "../../include/cuframes_composer/cpp/template_loader.hpp"
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <vector>
namespace {
static std::mutex g_mu;
static std::vector<cfc_layout_t> g_c_cache;
static std::string g_loaded_path;
static std::vector<cfc::LayoutTemplate> g_cpp_cache;
void rebuild_c_cache_locked()
{
g_c_cache.clear();
g_c_cache.reserve(g_cpp_cache.size());
for (const auto& t : g_cpp_cache) {
cfc_layout_t l{};
std::strncpy(l.name, t.name.c_str(), sizeof(l.name) - 1);
l.priority = t.priority;
l.nb_cells = static_cast<int>(t.cells.size());
if (l.nb_cells > CFC_LAYOUT_MAX_CELLS) l.nb_cells = CFC_LAYOUT_MAX_CELLS;
l.nb_camera_cells = 0;
for (int i = 0; i < l.nb_cells; i++) {
const auto& c = t.cells[i];
l.cells[i].col = c.col;
l.cells[i].row = c.row;
l.cells[i].cs = c.cs;
l.cells[i].rs = c.rs;
l.cells[i].role = (c.role == cfc::CellRole::Widget)
? CFC_CELL_WIDGET : CFC_CELL_CAMERA;
l.cells[i].order = c.order;
std::strncpy(l.cells[i].widget, c.widget.c_str(),
sizeof(l.cells[i].widget) - 1);
if (l.cells[i].role == CFC_CELL_CAMERA) l.nb_camera_cells++;
}
g_c_cache.push_back(l);
}
}
void ensure_loaded_locked()
{
/* Источник истины = global registry; кеш C-структур пересинхронизируется
* каждый раз когда состав изменился (поэтому простая проверка empty
* не годится — может появиться обновление через load_file). */
g_cpp_cache = cfc::current_templates();
rebuild_c_cache_locked();
}
} // namespace
extern "C" {
const cfc_layout_t* cfc_layout_find(const char* name)
{
if (!name) return nullptr;
std::lock_guard<std::mutex> lk(g_mu);
ensure_loaded_locked();
for (const auto& l : g_c_cache) {
if (std::strcmp(l.name, name) == 0) return &l;
}
return nullptr;
}
const cfc_layout_t* cfc_layout_all(int* out_count)
{
std::lock_guard<std::mutex> lk(g_mu);
ensure_loaded_locked();
if (out_count) *out_count = static_cast<int>(g_c_cache.size());
return g_c_cache.data();
}
void cfc_layout_to_pixels(const cfc_cell_t* cell, int W, int H,
int* out_x, int* out_y, int* out_w, int* out_h)
{
if (!cell) return;
int x = (cell->col * W) / CFC_GRID_COLS;
int y = (cell->row * H) / CFC_GRID_ROWS;
int w = (cell->cs * W) / CFC_GRID_COLS;
int h = (cell->rs * H) / CFC_GRID_ROWS;
x &= ~1; y &= ~1; w &= ~1; h &= ~1;
if (x + w > W) w = W - x;
if (y + h > H) h = H - y;
if (out_x) *out_x = x;
if (out_y) *out_y = y;
if (out_w) *out_w = w;
if (out_h) *out_h = h;
}
int cfc_layout_load_file(const char* path)
{
if (!path) return -3;
int r = cfc::load_into_current(path); /* обновит global registry */
if (r > 0) {
std::lock_guard<std::mutex> lk(g_mu);
g_cpp_cache = cfc::current_templates();
rebuild_c_cache_locked();
g_loaded_path = path;
}
return r;
}
int cfc_layout_reload(void)
{
std::string path;
{
std::lock_guard<std::mutex> lk(g_mu);
path = g_loaded_path;
}
if (path.empty()) return -1;
return cfc_layout_load_file(path.c_str());
}
const char* cfc_layout_loaded_path(void)
{
std::lock_guard<std::mutex> lk(g_mu);
return g_loaded_path.empty() ? nullptr : g_loaded_path.c_str();
}
} // extern "C"
+317
View File
@@ -0,0 +1,317 @@
/* MqttOverlay — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/mqtt_overlay.hpp"
#include <json-c/json.h>
#include <mosquitto.h>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
namespace cfc {
// ── MqttOverlayItem ──────────────────────────────────────────────────────
MqttOverlayItem::MqttOverlayItem(const MqttOverlayCfg& cfg,
const MqttBrokerCfg& broker,
int frame_w, int frame_h)
: cfg_(cfg), broker_(broker), frame_w_(frame_w), frame_h_(frame_h)
{
mosquitto_lib_init();
}
MqttOverlayItem::~MqttOverlayItem()
{
running_.store(false);
if (mosq_) {
mosquitto_disconnect(mosq_);
mosquitto_loop_stop(mosq_, true);
mosquitto_destroy(mosq_);
mosq_ = nullptr;
}
/* Overlay ownership — Composer; не уничтожаем. */
}
void MqttOverlayItem::on_connect(struct mosquitto* m, void* user, int rc)
{
auto* self = static_cast<MqttOverlayItem*>(user);
if (rc == 0) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] connected, subscribe '%s'\n",
self->cfg_.id.c_str(), self->cfg_.topic.c_str());
mosquitto_subscribe(m, nullptr, self->cfg_.topic.c_str(), 0);
} else {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] connect failed: %s\n",
self->cfg_.id.c_str(), mosquitto_connack_string(rc));
}
}
void MqttOverlayItem::on_message(struct mosquitto*, void* user,
const struct mosquitto_message* msg)
{
auto* self = static_cast<MqttOverlayItem*>(user);
if (!msg || !msg->payload || msg->payloadlen <= 0) return;
self->handle_payload(static_cast<const char*>(msg->payload),
static_cast<std::size_t>(msg->payloadlen));
}
void MqttOverlayItem::handle_payload(const char* payload, std::size_t len)
{
std::string buf(payload, len);
std::string text;
if (!cfg_.json_field.empty()) {
struct json_object* root = json_tokener_parse(buf.c_str());
if (!root) return;
struct json_object* v = nullptr;
json_object_object_get_ex(root, cfg_.json_field.c_str(), &v);
if (!v) { json_object_put(root); return; }
char tmp[128];
/* Если значение numeric — извлекаем как double, форматируем
* printf'ом. Если string — как %s. */
if (json_object_is_type(v, json_type_double) ||
json_object_is_type(v, json_type_int)) {
double d = json_object_get_double(v);
std::snprintf(tmp, sizeof(tmp), cfg_.format.c_str(), d);
} else {
const char* s = json_object_get_string(v);
std::snprintf(tmp, sizeof(tmp), cfg_.format.c_str(), s ? s : "");
}
json_object_put(root);
text = tmp;
} else {
/* Raw payload — format должен быть "%s" или совместимый. */
char tmp[256];
std::snprintf(tmp, sizeof(tmp), cfg_.format.c_str(), buf.c_str());
text = tmp;
}
update_text(text);
}
void MqttOverlayItem::update_text(const std::string& text)
{
if (text == last_text_) return;
last_text_ = text;
if (!overlay_) return;
cfc_overlay_text_config_t tc{};
tc.font_path = cfg_.font_path.c_str();
tc.text = text.c_str();
tc.pixel_size = cfg_.pixel_size;
tc.x = 0; tc.y = 0;
tc.r = cfg_.r; tc.g = cfg_.g; tc.b = cfg_.b;
tc.extra_alpha = cfg_.alpha;
tc.visible = 1;
tc.bg_alpha = cfg_.bg_alpha;
tc.bg_y = cfg_.bg_y; tc.bg_u = cfg_.bg_u; tc.bg_v = cfg_.bg_v;
tc.bg_pad = cfg_.bg_pad;
cfc_overlay_update_text(overlay_, &tc);
reposition_overlay();
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] '%s'\n",
cfg_.id.c_str(), text.c_str());
}
void MqttOverlayItem::reposition_overlay()
{
if (!overlay_) return;
int w = 0, h = 0;
cfc_overlay_text_size(overlay_, &w, &h);
if (w <= 0 || h <= 0) return;
int x = 0, y = 0;
if (cfg_.anchor == "right-bottom") {
x = frame_w_ - w - cfg_.margin_x;
y = frame_h_ - h - cfg_.margin_y;
} else if (cfg_.anchor == "right-top") {
x = frame_w_ - w - cfg_.margin_x;
y = cfg_.margin_y;
} else if (cfg_.anchor == "left-bottom") {
x = cfg_.margin_x;
y = frame_h_ - h - cfg_.margin_y;
} else if (cfg_.anchor == "left-top") {
x = cfg_.margin_x;
y = cfg_.margin_y;
} else if (cfg_.anchor == "center") {
x = (frame_w_ - w) / 2;
y = (frame_h_ - h) / 2;
} else {
x = cfg_.margin_x; y = cfg_.margin_y;
}
x &= ~1; y &= ~1;
if (x < 0) x = 0;
if (y < 0) y = 0;
cfc_overlay_text_config_t tc{};
tc.font_path = cfg_.font_path.c_str();
tc.text = last_text_.c_str();
tc.pixel_size = cfg_.pixel_size;
tc.x = x; tc.y = y;
tc.r = cfg_.r; tc.g = cfg_.g; tc.b = cfg_.b;
tc.extra_alpha = cfg_.alpha;
tc.visible = 1;
tc.bg_alpha = cfg_.bg_alpha;
tc.bg_y = cfg_.bg_y; tc.bg_u = cfg_.bg_u; tc.bg_v = cfg_.bg_v;
tc.bg_pad = cfg_.bg_pad;
cfc_overlay_update_text(overlay_, &tc);
}
bool MqttOverlayItem::start()
{
/* Persistent text overlay — сразу visible=1 с placeholder, чтобы было
* видно (с подложкой) даже без MQTT-сообщения. */
const std::string ph = cfg_.placeholder.empty() ? std::string("") : cfg_.placeholder;
cfc_overlay_text_config_t tc{};
tc.font_path = cfg_.font_path.c_str();
tc.text = ph.c_str();
tc.pixel_size = cfg_.pixel_size;
tc.x = cfg_.margin_x; tc.y = cfg_.margin_y;
tc.r = cfg_.r; tc.g = cfg_.g; tc.b = cfg_.b;
tc.extra_alpha = cfg_.alpha;
tc.visible = 1;
tc.bg_alpha = cfg_.bg_alpha;
tc.bg_y = cfg_.bg_y; tc.bg_u = cfg_.bg_u; tc.bg_v = cfg_.bg_v;
tc.bg_pad = cfg_.bg_pad;
if (cfc_overlay_create_text(&tc, &overlay_) != 0) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] create_text failed (font '%s')\n",
cfg_.id.c_str(), cfg_.font_path.c_str());
return false;
}
cfc_overlay_set_id(overlay_, cfg_.id.c_str());
last_text_ = ph;
reposition_overlay(); /* поставить в anchor сразу */
/* MQTT subscriber. */
char cid[64];
std::snprintf(cid, sizeof(cid), "composer-overlay-%s-%p",
cfg_.id.c_str(), static_cast<void*>(this));
mosq_ = mosquitto_new(cid, true, this);
if (!mosq_) return false;
if (!broker_.username.empty()) {
mosquitto_username_pw_set(mosq_, broker_.username.c_str(),
broker_.password.empty() ? nullptr : broker_.password.c_str());
}
mosquitto_connect_callback_set(mosq_, &MqttOverlayItem::on_connect);
mosquitto_message_callback_set(mosq_, &MqttOverlayItem::on_message);
mosquitto_reconnect_delay_set(mosq_, 1, 30, true);
int r = mosquitto_connect_async(mosq_, broker_.host.c_str(), broker_.port, 60);
if (r != MOSQ_ERR_SUCCESS) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] connect_async failed: %s\n",
cfg_.id.c_str(), mosquitto_strerror(r));
return false;
}
r = mosquitto_loop_start(mosq_);
if (r != MOSQ_ERR_SUCCESS) {
std::fprintf(stderr, "[cfc/mqtt-overlay/%s] loop_start failed: %s\n",
cfg_.id.c_str(), mosquitto_strerror(r));
return false;
}
running_.store(true);
return true;
}
// ── MqttOverlayManager ───────────────────────────────────────────────────
namespace {
const char* jstr(struct json_object* o, const char* k, const char* def = "")
{
struct json_object* v;
if (!json_object_object_get_ex(o, k, &v)) return def;
return json_object_get_string(v);
}
int jint(struct json_object* o, const char* k, int def)
{
struct json_object* v;
if (!json_object_object_get_ex(o, k, &v)) return def;
return json_object_get_int(v);
}
} // namespace
int MqttOverlayManager::load_from_file(const std::string& path, int W, int H)
{
std::ifstream f(path);
if (!f.is_open()) {
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: open failed\n", path.c_str());
return -3;
}
std::stringstream ss; ss << f.rdbuf();
std::string buf = ss.str();
struct json_object* root = json_tokener_parse(buf.c_str());
if (!root) {
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: parse failed\n", path.c_str());
return -1;
}
struct json_object* jarr = nullptr;
if (!json_object_object_get_ex(root, "overlays", &jarr) ||
!json_object_is_type(jarr, json_type_array)) {
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: 'overlays' missing\n", path.c_str());
json_object_put(root);
return -2;
}
clear();
int n = static_cast<int>(json_object_array_length(jarr));
for (int i = 0; i < n; i++) {
struct json_object* jo = json_object_array_get_idx(jarr, i);
if (!jo) continue;
MqttOverlayCfg cfg;
cfg.id = jstr(jo, "id", "");
cfg.topic = jstr(jo, "topic", "");
cfg.json_field = jstr(jo, "json_field", "");
cfg.format = jstr(jo, "format", "%s");
cfg.anchor = jstr(jo, "anchor", "right-bottom");
cfg.margin_x = jint(jo, "margin_x", 32);
cfg.margin_y = jint(jo, "margin_y", 24);
cfg.pixel_size = jint(jo, "pixel_size", 32);
cfg.alpha = jint(jo, "alpha", 230);
cfg.bg_alpha = jint(jo, "bg_alpha", 160);
cfg.bg_y = jint(jo, "bg_y", 16);
cfg.bg_u = jint(jo, "bg_u", 128);
cfg.bg_v = jint(jo, "bg_v", 128);
cfg.bg_pad = jint(jo, "bg_pad", 10);
const char* ph = jstr(jo, "placeholder", "");
if (*ph) cfg.placeholder = ph;
const char* fp = jstr(jo, "font_path", "");
if (*fp) cfg.font_path = fp;
struct json_object* jcolor = nullptr;
if (json_object_object_get_ex(jo, "color", &jcolor) &&
json_object_is_type(jcolor, json_type_array) &&
json_object_array_length(jcolor) >= 3) {
cfg.r = json_object_get_int(json_object_array_get_idx(jcolor, 0));
cfg.g = json_object_get_int(json_object_array_get_idx(jcolor, 1));
cfg.b = json_object_get_int(json_object_array_get_idx(jcolor, 2));
}
if (cfg.id.empty() || cfg.topic.empty()) {
std::fprintf(stderr, "[cfc/mqtt-overlay] entry[%d] без id/topic — skip\n", i);
continue;
}
auto item = std::make_unique<MqttOverlayItem>(cfg, broker_, W, H);
if (item->start()) items_.push_back(std::move(item));
}
json_object_put(root);
std::fprintf(stderr, "[cfc/mqtt-overlay] %s: started %zu overlays\n",
path.c_str(), items_.size());
return static_cast<int>(items_.size());
}
std::vector<cfc_overlay_t*> MqttOverlayManager::overlay_handles() const
{
std::vector<cfc_overlay_t*> v;
v.reserve(items_.size());
for (auto& i : items_) v.push_back(i->overlay());
return v;
}
void MqttOverlayManager::clear()
{
items_.clear();
}
} // namespace cfc
+45
View File
@@ -0,0 +1,45 @@
/* C wrapper для MqttOverlayManager (Phase 11b). */
#include "../../include/cuframes_composer/composer.h"
#include "../../include/cuframes_composer/cpp/mqtt_overlay.hpp"
#include <memory>
namespace {
std::unique_ptr<cfc::MqttOverlayManager> g_mgr;
}
extern "C" {
int cfc_mqtt_overlays_load(cfc_composer_t* composer,
const char* path,
const char* mqtt_host, int mqtt_port,
const char* mqtt_user, const char* mqtt_pass,
int frame_w, int frame_h)
{
if (!composer || !path) return -1;
cfc::MqttBrokerCfg br;
if (mqtt_host) br.host = mqtt_host;
if (mqtt_port > 0) br.port = mqtt_port;
if (mqtt_user) br.username = mqtt_user;
if (mqtt_pass) br.password = mqtt_pass;
g_mgr = std::make_unique<cfc::MqttOverlayManager>(br);
int n = g_mgr->load_from_file(path, frame_w, frame_h);
if (n <= 0) {
g_mgr.reset();
return n;
}
for (cfc_overlay_t* ov : g_mgr->overlay_handles()) {
cfc_composer_add_overlay(composer, ov);
}
return n;
}
void cfc_mqtt_overlays_stop(void)
{
g_mgr.reset();
}
} // extern "C"
+110
View File
@@ -0,0 +1,110 @@
/* SourcePool — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/source_pool.hpp"
#include <algorithm>
#include <chrono>
#include <cstdio>
namespace cfc {
static std::int64_t now_ms_mono()
{
using namespace std::chrono;
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
SourcePool::~SourcePool()
{
for (auto& e : entries_) {
if (e->source) cfc_source_destroy(e->source);
}
}
int SourcePool::add(const std::string& key,
const std::string& fcam,
int priority,
const std::vector<std::string>& zones,
const SubscribeOpts& opts)
{
std::lock_guard<std::mutex> lk(mu_);
/* duplicate guard */
for (auto& e : entries_) {
if (e->cuframes_key == key) {
e->frigate_camera = fcam;
e->priority = priority;
e->required_zones = zones;
return -1;
}
}
auto e = std::make_unique<PoolEntry>();
e->cuframes_key = key;
e->frigate_camera = fcam;
e->priority = priority;
e->required_zones = zones;
e->last_motion_ms.store(0);
char consumer_name[64];
std::snprintf(consumer_name, sizeof(consumer_name), "%s-%zu",
opts.consumer_prefix.c_str(), entries_.size());
cfc_source_config_t cfg{};
cfg.key = e->cuframes_key.c_str();
cfg.consumer_name = consumer_name;
cfg.cuda_device = opts.cuda_device;
cfg.reconnect_min_ms = opts.reconnect_min_ms;
cfg.reconnect_max_ms = opts.reconnect_max_ms;
cfg.stale_threshold_ms = opts.stale_threshold_ms;
cfg.dead_threshold_ms = opts.dead_threshold_ms;
if (cfc_source_create(&cfg, &e->source) != 0) {
std::fprintf(stderr, "[cfc/pool] subscribe '%s' failed — будет blackout\n",
key.c_str());
e->source = nullptr;
}
int idx = static_cast<int>(entries_.size());
entries_.push_back(std::move(e));
return idx;
}
PoolEntry* SourcePool::by_key(const std::string& key)
{
for (auto& e : entries_) {
if (e->cuframes_key == key) return e.get();
}
return nullptr;
}
PoolEntry* SourcePool::by_frigate_camera(const std::string& fcam)
{
for (auto& e : entries_) {
if (e->frigate_camera == fcam) return e.get();
}
return nullptr;
}
void SourcePool::motion_pulse(const std::string& frigate_camera,
const std::vector<std::string>& current_zones)
{
std::lock_guard<std::mutex> lk(mu_);
std::int64_t now = now_ms_mono();
for (auto& e : entries_) {
if (e->frigate_camera != frigate_camera) continue;
/* Zone-filter — пропускаем если есть required_zones и не пересекаются. */
if (!e->required_zones.empty()) {
bool match = false;
for (auto& cz : current_zones) {
if (std::find(e->required_zones.begin(),
e->required_zones.end(), cz) != e->required_zones.end()) {
match = true; break;
}
}
if (!match) continue;
}
e->last_motion_ms.store(now);
}
}
} // namespace cfc
+172
View File
@@ -0,0 +1,172 @@
/* Template loader — реализация (Phase 11b). */
#include "../../include/cuframes_composer/cpp/template_loader.hpp"
#include <json-c/json.h>
#include <cstdio>
#include <fstream>
#include <mutex>
#include <sstream>
namespace cfc {
namespace {
int json_int(struct json_object* obj, const char* key, int def)
{
struct json_object* v;
if (!json_object_object_get_ex(obj, key, &v)) return def;
return json_object_get_int(v);
}
const char* json_str(struct json_object* obj, const char* key)
{
struct json_object* v;
if (!json_object_object_get_ex(obj, key, &v)) return nullptr;
return json_object_get_string(v);
}
CellRole parse_role(const char* s)
{
if (s && std::string(s) == "widget") return CellRole::Widget;
return CellRole::Camera;
}
bool parse_template(struct json_object* jt, LayoutTemplate& out)
{
const char* name = json_str(jt, "name");
if (!name) return false;
out.name = name;
out.priority = json_int(jt, "priority", 0);
struct json_object* jcells;
if (!json_object_object_get_ex(jt, "cells", &jcells) ||
!json_object_is_type(jcells, json_type_array)) return false;
int n = static_cast<int>(json_object_array_length(jcells));
for (int i = 0; i < n; i++) {
struct json_object* jc = json_object_array_get_idx(jcells, i);
if (!jc) continue;
CellTemplate c;
c.col = json_int(jc, "col", 0);
c.row = json_int(jc, "row", 0);
c.cs = json_int(jc, "cs", 1);
c.rs = json_int(jc, "rs", 1);
c.role = parse_role(json_str(jc, "role"));
c.order = json_int(jc, "order", 0);
const char* w = json_str(jc, "widget");
if (w) c.widget = w;
/* bounds */
if (c.cs < 1 || c.rs < 1 || c.col < 0 || c.row < 0 ||
c.col + c.cs > kGridCols || c.row + c.rs > kGridRows) {
std::fprintf(stderr, "[cfc/loader] '%s' cell[%d] outside 8×8 — skip\n",
name, i);
continue;
}
out.cells.push_back(std::move(c));
}
return !out.cells.empty();
}
} // namespace
int load_templates_from_file(const std::string& path, std::vector<LayoutTemplate>& out)
{
std::ifstream f(path);
if (!f.is_open()) {
std::fprintf(stderr, "[cfc/loader] %s: open failed\n", path.c_str());
return -3;
}
std::stringstream ss;
ss << f.rdbuf();
std::string buf = ss.str();
struct json_object* root = json_tokener_parse(buf.c_str());
if (!root) {
std::fprintf(stderr, "[cfc/loader] %s: JSON parse failed\n", path.c_str());
return -1;
}
struct json_object* jtpls;
if (!json_object_object_get_ex(root, "templates", &jtpls) ||
!json_object_is_type(jtpls, json_type_array)) {
std::fprintf(stderr, "[cfc/loader] %s: 'templates' missing\n", path.c_str());
json_object_put(root);
return -2;
}
int n = static_cast<int>(json_object_array_length(jtpls));
std::vector<LayoutTemplate> tmp;
for (int i = 0; i < n; i++) {
struct json_object* jt = json_object_array_get_idx(jtpls, i);
LayoutTemplate t;
if (parse_template(jt, t)) tmp.push_back(std::move(t));
}
json_object_put(root);
if (tmp.empty()) {
std::fprintf(stderr, "[cfc/loader] %s: no valid templates\n", path.c_str());
return -2;
}
out = std::move(tmp);
std::fprintf(stderr, "[cfc/loader] %s: loaded %zu templates\n",
path.c_str(), out.size());
return static_cast<int>(out.size());
}
std::vector<LayoutTemplate> builtin_templates()
{
std::vector<LayoutTemplate> v;
/* tpl_1: одна камера во весь экран. */
{
LayoutTemplate t; t.name = "tpl_1"; t.priority = 0;
t.cells.push_back({0, 0, 8, 8, CellRole::Camera, 0, ""});
v.push_back(std::move(t));
}
/* tpl_4: quad 2×2 — 4 камеры 16:9. */
{
LayoutTemplate t; t.name = "tpl_4"; t.priority = 0;
t.cells.push_back({0, 0, 4, 4, CellRole::Camera, 0, ""});
t.cells.push_back({4, 0, 4, 4, CellRole::Camera, 1, ""});
t.cells.push_back({0, 4, 4, 4, CellRole::Camera, 2, ""});
t.cells.push_back({4, 4, 4, 4, CellRole::Camera, 3, ""});
v.push_back(std::move(t));
}
return v;
}
/* ── Global registry ─────────────────────────────────────────────────── */
namespace {
std::mutex g_reg_mu;
std::vector<LayoutTemplate> g_registry;
void ensure_registry_locked()
{
if (g_registry.empty()) g_registry = builtin_templates();
}
} // namespace
const std::vector<LayoutTemplate>& current_templates()
{
std::lock_guard<std::mutex> lk(g_reg_mu);
ensure_registry_locked();
return g_registry;
}
void set_current_templates(std::vector<LayoutTemplate> new_templates)
{
if (new_templates.empty()) return;
std::lock_guard<std::mutex> lk(g_reg_mu);
g_registry = std::move(new_templates);
}
int load_into_current(const std::string& path)
{
std::vector<LayoutTemplate> v;
int r = load_templates_from_file(path, v);
if (r > 0) {
set_current_templates(std::move(v));
}
return r;
}
} // namespace cfc
+24
View File
@@ -0,0 +1,24 @@
/* WidgetCell — реализация (Phase 11b MVP).
* Тёмный fill + label с именем widget'а в углу через LabelDecoration.
*
* Сам label-overlay создаётся при Layout::apply_template и добавляется как
* decoration. Здесь только content — фон cell.
*/
#include "../../include/cuframes_composer/cpp/widget_cell.hpp"
#include "../../include/cuframes_composer/cugrid.h"
namespace cfc {
void WidgetCell::draw_content(CUstream stream, NV12Ref& dst)
{
if (geom_.empty()) return;
/* Тёмно-серый Y=40, UV=128 (нейтральный). */
cfc_cugrid_fill_nv12(
stream,
dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
geom_.x, geom_.y, geom_.w, geom_.h,
40, 128, 128, 255);
}
} // namespace cfc
-275
View File
@@ -1,275 +0,0 @@
/* Layout-templates на 8×8 микро-сетке (Phase 11).
*
* Step 3: JSON-based templates + hot-reload через ZMQ. Built-in templates
* остаются как fallback (если JSON-файл недоступен).
*
* Лицензия: LGPL-2.1+
*/
#include "../include/cuframes_composer/layouts.h"
#include <errno.h>
#include <json-c/json.h>
#include <pthread.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static cfc_layout_t g_layouts[64];
static int g_layouts_count = 0;
static char g_loaded_path[512] = {0};
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static void recount_camera_cells(cfc_layout_t *l)
{
int n = 0;
for (int i = 0; i < l->nb_cells; i++) {
if (l->cells[i].role == CFC_CELL_CAMERA) n++;
}
l->nb_camera_cells = n;
}
static cfc_layout_t *push_layout(const char *name, int priority)
{
if (g_layouts_count >= (int)(sizeof(g_layouts) / sizeof(g_layouts[0]))) return NULL;
cfc_layout_t *l = &g_layouts[g_layouts_count++];
memset(l, 0, sizeof(*l));
strncpy(l->name, name, sizeof(l->name) - 1);
l->name[sizeof(l->name) - 1] = '\0';
l->priority = priority;
return l;
}
static void push_cell(cfc_layout_t *l, int col, int row, int cs, int rs,
cfc_cell_role_t role, int order, const char *widget)
{
if (l->nb_cells >= CFC_LAYOUT_MAX_CELLS) return;
cfc_cell_t *c = &l->cells[l->nb_cells++];
c->col = col; c->row = row;
c->cs = cs; c->rs = rs;
c->role = role;
c->order = order;
if (widget) {
strncpy(c->widget, widget, sizeof(c->widget) - 1);
c->widget[sizeof(c->widget) - 1] = '\0';
} else {
c->widget[0] = '\0';
}
}
/* Built-in fallback templates — используются если JSON не загружен. */
static void load_builtin_layouts_locked(void)
{
g_layouts_count = 0;
cfc_layout_t *l;
/* tpl_1 — одна камера во весь экран. */
l = push_layout("tpl_1", 0);
push_cell(l, 0, 0, 8, 8, CFC_CELL_CAMERA, 0, NULL);
recount_camera_cells(l);
/* tpl_4 — quad 2×2 (cells 4×4 микроячейки, 960×540 16:9). */
l = push_layout("tpl_4", 0);
push_cell(l, 0, 0, 4, 4, CFC_CELL_CAMERA, 0, NULL);
push_cell(l, 4, 0, 4, 4, CFC_CELL_CAMERA, 1, NULL);
push_cell(l, 0, 4, 4, 4, CFC_CELL_CAMERA, 2, NULL);
push_cell(l, 4, 4, 4, 4, CFC_CELL_CAMERA, 3, NULL);
recount_camera_cells(l);
}
static void ensure_loaded(void)
{
if (g_layouts_count > 0) return;
load_builtin_layouts_locked();
}
const cfc_layout_t *cfc_layout_find(const char *name)
{
if (!name) return NULL;
pthread_mutex_lock(&g_mu);
ensure_loaded();
const cfc_layout_t *found = NULL;
for (int i = 0; i < g_layouts_count; i++) {
if (!strcmp(g_layouts[i].name, name)) { found = &g_layouts[i]; break; }
}
pthread_mutex_unlock(&g_mu);
return found;
}
const cfc_layout_t *cfc_layout_all(int *out_count)
{
pthread_mutex_lock(&g_mu);
ensure_loaded();
if (out_count) *out_count = g_layouts_count;
const cfc_layout_t *p = g_layouts;
pthread_mutex_unlock(&g_mu);
return p;
}
void cfc_layout_to_pixels(const cfc_cell_t *cell, int W, int H,
int *out_x, int *out_y, int *out_w, int *out_h)
{
if (!cell) return;
int x = (cell->col * W) / CFC_GRID_COLS;
int y = (cell->row * H) / CFC_GRID_ROWS;
int w = (cell->cs * W) / CFC_GRID_COLS;
int h = (cell->rs * H) / CFC_GRID_ROWS;
x &= ~1; y &= ~1; w &= ~1; h &= ~1;
if (x + w > W) w = W - x;
if (y + h > H) h = H - y;
if (out_x) *out_x = x;
if (out_y) *out_y = y;
if (out_w) *out_w = w;
if (out_h) *out_h = h;
}
/* ── JSON loader ──────────────────────────────────────────────────────── */
static int parse_role(const char *s)
{
if (!s) return CFC_CELL_CAMERA;
if (!strcmp(s, "widget")) return CFC_CELL_WIDGET;
return CFC_CELL_CAMERA;
}
static int json_int(struct json_object *obj, const char *key, int def)
{
struct json_object *v;
if (!json_object_object_get_ex(obj, key, &v)) return def;
return json_object_get_int(v);
}
static const char *json_str(struct json_object *obj, const char *key)
{
struct json_object *v;
if (!json_object_object_get_ex(obj, key, &v)) return NULL;
return json_object_get_string(v);
}
static int parse_template(struct json_object *jt, cfc_layout_t *out)
{
memset(out, 0, sizeof(*out));
const char *name = json_str(jt, "name");
if (!name) {
fprintf(stderr, "[cfc/layouts] template без 'name'\n");
return -1;
}
strncpy(out->name, name, sizeof(out->name) - 1);
out->priority = json_int(jt, "priority", 0);
struct json_object *jcells;
if (!json_object_object_get_ex(jt, "cells", &jcells) ||
!json_object_is_type(jcells, json_type_array)) {
fprintf(stderr, "[cfc/layouts] template '%s' без 'cells'\n", name);
return -1;
}
int nb = (int)json_object_array_length(jcells);
if (nb > CFC_LAYOUT_MAX_CELLS) {
fprintf(stderr, "[cfc/layouts] template '%s' has %d cells > max %d, truncated\n",
name, nb, CFC_LAYOUT_MAX_CELLS);
nb = CFC_LAYOUT_MAX_CELLS;
}
for (int i = 0; i < nb; i++) {
struct json_object *jc = json_object_array_get_idx(jcells, i);
if (!jc) continue;
cfc_cell_t *c = &out->cells[out->nb_cells];
c->col = json_int(jc, "col", 0);
c->row = json_int(jc, "row", 0);
c->cs = json_int(jc, "cs", 1);
c->rs = json_int(jc, "rs", 1);
c->role = parse_role(json_str(jc, "role"));
c->order = json_int(jc, "order", 0);
const char *w = json_str(jc, "widget");
if (w) {
strncpy(c->widget, w, sizeof(c->widget) - 1);
c->widget[sizeof(c->widget) - 1] = '\0';
}
/* Валидация bounds. */
if (c->cs < 1 || c->rs < 1 ||
c->col < 0 || c->row < 0 ||
c->col + c->cs > CFC_GRID_COLS ||
c->row + c->rs > CFC_GRID_ROWS) {
fprintf(stderr, "[cfc/layouts] '%s' cell[%d] outside grid: col=%d row=%d cs=%d rs=%d — пропуск\n",
name, i, c->col, c->row, c->cs, c->rs);
continue;
}
out->nb_cells++;
}
recount_camera_cells(out);
return 0;
}
int cfc_layout_load_file(const char *path)
{
if (!path) return -3;
FILE *f = fopen(path, "r");
if (!f) {
fprintf(stderr, "[cfc/layouts] %s: open failed: %s\n", path, strerror(errno));
return -3;
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0 || sz > 1 << 20) { fclose(f); return -1; }
char *buf = malloc(sz + 1);
if (!buf) { fclose(f); return -1; }
if ((long)fread(buf, 1, sz, f) != sz) { free(buf); fclose(f); return -1; }
buf[sz] = '\0';
fclose(f);
struct json_object *root = json_tokener_parse(buf);
free(buf);
if (!root) {
fprintf(stderr, "[cfc/layouts] %s: JSON parse failed\n", path);
return -1;
}
struct json_object *jtpls;
if (!json_object_object_get_ex(root, "templates", &jtpls) ||
!json_object_is_type(jtpls, json_type_array)) {
fprintf(stderr, "[cfc/layouts] %s: 'templates' array missing\n", path);
json_object_put(root);
return -2;
}
int n = (int)json_object_array_length(jtpls);
if (n <= 0) {
fprintf(stderr, "[cfc/layouts] %s: 'templates' пуст\n", path);
json_object_put(root);
return -2;
}
pthread_mutex_lock(&g_mu);
int new_count = 0;
cfc_layout_t tmp[64];
for (int i = 0; i < n && new_count < (int)(sizeof(tmp)/sizeof(tmp[0])); i++) {
struct json_object *jt = json_object_array_get_idx(jtpls, i);
if (parse_template(jt, &tmp[new_count]) == 0) new_count++;
}
if (new_count > 0) {
memcpy(g_layouts, tmp, sizeof(cfc_layout_t) * new_count);
g_layouts_count = new_count;
strncpy(g_loaded_path, path, sizeof(g_loaded_path) - 1);
g_loaded_path[sizeof(g_loaded_path) - 1] = '\0';
fprintf(stderr, "[cfc/layouts] %s: loaded %d templates\n", path, new_count);
} else {
fprintf(stderr, "[cfc/layouts] %s: no valid templates, keeping current\n", path);
}
pthread_mutex_unlock(&g_mu);
json_object_put(root);
return new_count;
}
int cfc_layout_reload(void)
{
if (!g_loaded_path[0]) return -1;
return cfc_layout_load_file(g_loaded_path);
}
const char *cfc_layout_loaded_path(void)
{
return g_loaded_path[0] ? g_loaded_path : NULL;
}
+42
View File
@@ -537,6 +537,11 @@ int cfc_overlay_update_text(cfc_overlay_t *ov,
td->cfg.y = cfg->y; td->cfg.y = cfg->y;
td->cfg.extra_alpha = cfg->extra_alpha ? cfg->extra_alpha : 255; td->cfg.extra_alpha = cfg->extra_alpha ? cfg->extra_alpha : 255;
td->cfg.visible = cfg->visible; td->cfg.visible = cfg->visible;
td->cfg.bg_alpha = cfg->bg_alpha;
td->cfg.bg_y = cfg->bg_y;
td->cfg.bg_u = cfg->bg_u;
td->cfg.bg_v = cfg->bg_v;
td->cfg.bg_pad = cfg->bg_pad;
if (need_rebuild) return text_rebuild_atlas(td); if (need_rebuild) return text_rebuild_atlas(td);
return 0; return 0;
@@ -563,6 +568,28 @@ static int draw_text(cfc_overlay_t *ov, CUstream stream,
int x = t->cfg.x & ~1; int x = t->cfg.x & ~1;
int y = t->cfg.y & ~1; int y = t->cfg.y & ~1;
/* Опциональный фон-подложка (для читаемости текста на любом фоне). */
if (t->cfg.bg_alpha > 0) {
int pad = t->cfg.bg_pad > 0 ? t->cfg.bg_pad : 8;
pad &= ~1;
int bx = x - pad, by = y - pad;
int bw = t->width + 2 * pad, bh = t->height + 2 * pad;
if (bx < 0) { bw += bx; bx = 0; }
if (by < 0) { bh += by; by = 0; }
if (bx + bw > frame_w) bw = frame_w - bx;
if (by + bh > frame_h) bh = frame_h - by;
bx &= ~1; by &= ~1; bw &= ~1; bh &= ~1;
if (bw > 0 && bh > 0) {
int by_v = t->cfg.bg_y ? t->cfg.bg_y : 16;
int bu_v = t->cfg.bg_u ? t->cfg.bg_u : 128;
int bv_v = t->cfg.bg_v ? t->cfg.bg_v : 128;
cfc_cugrid_fill_nv12(stream, dst_y, pitch_y, dst_uv, pitch_uv,
bx, by, bw, bh,
by_v, bu_v, bv_v, t->cfg.bg_alpha);
}
}
return cfc_cugrid_blit_rgba_nv12( return cfc_cugrid_blit_rgba_nv12(
stream, stream,
dst_y, pitch_y, dst_uv, pitch_uv, dst_y, pitch_y, dst_uv, pitch_uv,
@@ -620,6 +647,21 @@ const char *cfc_overlay_detbox_camera_key(cfc_overlay_t *ov)
return ov->u.detbox.camera_key; return ov->u.detbox.camera_key;
} }
int cfc_overlay_detbox_set_cell_geom(cfc_overlay_t *ov,
int cell_x, int cell_y,
int cell_w, int cell_h)
{
if (!ov || ov->type != CFC_OVERLAY_DETECTION_BOXES) return -1;
detbox_data_t *d = &ov->u.detbox;
pthread_mutex_lock(&d->mu);
d->cfg.cell_x = cell_x;
d->cfg.cell_y = cell_y;
d->cfg.cell_w = cell_w;
d->cfg.cell_h = cell_h;
pthread_mutex_unlock(&d->mu);
return 0;
}
int cfc_overlay_detbox_match_zones(cfc_overlay_t *ov, int cfc_overlay_detbox_match_zones(cfc_overlay_t *ov,
const char *const *current_zones, const char *const *current_zones,
int n) int n)