Phase 9 #193: runtime layout switching через ZMQ set_layout

10 predefined layouts (single, dual_horizontal, dual_vertical, quad,
main_plus_preview, six_grid, nine_grid, sixteen_grid, panoramic,
main_with_strip) — normalized [0..1] координаты, port из vf_cuda_grid.c
старого FFmpeg patch'а. Применяются к фактическому output разрешению
композитора через cfc_composer_set_layout(name).

Source pool НЕ пересоздаётся: sources привязаны к индексам cells, layout
меняет только геометрию (cell.x/y/w/h). Это даёт zero-disruption switch
без потери накопленного state и без re-subscribe к cuframes publishers.

ZMQ verbs: set_layout / list_layouts / get_layout. CLI: --layout=NAME
перетирает --cell координаты на старте.

Используется ONVIF wrapper'ом (gx/cctv-onvif:0.1) для PTZ presets:
GotoPreset(token) → ZMQ set_layout(token).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 18:14:58 +01:00
parent ac86534769
commit 2d7fc1e640
7 changed files with 280 additions and 1 deletions
+15 -1
View File
@@ -26,6 +26,7 @@
#include "../include/cuframes_composer/writer.h"
#include "../include/cuframes_composer/audio.h"
#include "../include/cuframes_composer/frigate_mqtt.h"
#include "../include/cuframes_composer/layouts.h"
#include <cuda.h>
@@ -126,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 *initial_layout = NULL; /* --layout NAME → set_layout после init */
/* --detection-cell key,camera,dx,dy,dw,dh,detect_w,detect_h[,zone1:zone2:...]
* key — символьное имя для логов (например "parking")
* camera — Frigate camera_key для MQTT match'а ("parking_overview")
@@ -166,10 +168,11 @@ int main(int argc, char **argv)
{"frigate-mqtt", required_argument, 0, 'G'}, /* host[:port] */
{"frigate-topic", required_argument, 0, 'T'},
{"detection-cell", required_argument, 0, 'D'},
{"layout", required_argument, 0, 'L'}, /* named layout (quad, single, ...) */
{0, 0, 0, 0},
};
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:", 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:", opts, NULL)) != -1) {
switch (c) {
case 'o': out_path = optarg; break;
case 'c':
@@ -223,6 +226,7 @@ int main(int argc, char **argv)
break;
}
case 'T': frigate_topic = optarg; break;
case 'L': initial_layout = optarg; 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';
@@ -374,6 +378,16 @@ int main(int argc, char **argv)
fprintf(stderr, "[grid_record] composer %dx%d, %d ячеек\n",
out_w, out_h, num_cells);
/* --layout NAME → applies named layout поверх --cell координат. Удобно
* как default для ONVIF PTZ-управляемого composer'а (старт в quad,
* далее set_layout через ZMQ). */
if (initial_layout) {
if (cfc_composer_set_layout(comp, initial_layout) != 0) {
fprintf(stderr, "[grid_record] --layout '%s' unknown\n", initial_layout);
return 1;
}
}
/* TEXT overlays. */
for (int i = 0; i < num_texts; i++) {
cfc_overlay_text_config_t tc = {
+18
View File
@@ -99,6 +99,24 @@ int cfc_composer_add_overlay(cfc_composer_t *comp, cfc_overlay_t *ov);
* (содержимое overlay'я под mutex'ом не лежит, нужно лочиться вызывающему). */
cfc_overlay_t *cfc_composer_find_overlay(cfc_composer_t *comp, const char *id);
/* Переключить layout runtime: применяет normalized cells из named layout
* к фактическому output разрешению, пересчитывает comp->cells[].x/y/w/h.
*
* Source pool НЕ пересоздаётся: source threads привязаны к индексам ячеек
* (cell[0]=source[0], cell[1]=source[1], ...). Если у нового layout
* больше ячеек чем sources — лишние cells без источника останутся blackout'нутыми.
* Если у нового layout меньше ячеек чем sources — лишние sources продолжают
* работать, но не draw'аются (Phase 9 acceptable).
*
* Thread-safety: caller (ZMQ control thread) должен гарантировать что
* compose thread не выполняется параллельно. На single-threaded compose
* loop'е (Phase 2/3) это natural: control обработает запрос между кадрами. */
int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name);
/* Получить имя текущего layout'а (если был задан через set_layout).
* Возвращает NULL если cells выставлены вручную через --cell. */
const char *cfc_composer_current_layout(cfc_composer_t *comp);
/* Получить layout статистику по источникам — для debug / health-репортов. */
typedef struct cfc_composer_health {
int total; /* всего источников */
+44
View File
@@ -0,0 +1,44 @@
/* cuframes-composer — predefined layout templates.
*
* Phase 9 (task #194): runtime layout switching через ZMQ verb + ONVIF PTZ.
*
* Layouts описаны normalized (0..1) — масштабируются к фактическому output
* width/height композитора при apply_to_cells(). Список синхронизирован с
* historic'ой `vf_cuda_grid.c` layouts[] (старый FFmpeg patch), что
* обеспечивает совместимость с Python controller'ом и предыдущими ONVIF
* presets.
*
* Лицензия: LGPL-2.1+
*/
#ifndef CUFRAMES_COMPOSER_LAYOUTS_H
#define CUFRAMES_COMPOSER_LAYOUTS_H
#ifdef __cplusplus
extern "C" {
#endif
#define CFC_LAYOUT_MAX_CELLS 16
#define CFC_LAYOUT_MAX_NAME 32
typedef struct cfc_layout_cell {
float x, y, w, h; /* normalized fraction of output dims */
} cfc_layout_cell_t;
typedef struct cfc_layout {
const char *name;
int nb_cells;
cfc_layout_cell_t cells[CFC_LAYOUT_MAX_CELLS];
} cfc_layout_t;
/* Найти layout по имени; NULL если нет. */
const cfc_layout_t *cfc_layout_find(const char *name);
/* Возвращает указатель на array всех layout'ов + их количество. */
const cfc_layout_t *cfc_layout_all(int *out_count);
#ifdef __cplusplus
}
#endif
#endif /* CUFRAMES_COMPOSER_LAYOUTS_H */
+1
View File
@@ -21,6 +21,7 @@ set(COMPOSER_SOURCES_C
writer.c
audio.c
frigate_mqtt.c
layouts.c
)
set(COMPOSER_SOURCES_CU
cugrid/cugrid.cu
+57
View File
@@ -25,6 +25,7 @@
#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>
@@ -61,6 +62,10 @@ struct cfc_composer {
/* 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];
};
/* ── Helpers ──────────────────────────────────────────────────────────── */
@@ -111,6 +116,8 @@ 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;
cfc_source_snapshot_t snap;
cfc_source_get_latest(src, &snap);
@@ -281,6 +288,56 @@ cfc_overlay_t *cfc_composer_find_overlay(cfc_composer_t *comp, const char *id)
return NULL;
}
int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name)
{
if (!comp || !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;
/* Применяем нормализованные координаты к каждой существующей ячейке.
* source_key и индексы сохраняются. Если у layout'а меньше cells чем у
* composer'а — лишние оставляем с нулевой геометрией (compose_cell
* увидит w<=0 и пропустит). */
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;
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;
}
int cfc_composer_get_health(cfc_composer_t *comp, cfc_composer_health_t *out)
{
if (!comp || !out) return -1;
+45
View File
@@ -19,6 +19,7 @@
*/
#include "../include/cuframes_composer/control.h"
#include "../include/cuframes_composer/layouts.h"
#include "../include/cuframes_composer/overlay.h"
#include <cuda.h>
@@ -162,6 +163,47 @@ static void cmd_set_visible(void *sock, cfc_composer_t *comp, struct json_object
send_error(sock, "set_visible not implemented for this overlay type");
}
static void cmd_set_layout(void *sock, cfc_composer_t *comp, struct json_object *req)
{
const char *name = get_str(req, "name");
if (!name) { send_error(sock, "missing 'name'"); return; }
if (cfc_composer_set_layout(comp, name) != 0) {
send_error(sock, "unknown layout (см. list_layouts для допустимых)");
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, "layout", json_object_new_string(name));
send_json(sock, o);
}
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);
}
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);
send_json(sock, o);
}
static void cmd_get_layout(void *sock, cfc_composer_t *comp)
{
const char *cur = cfc_composer_current_layout(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, "layout",
cur ? json_object_new_string(cur) : NULL);
send_json(sock, o);
}
static void cmd_list_overlays(void *sock, cfc_composer_t *comp)
{
struct json_object *arr = json_object_new_array();
@@ -197,6 +239,9 @@ static void dispatch(void *sock, cfc_composer_t *comp, const char *json_str, siz
else if (!strcmp(cmd, "set_text")) cmd_set_text(sock, comp, req);
else if (!strcmp(cmd, "set_visible")) cmd_set_visible(sock, comp, req);
else if (!strcmp(cmd, "list_overlays")) cmd_list_overlays(sock, comp);
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 send_error(sock, "unknown cmd");
out:
+100
View File
@@ -0,0 +1,100 @@
/* Predefined layouts table — port из vf_cuda_grid.c (старый FFmpeg patch).
*
* Layouts normalized [0..1], apply'ятся к фактическому output разрешению
* композитора в момент set_layout — не привязаны к конкретным cell coords.
*
* Лицензия: LGPL-2.1+
*/
#include "../include/cuframes_composer/layouts.h"
#include <stddef.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 const int g_layouts_count = (int)(sizeof(g_layouts) / sizeof(g_layouts[0]));
const cfc_layout_t *cfc_layout_find(const char *name)
{
if (!name) return NULL;
for (int i = 0; i < g_layouts_count; i++) {
if (!strcmp(g_layouts[i].name, name)) return &g_layouts[i];
}
return NULL;
}
const cfc_layout_t *cfc_layout_all(int *out_count)
{
if (out_count) *out_count = g_layouts_count;
return g_layouts;
}