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>
This commit is contained in:
+7
-1
@@ -2,11 +2,17 @@ cmake_minimum_required(VERSION 3.20)
|
||||
project(cuframes-composer
|
||||
VERSION 0.1.0
|
||||
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_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)
|
||||
|
||||
# CUDA архитектуры. Покрываем production-сценарии:
|
||||
|
||||
@@ -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
@@ -2,19 +2,19 @@
|
||||
"version": 1,
|
||||
"grid_cols": 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": [
|
||||
{
|
||||
"name": "tpl_1",
|
||||
"_desc": "1 камера во весь экран.",
|
||||
"_desc": "Одна камера во весь экран.",
|
||||
"cells": [
|
||||
{"col": 0, "row": 0, "cs": 8, "rs": 8, "role": "camera", "order": 0}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tpl_3",
|
||||
"_desc": "Главная 1440×810 слева + 2 превью 480×270 справа стопкой, остаток — виджеты.",
|
||||
"_desc": "Главная 1440×810 + 2 превью 480×270 + widget справа-низ + widget снизу.",
|
||||
"cells": [
|
||||
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
|
||||
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{"col": 0, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 0},
|
||||
{"col": 4, "row": 0, "cs": 4, "rs": 4, "role": "camera", "order": 1},
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
"name": "tpl_5",
|
||||
"_desc": "1 главная + 4 превью справа стопкой, нижняя полоса — виджет.",
|
||||
"_desc": "Главная + 4 превью справа стопкой, снизу — widget.",
|
||||
"cells": [
|
||||
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
|
||||
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
{
|
||||
"name": "tpl_6",
|
||||
"_desc": "1 главная + 3 правые + 2 нижние, остаток — виджет.",
|
||||
"_desc": "Главная + 3 правых + 2 нижних, остаток нижней строки — widget.",
|
||||
"cells": [
|
||||
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
|
||||
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
{
|
||||
"name": "tpl_7",
|
||||
"_desc": "1 главная + 3 правые + 3 нижние, угол — виджет.",
|
||||
"_desc": "Главная + 3 правых + 3 нижних, угол — widget.",
|
||||
"cells": [
|
||||
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
|
||||
{"col": 6, "row": 0, "cs": 2, "rs": 2, "role": "camera", "order": 1},
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
{
|
||||
"name": "tpl_8",
|
||||
"_desc": "1+3+4 — главная + 3 правые + полная нижняя строка.",
|
||||
"_desc": "1+3+4 — главная + 3 правых + 4 в нижней строке (без widget).",
|
||||
"cells": [
|
||||
{"col": 0, "row": 0, "cs": 6, "rs": 6, "role": "camera", "order": 0},
|
||||
{"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": 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}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,3 +15,9 @@ target_include_directories(simple_record PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
add_executable(grid_record grid_record.c)
|
||||
target_link_libraries(grid_record PRIVATE cuframes_composer_static)
|
||||
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
@@ -127,6 +127,7 @@ int main(int argc, char **argv)
|
||||
const char *frigate_mqtt_host = NULL;
|
||||
int frigate_mqtt_port = 1883;
|
||||
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 */
|
||||
int motion_mode = 0; /* --motion-mode */
|
||||
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-ttl", required_argument, 0, 'k'}, /* TTL ms (default 45000) */
|
||||
{"templates", required_argument, 0, 'z'}, /* path to templates.json */
|
||||
{"mqtt-overlays", required_argument, 0, 'x'}, /* path to MQTT overlays JSON */
|
||||
{0, 0, 0, 0},
|
||||
};
|
||||
const char *templates_path = NULL;
|
||||
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) {
|
||||
case 'o': out_path = optarg; break;
|
||||
case 'c':
|
||||
@@ -241,6 +243,7 @@ int main(int argc, char **argv)
|
||||
case 'm': motion_mode = 1; break;
|
||||
case 'k': motion_ttl = atoi(optarg); break;
|
||||
case 'z': templates_path = optarg; break;
|
||||
case 'x': mqtt_overlays_path = optarg; break;
|
||||
case 'S': {
|
||||
if (num_sources >= 32) {
|
||||
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);
|
||||
}
|
||||
|
||||
/* Глобальные 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 координат. Удобно
|
||||
* как default для ONVIF PTZ-управляемого composer'а (старт в quad,
|
||||
* далее set_layout через ZMQ). В motion-mode не работает (relayout
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -105,6 +105,14 @@ typedef struct cfc_overlay_text_config {
|
||||
int r, g, b; /* sRGB цвет 0..255 */
|
||||
int extra_alpha; /* 0..255 общий множитель прозрачности */
|
||||
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;
|
||||
|
||||
/* Создать TEXT overlay. Открывает font через FreeType, рендерит строку
|
||||
@@ -168,6 +176,13 @@ int cfc_overlay_create_detection_boxes(
|
||||
* правильный overlay по incoming event'у. */
|
||||
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'я.
|
||||
* - если required_zones пуст → всегда 1 (filter off)
|
||||
* - если current_zones пуст → 0 (объект вне зон)
|
||||
|
||||
+28
-5
@@ -14,21 +14,40 @@ set(COMPOSER_SOURCES_C
|
||||
source.c
|
||||
nvenc_loader.c
|
||||
nvenc.c
|
||||
composer.c
|
||||
overlay.c
|
||||
control.c
|
||||
health.c
|
||||
writer.c
|
||||
audio.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
|
||||
cugrid/cugrid.cu
|
||||
)
|
||||
|
||||
add_library(cuframes_composer SHARED ${COMPOSER_SOURCES_C} ${COMPOSER_SOURCES_CU})
|
||||
add_library(cuframes_composer_static STATIC ${COMPOSER_SOURCES_C} ${COMPOSER_SOURCES_CU})
|
||||
add_library(cuframes_composer SHARED
|
||||
${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)
|
||||
target_include_directories(${target}
|
||||
@@ -39,16 +58,20 @@ foreach(target cuframes_composer cuframes_composer_static)
|
||||
${CMAKE_CURRENT_SOURCE_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).
|
||||
target_compile_options(${target} PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:C>:-Wall>
|
||||
$<$<COMPILE_LANGUAGE:C>:-Wextra>
|
||||
$<$<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>>:-g3>
|
||||
$<$<AND:$<COMPILE_LANGUAGE:C>,$<CONFIG:Release>>:-O2>
|
||||
$<$<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}
|
||||
PUBLIC
|
||||
|
||||
-764
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -537,6 +537,11 @@ int cfc_overlay_update_text(cfc_overlay_t *ov,
|
||||
td->cfg.y = cfg->y;
|
||||
td->cfg.extra_alpha = cfg->extra_alpha ? cfg->extra_alpha : 255;
|
||||
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);
|
||||
return 0;
|
||||
@@ -563,6 +568,28 @@ static int draw_text(cfc_overlay_t *ov, CUstream stream,
|
||||
|
||||
int x = t->cfg.x & ~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(
|
||||
stream,
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
const char *const *current_zones,
|
||||
int n)
|
||||
|
||||
Reference in New Issue
Block a user