Phase 10/11 WIP — pool + motion-mode + 8×8 templates (rolled back)

Объединённое состояние работ:
  - Phase 10: source pool, motion-driven layout, Frigate motion_pulse,
    zone-filter в pool, --source / --motion-mode CLI
  - Phase 11 Steps 1-3c: 8×8 микро-сетка, JSON templates с asymmetric
    layouts (tpl_1/3/4/5/6/7/8), reload через ZMQ, auto-labels per camera

В прод отдеплоено и откачено: используем gx/cuframes-composer:0.10 как
baseline. Phase 11 продолжится в branch phase11b-cpp как C++ refactor
с ООП-моделью Cell/Layout/Decoration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:24:23 +01:00
parent 2d7fc1e640
commit f8e27b9e85
10 changed files with 1097 additions and 152 deletions
+3
View File
@@ -72,6 +72,9 @@ COPY --from=builder /src/build/third_party/cuframes/libcuframes/libcuframes.so.0
COPY --from=builder /src/build/src/libcuframes_composer.so.0.1.0 \
/usr/lib/x86_64-linux-gnu/libcuframes_composer.so.0
# Phase 11 — layout templates.json. Editable in-container или mount override.
COPY docker/templates.json /etc/cfc-grid/templates.json
# NVIDIA_DRIVER_CAPABILITIES должен включать 'video' для NVENC через libnvidia-encode.
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
+90
View File
@@ -0,0 +1,90 @@
{
"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.",
"templates": [
{
"name": "tpl_1",
"_desc": "1 камера во весь экран.",
"cells": [
{"col": 0, "row": 0, "cs": 8, "rs": 8, "role": "camera", "order": 0}
]
},
{
"name": "tpl_3",
"_desc": "Главная 1440×810 слева + 2 превью 480×270 справа стопкой, остаток — виджеты.",
"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},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 4, "role": "widget", "widget": "temp_chart"},
{"col": 0, "row": 6, "cs": 6, "rs": 2, "role": "widget", "widget": "ha_chat"}
]
},
{
"name": "tpl_4",
"_desc": "Quad 2×2: 4 камеры 960×540. 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},
{"col": 0, "row": 4, "cs": 4, "rs": 4, "role": "camera", "order": 2},
{"col": 4, "row": 4, "cs": 4, "rs": 4, "role": "camera", "order": 3}
]
},
{
"name": "tpl_5",
"_desc": "1 главная + 4 превью справа стопкой, нижняя полоса — виджет.",
"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},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 0, "row": 6, "cs": 6, "rs": 2, "role": "widget", "widget": "ha_chat"}
]
},
{
"name": "tpl_6",
"_desc": "1 главная + 3 правые + 2 нижние, остаток — виджет.",
"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},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 6, "cs": 4, "rs": 2, "role": "widget", "widget": "info"}
]
},
{
"name": "tpl_7",
"_desc": "1 главная + 3 правые + 3 нижние, угол — виджет.",
"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},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "widget", "widget": "ha_chat"}
]
},
{
"name": "tpl_8",
"_desc": "1+3+4 — главная + 3 правые + полная нижняя строка.",
"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},
{"col": 6, "row": 2, "cs": 2, "rs": 2, "role": "camera", "order": 2},
{"col": 6, "row": 4, "cs": 2, "rs": 2, "role": "camera", "order": 3},
{"col": 0, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 4},
{"col": 2, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 5},
{"col": 4, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 6},
{"col": 6, "row": 6, "cs": 2, "rs": 2, "role": "camera", "order": 7}
]
}
]
}
+78 -7
View File
@@ -128,6 +128,12 @@ int main(int argc, char **argv)
int frigate_mqtt_port = 1883;
const char *frigate_topic = "frigate/events";
const char *initial_layout = NULL; /* --layout NAME → set_layout после init */
int motion_mode = 0; /* --motion-mode */
int motion_ttl = 45000; /* --motion-ttl ms */
/* --source cuframes_key,frigate=camera_name,priority=N[,zones=z1:z2:...] */
typedef struct { char key[64], frigate[48], zones[128]; int priority; } source_spec_t;
source_spec_t sources[32] = { 0 };
int num_sources = 0;
/* --detection-cell key,camera,dx,dy,dw,dh,detect_w,detect_h[,zone1:zone2:...]
* key — символьное имя для логов (например "parking")
* camera — Frigate camera_key для MQTT match'а ("parking_overview")
@@ -169,10 +175,15 @@ int main(int argc, char **argv)
{"frigate-topic", required_argument, 0, 'T'},
{"detection-cell", required_argument, 0, 'D'},
{"layout", required_argument, 0, 'L'}, /* named layout (quad, single, ...) */
{"source", required_argument, 0, 'S'}, /* pool source: key,frigate=...,priority=N */
{"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 */
{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:", 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:", opts, NULL)) != -1) {
switch (c) {
case 'o': out_path = optarg; break;
case 'c':
@@ -227,6 +238,30 @@ int main(int argc, char **argv)
}
case 'T': frigate_topic = optarg; break;
case 'L': initial_layout = optarg; break;
case 'm': motion_mode = 1; break;
case 'k': motion_ttl = atoi(optarg); break;
case 'z': templates_path = optarg; break;
case 'S': {
if (num_sources >= 32) {
fprintf(stderr, "max 32 sources\n"); return 1;
}
/* Формат: key[,frigate=name][,priority=N]. */
char buf[256]; strncpy(buf, optarg, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ",");
if (!tok) { fprintf(stderr, "bad --source\n"); return 1; }
strncpy(sources[num_sources].key, tok, sizeof(sources[num_sources].key) - 1);
while ((tok = strtok(NULL, ",")) != NULL) {
if (!strncmp(tok, "frigate=", 8)) {
strncpy(sources[num_sources].frigate, tok + 8,
sizeof(sources[num_sources].frigate) - 1);
} else if (!strncmp(tok, "priority=", 9)) {
sources[num_sources].priority = atoi(tok + 9);
}
}
num_sources++;
break;
}
case 'D': {
if (num_detcells >= MAX_CELLS) { fprintf(stderr, "max %d detcells\n", MAX_CELLS); return 1; }
char buf[512]; strncpy(buf, optarg, sizeof(buf) - 1); buf[sizeof(buf)-1] = '\0';
@@ -340,18 +375,39 @@ int main(int argc, char **argv)
default: return 1;
}
}
if (!out_path || num_cells == 0) {
if (!out_path || (num_cells == 0 && num_sources == 0)) {
fprintf(stderr,
"Использование: %s --out <file.h264> --cell key,x,y,w,h [--cell ...]\n"
" ИЛИ: %s --out <file.h264> --motion-mode --source ... [--source ...]\n"
" [--width 3840] [--height 2160] [--fps 25]\n"
" [--bitrate 10000] [--seconds N]\n",
argv[0]);
" [--bitrate 10000] [--seconds N]\n"
" --source cuframes_key[,frigate=name][,priority=N]\n",
argv[0], argv[0]);
return 1;
}
/* Motion-mode + только --source: создаём placeholder cell (motion_relayout
* перепишет его перед первым кадром). */
if (num_cells == 0 && num_sources > 0) {
strncpy(cell_keys[0], sources[0].key, 63);
cells[0].source_key = cell_keys[0];
cells[0].x = 0; cells[0].y = 0;
cells[0].w = out_w; cells[0].h = out_h;
num_cells = 1;
}
signal(SIGINT, on_sig);
signal(SIGTERM, on_sig);
/* Загружаем templates.json — если файла нет, остаются built-in. */
if (templates_path) {
int n = cfc_layout_load_file(templates_path);
if (n <= 0) {
fprintf(stderr, "[grid_record] templates %s: failed (rc=%d), using built-in\n",
templates_path, n);
}
}
/* CUDA primary context. */
CUresult cr = cuInit(0);
if (cr != CUDA_SUCCESS) { fprintf(stderr, "cuInit: %s\n", cu_err(cr)); return 1; }
@@ -378,9 +434,22 @@ int main(int argc, char **argv)
fprintf(stderr, "[grid_record] composer %dx%d, %d ячеек\n",
out_w, out_h, num_cells);
/* --source: добавить в pool до motion-mode init. Источники cuframes
* стартуют здесь же, до первого compose'а. */
for (int i = 0; i < num_sources; i++) {
cfc_composer_add_pool_source(comp, sources[i].key,
sources[i].frigate[0] ? sources[i].frigate : NULL,
sources[i].priority,
sources[i].zones[0] ? sources[i].zones : NULL);
}
if (motion_mode) {
cfc_composer_set_motion_mode(comp, 1, motion_ttl);
}
/* --layout NAME → applies named layout поверх --cell координат. Удобно
* как default для ONVIF PTZ-управляемого composer'а (старт в quad,
* далее set_layout через ZMQ). */
* далее set_layout через ZMQ). В motion-mode не работает (relayout
* перетирает на каждом кадре). */
if (initial_layout) {
if (cfc_composer_set_layout(comp, initial_layout) != 0) {
fprintf(stderr, "[grid_record] --layout '%s' unknown\n", initial_layout);
@@ -477,13 +546,15 @@ int main(int argc, char **argv)
fprintf(stderr, "\n");
}
/* Frigate MQTT subscriber (если задан --frigate-mqtt). */
/* Frigate MQTT subscriber: запускаем если есть detection-cells
* (overlay'ные bbox'ы) ИЛИ motion-mode (auto-layout drivers). */
cfc_frigate_mqtt_t *frigate = NULL;
if (frigate_mqtt_host && num_detcells > 0) {
if (frigate_mqtt_host && (num_detcells > 0 || motion_mode)) {
cfc_frigate_mqtt_config_t fc = {
.host = frigate_mqtt_host, .port = frigate_mqtt_port,
.username = mqtt_user, .password = mqtt_pass,
.topic = frigate_topic,
.composer = motion_mode ? comp : NULL, /* pulse'ы только в motion-mode */
};
if (cfc_frigate_mqtt_create(&fc, &frigate) == 0) {
for (int i = 0; i < num_detcells; i++) {
+49
View File
@@ -117,6 +117,55 @@ int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name);
* Возвращает NULL если cells выставлены вручную через --cell. */
const char *cfc_composer_current_layout(cfc_composer_t *comp);
/* ── Motion-driven auto layout (Phase 10) ────────────────────────────────
* Composer держит pool из N (до 32) sources с priority. При включённом
* motion-mode на каждом compose'е выбирает active (last_motion_ms < TTL),
* сортирует по priority DESC, и применяет layout по count активных:
* 0 → top-1 by priority в single
* 1 → single
* 2 → dual_horizontal
* 3-4 → quad
* 5 → main_with_strip
* 6 → six_grid
* 7-9 → nine_grid
* 10-16 → sixteen_grid
* Сигналы motion поступают через cfc_composer_motion_pulse() — обычно из
* Frigate MQTT subscriber'а (frigate_mqtt.c). */
/* Добавить источник в pool (отдельно от --cell). Источник создаётся сразу,
* cuframes subscription стартует. Source доступен через motion-relayout.
*
* required_zones (опц.) — colon-separated whitelist для motion-фильтра:
* только события Frigate с current_zones intersect этот список считаются
* motion. Без него все события от frigate_camera считаются motion
* (полезно если камера без zones либо motion и так осмысленный). */
int cfc_composer_add_pool_source(
cfc_composer_t *comp,
const char *cuframes_key, /* "cam-parking" */
const char *frigate_camera, /* "parking_overview" для motion match */
int priority, /* выше = важнее, top попадает в первую ячейку */
const char *required_zones /* "parking_zone:canopy:private_area" или NULL */
);
/* Включить/выключить motion-mode. ttl_ms — сколько камера остаётся
* активной после последнего события (0 → default 45000). */
int cfc_composer_set_motion_mode(cfc_composer_t *comp, int on, int ttl_ms);
/* Узнать, включён ли motion-mode. */
int cfc_composer_get_motion_mode(cfc_composer_t *comp);
/* Зафиксировать факт motion от Frigate-камеры. Используется Frigate MQTT
* subscriber'ом. Если frigate_camera не найдена в pool — no-op.
*
* current_zones — список зон из after.current_zones события. Если у source
* задан required_zones — будет применён фильтр: motion засчитывается только
* если current_zones intersect required_zones. Если required_zones пуст или
* NULL — фильтр выключен. */
int cfc_composer_motion_pulse(cfc_composer_t *comp,
const char *frigate_camera,
const char *const *current_zones,
int n_zones);
/* Получить layout статистику по источникам — для debug / health-репортов. */
typedef struct cfc_composer_health {
int total; /* всего источников */
+5
View File
@@ -16,6 +16,7 @@
#ifndef CUFRAMES_COMPOSER_FRIGATE_MQTT_H
#define CUFRAMES_COMPOSER_FRIGATE_MQTT_H
#include "composer.h"
#include "overlay.h"
#ifdef __cplusplus
@@ -28,6 +29,10 @@ typedef struct cfc_frigate_mqtt_config {
const char *username; /* MQTT user — обычно тот же, что у health */
const char *password;
const char *topic; /* default "frigate/events" */
/* Если задан — каждое new/update event для любой камеры вызывает
* cfc_composer_motion_pulse(composer, after.camera). Это драйвер для
* motion-driven auto-layout (Phase 10). NULL = motion-pulse не шлём. */
cfc_composer_t *composer;
} cfc_frigate_mqtt_config_t;
typedef struct cfc_frigate_mqtt cfc_frigate_mqtt_t;
+79 -15
View File
@@ -1,12 +1,18 @@
/* cuframes-composer — predefined layout templates.
/* cuframes-composer — модель layout'ов на 8×8 микро-сетке (Phase 11).
*
* Phase 9 (task #194): runtime layout switching через ZMQ verb + ONVIF PTZ.
* Output (1920×1080) разбит на 8 cols × 8 rows микроячеек 240×135 px.
* Каждая микроячейка имеет aspect 16:9 — любой квадрат N×N микроячеек
* тоже 16:9 (без растяжения камер).
*
* Layouts описаны normalized (0..1) — масштабируются к фактическому output
* width/height композитора при apply_to_cells(). Список синхронизирован с
* historic'ой `vf_cuda_grid.c` layouts[] (старый FFmpeg patch), что
* обеспечивает совместимость с Python controller'ом и предыдущими ONVIF
* presets.
* Cell template — прямоугольник {col, row, cs, rs} в микроячейках,
* с ролью CAMERA либо WIDGET. Camera-cells получают source из pool
* композитора по polю `order`: order=0 → main (top-priority active),
* order=1 → next и т.д.
*
* Layout template — набор cells одного экрана. Composer хранит таблицу
* built-in templates + опционально загружает override из JSON (Step 7).
*
* Phase 11 Step 1: единственный built-in `tpl_1` = одна cell 8×8 (single).
*
* Лицензия: LGPL-2.1+
*/
@@ -18,25 +24,83 @@
extern "C" {
#endif
#define CFC_LAYOUT_MAX_CELLS 16
#define CFC_GRID_COLS 8
#define CFC_GRID_ROWS 8
#define CFC_LAYOUT_MAX_CELLS 32
#define CFC_LAYOUT_MAX_NAME 32
#define CFC_WIDGET_NAME_MAX 32
typedef struct cfc_layout_cell {
float x, y, w, h; /* normalized fraction of output dims */
} cfc_layout_cell_t;
typedef enum cfc_cell_role {
CFC_CELL_CAMERA = 0,
CFC_CELL_WIDGET = 1,
} cfc_cell_role_t;
/* Описание одной cell внутри layout-template'а.
* col, row — top-left угол в микроячейках (0..7)
* cs, rs — span в микроячейках (≥1, col+cs ≤ 8, row+rs ≤ 8)
* role — CAMERA или WIDGET
* order — для CAMERA: порядок placement'а активных камер
* (order=0 получает top-priority); игнорируется для WIDGET
* widget — для WIDGET: имя placeholder'а (текст подписи) */
typedef struct cfc_cell {
int col, row;
int cs, rs;
cfc_cell_role_t role;
int order;
char widget[CFC_WIDGET_NAME_MAX];
} cfc_cell_t;
/* Описание layout-template'а: набор cells + metadata. */
typedef struct cfc_layout {
const char *name;
int nb_cells;
cfc_layout_cell_t cells[CFC_LAYOUT_MAX_CELLS];
char name[CFC_LAYOUT_MAX_NAME];
cfc_cell_t cells[CFC_LAYOUT_MAX_CELLS];
int nb_cells; /* всего cells */
int nb_camera_cells; /* кеш — для best-fit selection */
int priority; /* tie-break при best-fit (выше = победитель) */
} cfc_layout_t;
/* Найти layout по имени; NULL если нет. */
const cfc_layout_t *cfc_layout_find(const char *name);
/* Возвращает указатель на array всех layout'ов + их количество. */
/* Возвращает указатель на массив всех загруженных layout'ов и их количество. */
const cfc_layout_t *cfc_layout_all(int *out_count);
/* Перевести {col,row,cs,rs} в pixel-координаты под output W×H.
* x = col * (W/8), w = cs * (W/8) (выровнено на чётные).
* NV12 требует чётных координат — встроено внутрь. */
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);
/* Загрузить templates из JSON-файла. При успехе сменяет реестр; при ошибке
* реестр остаётся прежним (или built-in, если ничего не было).
* Возвращает количество загруженных templates, либо отрицательное число
* (-1 = parse error, -2 = invalid schema, -3 = file not found).
*
* Hot-reload: после успешного вызова cfc_layout_find/cfc_layout_all
* возвращают новые template'ы. Композитор подхватывает на следующем кадре.
*
* JSON schema:
* { "version": 1, "grid_cols": 8, "grid_rows": 8,
* "templates": [
* { "name": "tpl_1", "priority": 0,
* "cells": [
* { "col":0, "row":0, "cs":8, "rs":8,
* "role":"camera", "order":0,
* "widget":"ha_chat" // для role=widget
* }, ...
* ]
* }, ...
* ]
* } */
int cfc_layout_load_file(const char *path);
/* Перезагрузить из последнего успешно загруженного файла. Если файла не
* было — возвращает -1. */
int cfc_layout_reload(void);
/* Получить путь к текущему загруженному файлу (NULL если built-in). */
const char *cfc_layout_loaded_path(void);
#ifdef __cplusplus
}
#endif
+422 -32
View File
@@ -29,6 +29,9 @@
#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>
@@ -37,6 +40,25 @@
#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;
@@ -46,8 +68,8 @@ struct cfc_composer {
char cell_keys[CFC_COMPOSER_MAX_CELLS][64];
int num_cells;
/* Источники по индексу cell'а (или NULL если cell — статичная картинка). */
cfc_source_t *sources[CFC_COMPOSER_MAX_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 байт. */
@@ -66,8 +88,38 @@ struct cfc_composer {
/* Текущий 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)
@@ -114,10 +166,14 @@ static int compose_clear(cfc_composer_t *comp)
static int compose_cell(cfc_composer_t *comp, int idx)
{
const cfc_composer_cell_t *cell = &comp->cells[idx];
cfc_source_t *src = comp->sources[idx];
if (!src) return 0;
/* 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);
@@ -157,6 +213,8 @@ int cfc_composer_create(const cfc_composer_config_t *cfg, cfc_composer_t **out)
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;
@@ -188,17 +246,28 @@ int cfc_composer_create(const cfc_composer_config_t *cfg, cfc_composer_t **out)
return -1;
}
/* Создаём источники по одному на cell с source_key. */
/* Pool: для каждой уникальной cell.source_key создаём подписку.
* Если key уже в pool (тот же source в нескольких cells) — реюзим. */
for (int i = 0; i < comp->num_cells; i++) {
if (!comp->cells[i].source_key) continue;
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, i);
snprintf(name, sizeof(name), "%s-%d", prefix, comp->pool_count);
cfc_source_config_t scfg = {
.key = comp->cells[i].source_key,
.key = key,
.consumer_name = name,
.cuda_device = cfg->cuda_device,
.reconnect_min_ms = cfg->reconnect_min_ms,
@@ -206,13 +275,13 @@ int cfc_composer_create(const cfc_composer_config_t *cfg, cfc_composer_t **out)
.stale_threshold_ms = cfg->stale_threshold_ms,
.dead_threshold_ms = cfg->dead_threshold_ms,
};
if (cfc_source_create(&scfg, &comp->sources[i]) != 0) {
if (cfc_source_create(&scfg, &e->source) != 0) {
fprintf(stderr,
"[cfc/composer] cfc_source_create failed для cell %d (%s)\n",
i, comp->cells[i].source_key);
/* Не fatal — DEAD source просто будет показывать blackout.
* Ячейка остаётся NULL → compose_cell сразу выйдет. */
"[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();
@@ -229,6 +298,9 @@ int cfc_composer_compose(cfc_composer_t *comp,
{
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++) {
@@ -291,6 +363,13 @@ cfc_overlay_t *cfc_composer_find_overlay(cfc_composer_t *comp, const char *id)
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);
@@ -301,19 +380,11 @@ int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name)
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;
/* Применяем нормализованные координаты к каждой существующей ячейке.
* source_key и индексы сохраняются. Если у layout'а меньше cells чем у
* composer'а — лишние оставляем с нулевой геометрией (compose_cell
* увидит w<=0 и пропустит). */
/* Микро-сетка → pixel coords для каждой cell. Source_key привязок не
* меняем (Step 2+ добавит role/order распределение). */
for (int i = 0; i < n_apply; i++) {
int x = (int)(lay->cells[i].x * W + 0.5f);
int y = (int)(lay->cells[i].y * H + 0.5f);
int w = (int)(lay->cells[i].w * W + 0.5f);
int h = (int)(lay->cells[i].h * H + 0.5f);
/* Чётные координаты — требование 4:2:0 NV12. */
x &= ~1; y &= ~1; w &= ~1; h &= ~1;
if (x + w > W) w = W - x;
if (y + h > H) h = H - y;
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;
@@ -338,18 +409,336 @@ const char *cfc_composer_current_layout(cfc_composer_t *comp)
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->num_cells;
for (int i = 0; i < comp->num_cells; i++) {
if (!comp->sources[i]) {
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->sources[i], &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;
@@ -362,12 +751,13 @@ int cfc_composer_get_health(cfc_composer_t *comp, cfc_composer_health_t *out)
int cfc_composer_destroy(cfc_composer_t *comp)
{
if (!comp) return 0;
for (int i = 0; i < comp->num_cells; i++) {
if (comp->sources[i]) cfc_source_destroy(comp->sources[i]);
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;
+94 -4
View File
@@ -177,20 +177,82 @@ static void cmd_set_layout(void *sock, cfc_composer_t *comp, struct json_object
send_json(sock, o);
}
static struct json_object *template_to_json(const cfc_layout_t *l)
{
struct json_object *t = json_object_new_object();
json_object_object_add(t, "name", json_object_new_string(l->name));
json_object_object_add(t, "priority", json_object_new_int(l->priority));
json_object_object_add(t, "nb_cells", json_object_new_int(l->nb_cells));
json_object_object_add(t, "nb_camera_cells", json_object_new_int(l->nb_camera_cells));
struct json_object *cells = json_object_new_array();
for (int i = 0; i < l->nb_cells; i++) {
const cfc_cell_t *c = &l->cells[i];
struct json_object *jc = json_object_new_object();
json_object_object_add(jc, "col", json_object_new_int(c->col));
json_object_object_add(jc, "row", json_object_new_int(c->row));
json_object_object_add(jc, "cs", json_object_new_int(c->cs));
json_object_object_add(jc, "rs", json_object_new_int(c->rs));
json_object_object_add(jc, "role",
json_object_new_string(c->role == CFC_CELL_WIDGET ? "widget" : "camera"));
json_object_object_add(jc, "order", json_object_new_int(c->order));
if (c->widget[0]) {
json_object_object_add(jc, "widget", json_object_new_string(c->widget));
}
json_object_array_add(cells, jc);
}
json_object_object_add(t, "cells", cells);
return t;
}
static void cmd_list_layouts(void *sock)
{
int n = 0;
const cfc_layout_t *all = cfc_layout_all(&n);
struct json_object *arr = json_object_new_array();
for (int i = 0; i < n; i++) {
struct json_object *l = json_object_new_object();
json_object_object_add(l, "name", json_object_new_string(all[i].name));
json_object_object_add(l, "nb_cells", json_object_new_int(all[i].nb_cells));
json_object_array_add(arr, l);
json_object_array_add(arr, template_to_json(&all[i]));
}
struct json_object *o = json_object_new_object();
json_object_object_add(o, "ok", json_object_new_boolean(1));
json_object_object_add(o, "layouts", arr);
const char *p = cfc_layout_loaded_path();
json_object_object_add(o, "source",
json_object_new_string(p ? p : "built-in"));
send_json(sock, o);
}
static void cmd_get_template(void *sock, struct json_object *req)
{
const char *name = get_str(req, "name");
if (!name) { send_error(sock, "missing 'name'"); return; }
const cfc_layout_t *l = cfc_layout_find(name);
if (!l) { send_error(sock, "template not found"); return; }
struct json_object *o = json_object_new_object();
json_object_object_add(o, "ok", json_object_new_boolean(1));
json_object_object_add(o, "template", template_to_json(l));
send_json(sock, o);
}
static void cmd_reload_templates(void *sock, struct json_object *req)
{
const char *path = get_str(req, "path");
int r;
if (path) r = cfc_layout_load_file(path);
else r = cfc_layout_reload();
if (r < 0) {
const char *msg = "reload failed";
if (r == -1) msg = "JSON parse error";
else if (r == -2) msg = "schema invalid";
else if (r == -3) msg = "file not found / no previous load";
send_error(sock, msg);
return;
}
struct json_object *o = json_object_new_object();
json_object_object_add(o, "ok", json_object_new_boolean(1));
json_object_object_add(o, "templates", json_object_new_int(r));
const char *p = cfc_layout_loaded_path();
json_object_object_add(o, "source",
json_object_new_string(p ? p : "built-in"));
send_json(sock, o);
}
@@ -204,6 +266,30 @@ static void cmd_get_layout(void *sock, cfc_composer_t *comp)
send_json(sock, o);
}
static void cmd_set_motion_mode(void *sock, cfc_composer_t *comp,
struct json_object *req)
{
int on = get_int(req, "on", 1);
int ttl = get_int(req, "ttl_ms", 0);
if (cfc_composer_set_motion_mode(comp, on, ttl) != 0) {
send_error(sock, "set_motion_mode failed");
return;
}
struct json_object *o = json_object_new_object();
json_object_object_add(o, "ok", json_object_new_boolean(1));
json_object_object_add(o, "motion_mode", json_object_new_int(on ? 1 : 0));
send_json(sock, o);
}
static void cmd_get_motion_mode(void *sock, cfc_composer_t *comp)
{
struct json_object *o = json_object_new_object();
json_object_object_add(o, "ok", json_object_new_boolean(1));
json_object_object_add(o, "motion_mode",
json_object_new_int(cfc_composer_get_motion_mode(comp)));
send_json(sock, o);
}
static void cmd_list_overlays(void *sock, cfc_composer_t *comp)
{
struct json_object *arr = json_object_new_array();
@@ -242,6 +328,10 @@ static void dispatch(void *sock, cfc_composer_t *comp, const char *json_str, siz
else if (!strcmp(cmd, "set_layout")) cmd_set_layout(sock, comp, req);
else if (!strcmp(cmd, "list_layouts")) cmd_list_layouts(sock);
else if (!strcmp(cmd, "get_layout")) cmd_get_layout(sock, comp);
else if (!strcmp(cmd, "set_motion_mode")) cmd_set_motion_mode(sock, comp, req);
else if (!strcmp(cmd, "get_motion_mode")) cmd_get_motion_mode(sock, comp);
else if (!strcmp(cmd, "get_template")) cmd_get_template(sock, req);
else if (!strcmp(cmd, "reload_templates")) cmd_reload_templates(sock, req);
else send_error(sock, "unknown cmd");
out:
+24 -16
View File
@@ -104,8 +104,29 @@ static void parse_event(cfc_frigate_mqtt_t *f, const char *payload)
const char *camera = json_object_get_string(jcam);
const char *event_id = json_object_get_string(jid);
/* Распарсить current_zones — нужны и для motion_pulse, и для bbox-фильтра. */
const char *zone_buf[16];
int n_zones = 0;
if (jzones && json_object_is_type(jzones, json_type_array)) {
int len = (int)json_object_array_length(jzones);
if (len > 16) len = 16;
for (int i = 0; i < len; i++) {
struct json_object *jz = json_object_array_get_idx(jzones, i);
if (jz && json_object_is_type(jz, json_type_string)) {
zone_buf[n_zones++] = json_object_get_string(jz);
}
}
}
/* Motion pulse — для ВСЕХ камер из pool'а композитора, независимо
* от того есть ли detbox overlay. Это драйвер auto-layout (Phase 10).
* Per-source zone-filter применится внутри composer.motion_pulse. */
if (f->cfg.composer && strcmp(type, "end") != 0) {
cfc_composer_motion_pulse(f->cfg.composer, camera, zone_buf, n_zones);
}
cfc_overlay_t *ov = lookup_overlay(f, camera);
if (!ov) { json_object_put(root); return; } /* не наша камера */
if (!ov) { json_object_put(root); return; } /* не наша камера для overlay */
atomic_fetch_add(&f->events_received, 1);
@@ -133,21 +154,8 @@ static void parse_event(cfc_frigate_mqtt_t *f, const char *payload)
const char *label = jlabel ? json_object_get_string(jlabel) : "";
double frame_time = jft ? json_object_get_double(jft) : 0.0;
/* Zone-filter — отсев street-флуда. Frigate 0.17 schema не даёт native
* objects.filters.required_zones (Pydantic extra-inputs-not-permitted →
* safe mode), поэтому фильтруем на стороне subscriber'а. */
const char *zone_buf[16];
int n_zones = 0;
if (jzones && json_object_is_type(jzones, json_type_array)) {
int len = (int)json_object_array_length(jzones);
if (len > 16) len = 16;
for (int i = 0; i < len; i++) {
struct json_object *jz = json_object_array_get_idx(jzones, i);
if (jz && json_object_is_type(jz, json_type_string)) {
zone_buf[n_zones++] = json_object_get_string(jz);
}
}
}
/* Zone-filter overlay'я (current_zones уже распарсены выше). Отсев
* street-флуда — Frigate 0.17 не даёт native objects.filters.required_zones. */
if (!cfc_overlay_detbox_match_zones(ov, zone_buf, n_zones)) {
json_object_put(root);
return;
+252 -77
View File
@@ -1,100 +1,275 @@
/* Predefined layouts table — port из vf_cuda_grid.c (старый FFmpeg patch).
/* Layout-templates на 8×8 микро-сетке (Phase 11).
*
* Layouts normalized [0..1], apply'ятся к фактическому output разрешению
* композитора в момент set_layout — не привязаны к конкретным cell coords.
* 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 const cfc_layout_t g_layouts[] = {
{
"single", 1,
{ {0.0f, 0.0f, 1.0f, 1.0f} }
},
{
"dual_horizontal", 2,
{ {0.0f, 0.0f, 0.5f, 1.0f}, {0.5f, 0.0f, 0.5f, 1.0f} }
},
{
"dual_vertical", 2,
{ {0.0f, 0.0f, 1.0f, 0.5f}, {0.0f, 0.5f, 1.0f, 0.5f} }
},
{
"quad", 4,
{
{0.0f, 0.0f, 0.5f, 0.5f}, {0.5f, 0.0f, 0.5f, 0.5f},
{0.0f, 0.5f, 0.5f, 0.5f}, {0.5f, 0.5f, 0.5f, 0.5f},
}
},
{
/* Main 2/3 width слева + 3 preview справа сверху вниз. */
"main_plus_preview", 4,
{
{0.0f, 0.0f, 2.0f/3, 1.0f},
{2.0f/3, 0.0f, 1.0f/3, 1.0f/3},
{2.0f/3, 1.0f/3, 1.0f/3, 1.0f/3},
{2.0f/3, 2.0f/3, 1.0f/3, 1.0f/3},
}
},
{
"six_grid", 6,
{
{0.0f, 0.0f, 1.0f/3, 0.5f}, {1.0f/3, 0.0f, 1.0f/3, 0.5f}, {2.0f/3, 0.0f, 1.0f/3, 0.5f},
{0.0f, 0.5f, 1.0f/3, 0.5f}, {1.0f/3, 0.5f, 1.0f/3, 0.5f}, {2.0f/3, 0.5f, 1.0f/3, 0.5f},
}
},
{
"nine_grid", 9,
{
{0.0f, 0.0f, 1.0f/3, 1.0f/3}, {1.0f/3, 0.0f, 1.0f/3, 1.0f/3}, {2.0f/3, 0.0f, 1.0f/3, 1.0f/3},
{0.0f, 1.0f/3, 1.0f/3, 1.0f/3}, {1.0f/3, 1.0f/3, 1.0f/3, 1.0f/3}, {2.0f/3, 1.0f/3, 1.0f/3, 1.0f/3},
{0.0f, 2.0f/3, 1.0f/3, 1.0f/3}, {1.0f/3, 2.0f/3, 1.0f/3, 1.0f/3}, {2.0f/3, 2.0f/3, 1.0f/3, 1.0f/3},
}
},
{
"sixteen_grid", 16,
{
{0.00f, 0.00f, 0.25f, 0.25f}, {0.25f, 0.00f, 0.25f, 0.25f}, {0.50f, 0.00f, 0.25f, 0.25f}, {0.75f, 0.00f, 0.25f, 0.25f},
{0.00f, 0.25f, 0.25f, 0.25f}, {0.25f, 0.25f, 0.25f, 0.25f}, {0.50f, 0.25f, 0.25f, 0.25f}, {0.75f, 0.25f, 0.25f, 0.25f},
{0.00f, 0.50f, 0.25f, 0.25f}, {0.25f, 0.50f, 0.25f, 0.25f}, {0.50f, 0.50f, 0.25f, 0.25f}, {0.75f, 0.50f, 0.25f, 0.25f},
{0.00f, 0.75f, 0.25f, 0.25f}, {0.25f, 0.75f, 0.25f, 0.25f}, {0.50f, 0.75f, 0.25f, 0.25f}, {0.75f, 0.75f, 0.25f, 0.25f},
}
},
{
"panoramic", 1,
{ {0.0f, 0.0f, 1.0f, 1.0f} }
},
{
/* Main 16:9 (75%×75%) + 3 preview right + bottom info strip. */
"main_with_strip", 5,
{
{0.0f, 0.0f, 0.75f, 0.75f},
{0.75f, 0.0f, 0.25f, 0.25f},
{0.75f, 0.25f, 0.25f, 0.25f},
{0.75f, 0.5f, 0.25f, 0.25f},
{0.0f, 0.75f, 1.0f, 0.25f},
}
},
};
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 const int g_layouts_count = (int)(sizeof(g_layouts) / sizeof(g_layouts[0]));
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)) return &g_layouts[i];
if (!strcmp(g_layouts[i].name, name)) { found = &g_layouts[i]; break; }
}
return NULL;
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;
return g_layouts;
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;
}