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:
+15
-1
@@ -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 = {
|
||||
|
||||
@@ -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; /* всего источников */
|
||||
|
||||
@@ -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 */
|
||||
@@ -21,6 +21,7 @@ set(COMPOSER_SOURCES_C
|
||||
writer.c
|
||||
audio.c
|
||||
frigate_mqtt.c
|
||||
layouts.c
|
||||
)
|
||||
set(COMPOSER_SOURCES_CU
|
||||
cugrid/cugrid.cu
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user