From 2d7fc1e6400f3052081e47da7f81a1164410ae48 Mon Sep 17 00:00:00 2001 From: Evgeny Demchenko Date: Wed, 3 Jun 2026 18:14:58 +0100 Subject: [PATCH] =?UTF-8?q?Phase=209=20#193:=20runtime=20layout=20switchin?= =?UTF-8?q?g=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20ZMQ=20set=5Flayout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/grid_record.c | 16 ++++- include/cuframes_composer/composer.h | 18 +++++ include/cuframes_composer/layouts.h | 44 ++++++++++++ src/CMakeLists.txt | 1 + src/composer.c | 57 +++++++++++++++ src/control.c | 45 ++++++++++++ src/layouts.c | 100 +++++++++++++++++++++++++++ 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 include/cuframes_composer/layouts.h create mode 100644 src/layouts.c diff --git a/examples/grid_record.c b/examples/grid_record.c index dc20473..bd95908 100644 --- a/examples/grid_record.c +++ b/examples/grid_record.c @@ -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 @@ -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 = { diff --git a/include/cuframes_composer/composer.h b/include/cuframes_composer/composer.h index 85f04b7..45b381e 100644 --- a/include/cuframes_composer/composer.h +++ b/include/cuframes_composer/composer.h @@ -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; /* всего источников */ diff --git a/include/cuframes_composer/layouts.h b/include/cuframes_composer/layouts.h new file mode 100644 index 0000000..85fbdb4 --- /dev/null +++ b/include/cuframes_composer/layouts.h @@ -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 */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c49b8c..d6d79fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ set(COMPOSER_SOURCES_C writer.c audio.c frigate_mqtt.c + layouts.c ) set(COMPOSER_SOURCES_CU cugrid/cugrid.cu diff --git a/src/composer.c b/src/composer.c index e07bef4..232ed25 100644 --- a/src/composer.c +++ b/src/composer.c @@ -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 @@ -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; diff --git a/src/control.c b/src/control.c index 7da0c17..165ffdf 100644 --- a/src/control.c +++ b/src/control.c @@ -19,6 +19,7 @@ */ #include "../include/cuframes_composer/control.h" +#include "../include/cuframes_composer/layouts.h" #include "../include/cuframes_composer/overlay.h" #include @@ -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: diff --git a/src/layouts.c b/src/layouts.c new file mode 100644 index 0000000..91b60b0 --- /dev/null +++ b/src/layouts.c @@ -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 +#include + +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; +}