Files
cuframes-composer/examples/grid_record.c
T
gx 265c5c9503 detbox: label + score pill над bbox (FreeType)
Раньше detbox рисовал только рамку. Теперь на каждое active событие
отображает "label NN%" в полупрозрачной pill цвета рамки над верхним
краем bbox.

include/cuframes_composer/overlay.h:
- cfc_overlay_detbox_config_t: + font_path, font_size, label_bg_alpha
- cfc_overlay_detbox_upsert: + float score parameter

src/overlay.c:
- detbox_entry_t: + score, rendered_text (кеш), text_atlas/w/h/pitch
- detbox_data_t: + ft_library, ft_face, font_path_copy, font_size_px
- cfc_overlay_create_detection_boxes: opens FT face если font_path задан
- cfc_overlay_destroy: free cached atlas'ы + FT face/library
- detbox_rebuild_label_atlas: format "label NN%" → FT render → VRAM upload
  (reuse text_measure / text_render из CFC_OVERLAY_TEXT)
- upsert: вызывает rebuild при изменении label/score (кеш по rendered_text)
- draw_detection_boxes: snap расширен под text_atlas; после border рисует
  pill bg (fill_nv12 цветом рамки) + текст (blit_rgba_nv12, белый)
- mutex hold всю draw — atlas чтение должно быть atomic против upsert

src/frigate_mqtt.c:
- parse after.score → передаём в upsert (Frigate & yoloworld envelope
  совместимы: оба содержат score)

examples/grid_record.c:
- 4 frigate detbox: font_path=/fonts/DejaVuSans-Bold.ttf, font_size=16
- 4 yw detbox: то же — magenta pill с белым текстом

Live: cfc-grid healthy 25 fps, image gx/cuframes-composer:0.11b-step4
deployed. Видно на TV при первом detection event.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 03:08:14 +01:00

899 lines
40 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* grid_record — Phase 2 smoke test.
*
* Подписывается на 4 cuframes-источника (4 камеры), композирует их в 2×2 grid
* через cfc_composer, кодирует через NVENC, пишет H.264 в файл.
*
* Layout 2×2 1080p:
* Output: 3840×2160 (4K)
* Cells: 4 шт. 1920×1080 в углах
*
* Использование:
* grid_record --out 4k.h264 \
* --cell cam-parking,0,0,1920,1080 \
* --cell cam-back_yard,1920,0,1920,1080 \
* --cell cam-front_yard,0,1080,1920,1080 \
* --cell cam-gate_lpr,1920,1080,1920,1080 \
* --seconds 15
*
* Лицензия: LGPL-2.1+
*/
#include "../include/cuframes_composer/composer.h"
#include "../include/cuframes_composer/nvenc.h"
#include "../include/cuframes_composer/overlay.h"
#include "../include/cuframes_composer/control.h"
#include "../include/cuframes_composer/health.h"
#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>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define MAX_CELLS 16
static volatile sig_atomic_t g_stop = 0;
static void on_sig(int s) { (void)s; g_stop = 1; }
typedef struct write_ctx {
cfc_writer_t *writer;
uint64_t bytes_written;
uint64_t frames_encoded;
uint64_t idr_count;
} write_ctx_t;
static void on_bitstream(const uint8_t *bs, size_t size, int64_t pts_ns,
int is_idr, void *user)
{
write_ctx_t *ctx = (write_ctx_t *)user;
if (cfc_writer_write(ctx->writer, bs, size, pts_ns, is_idr) == 0) {
ctx->bytes_written += size;
ctx->frames_encoded++;
if (is_idr) ctx->idr_count++;
}
}
static int parse_cell(const char *arg, cfc_composer_cell_t *out,
char *key_storage)
{
/* Формат: key,x,y,w,h */
char buf[128];
strncpy(buf, arg, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ",");
if (!tok) return -1;
strncpy(key_storage, tok, 63);
key_storage[63] = '\0';
out->source_key = key_storage;
tok = strtok(NULL, ","); if (!tok) return -1; out->x = atoi(tok);
tok = strtok(NULL, ","); if (!tok) return -1; out->y = atoi(tok);
tok = strtok(NULL, ","); if (!tok) return -1; out->w = atoi(tok);
tok = strtok(NULL, ","); if (!tok) return -1; out->h = atoi(tok);
return 0;
}
static const char *cu_err(CUresult r)
{
const char *s = NULL;
cuGetErrorString(r, &s);
return s ? s : "?";
}
int main(int argc, char **argv)
{
const char *out_path = NULL;
/* Default'ы: 1080p / 4 Mbps подходят для GTX 1050 (Pascal), на которой
* крутится production. 4K требует --width 3840 --height 2160 явно. */
int fps = 25, bitrate = 4000, max_seconds = 0;
int out_w = 1920, out_h = 1080;
int border_thickness = 0; /* 0 = без border'ов */
int intra_refresh = 0; /* 1 = intra refresh вместо IDR (low-latency multi-source) */
cfc_composer_cell_t cells[MAX_CELLS] = { 0 };
static char cell_keys[MAX_CELLS][64];
int num_cells = 0;
/* --icon path,x,y[,alpha] */
typedef struct { const char *path; int x, y, alpha; } icon_spec_t;
icon_spec_t icons[MAX_CELLS] = { 0 };
int num_icons = 0;
/* --text font,size,r,g,b,x,y,text — формат хранения как есть в спецификации.
* Также можно префиксовать аргумент через "id=NAME:" — тогда overlay получает
* назначенный ID для управления через control plane. */
typedef struct { const char *font, *text; int size, r, g, b, x, y;
char id[32]; } text_spec_t;
text_spec_t texts[MAX_CELLS] = { 0 };
int num_texts = 0;
const char *control_endpoint = NULL; /* --control tcp://0.0.0.0:5599 */
const char *mqtt_host = NULL; /* --mqtt host[:port] */
int mqtt_port = 1883;
const char *mqtt_instance = "cfc-grid"; /* --mqtt-instance NAME */
const char *mqtt_user = NULL;
const char *mqtt_pass = NULL;
const char *out_format = "h264"; /* --format h264|mpegts */
const char *audio_source = NULL; /* --audio-source rtsp://.../live-audio */
const char *frigate_mqtt_host = NULL;
int frigate_mqtt_port = 1883;
const char *frigate_topic = "frigate/events";
/* YOLO-World subscriber (Phase 3 yolo-world-detector) — параллельный
* detection-overlay поток. Использует те же detection-cells что и
* Frigate, но рендерит bbox magenta цветом. По умолчанию выключен. */
const char *yw_mqtt_host = NULL;
int yw_mqtt_port = 1883;
const char *yw_topic = "yoloworld/events";
const char *mqtt_overlays_path = NULL; /* JSON-конфиг MQTT-driven text overlays */
const char *initial_layout = NULL; /* --layout NAME → set_layout после init */
int motion_mode = 0; /* --motion-mode */
int motion_ttl = 45000; /* --motion-ttl ms */
/* --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")
* dx,dy,dw,dh — координаты ячейки composer'а на output frame
* detect_w,detect_h — Frigate detect.{width,height} (640,480)
* zones (опц.) — colon-separated whitelist; если задан, события вне
* этих зон отбрасываются subscriber'ом (zone-filter
* заменяет Frigate-side objects.filters.required_zones
* который не работает в 0.17 schema). */
#define DETCELL_ZONE_MAX 8
typedef struct { char key[32], camera[48];
int dx, dy, dw, dh, detect_w, detect_h;
char zone_storage[DETCELL_ZONE_MAX][32];
const char *zone_ptrs[DETCELL_ZONE_MAX];
int num_zones; } detcell_t;
detcell_t detcells[MAX_CELLS] = { 0 };
int num_detcells = 0;
static struct option opts[] = {
{"out", required_argument, 0, 'o'},
{"cell", required_argument, 0, 'c'},
{"fps", required_argument, 0, 'f'},
{"bitrate", required_argument, 0, 'b'},
{"width", required_argument, 0, 'W'},
{"height", required_argument, 0, 'H'},
{"seconds", required_argument, 0, 's'},
{"border", required_argument, 0, 'r'}, /* толщина border'ов */
{"icon", required_argument, 0, 'i'}, /* path,x,y[,alpha] */
{"text", required_argument, 0, 't'}, /* font,size,r,g,b,x,y,text */
{"control", required_argument, 0, 'C'}, /* ZMQ bind endpoint */
{"mqtt", required_argument, 0, 'M'}, /* MQTT broker host[:port] */
{"mqtt-instance", required_argument, 0, 'I'}, /* instance ID для топиков */
{"mqtt-user", required_argument, 0, 'U'},
{"mqtt-pass", required_argument, 0, 'P'},
{"intra-refresh", no_argument, 0, 'R'},
{"format", required_argument, 0, 'F'}, /* h264|mpegts */
{"audio-source", required_argument, 0, 'A'}, /* RTSP audio URL */
{"frigate-mqtt", required_argument, 0, 'G'}, /* host[:port] */
{"frigate-topic", required_argument, 0, 'T'},
{"yw-mqtt", required_argument, 0, 'Y'}, /* host[:port] для yolo-world detector */
{"yw-topic", required_argument, 0, 'Q'},
{"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 */
{"mqtt-overlays", required_argument, 0, 'x'}, /* path to MQTT overlays JSON */
{0, 0, 0, 0},
};
const char *templates_path = NULL;
int c;
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:RF:A:G:T:Y:Q:D:L:S:mk:z:x:", opts, NULL)) != -1) {
switch (c) {
case 'o': out_path = optarg; break;
case 'c':
if (num_cells >= MAX_CELLS) {
fprintf(stderr, "max %d cells\n", MAX_CELLS);
return 1;
}
if (parse_cell(optarg, &cells[num_cells], cell_keys[num_cells]) != 0) {
fprintf(stderr, "invalid --cell '%s' (key,x,y,w,h)\n", optarg);
return 1;
}
num_cells++;
break;
case 'f': fps = atoi(optarg); break;
case 'b': bitrate = atoi(optarg); break;
case 'W': out_w = atoi(optarg); break;
case 'H': out_h = atoi(optarg); break;
case 's': max_seconds = atoi(optarg); break;
case 'r': border_thickness = atoi(optarg); break;
case 'C': control_endpoint = optarg; break;
case 'M': {
mqtt_host = optarg;
const char *colon = strchr(optarg, ':');
if (colon) {
static char host_buf[64];
int n = colon - optarg;
if (n >= (int)sizeof(host_buf)) n = sizeof(host_buf) - 1;
memcpy(host_buf, optarg, n); host_buf[n] = '\0';
mqtt_host = host_buf;
mqtt_port = atoi(colon + 1);
}
break;
}
case 'I': mqtt_instance = optarg; break;
case 'U': mqtt_user = optarg; break;
case 'P': mqtt_pass = optarg; break;
case 'R': intra_refresh = 1; break;
case 'F': out_format = optarg; break;
case 'A': audio_source = optarg; break;
case 'G': {
frigate_mqtt_host = optarg;
const char *colon = strchr(optarg, ':');
if (colon) {
static char buf[64];
int n = colon - optarg;
if (n >= (int)sizeof(buf)) n = sizeof(buf) - 1;
memcpy(buf, optarg, n); buf[n] = '\0';
frigate_mqtt_host = buf;
frigate_mqtt_port = atoi(colon + 1);
}
break;
}
case 'T': frigate_topic = optarg; break;
case 'Y': {
yw_mqtt_host = optarg;
const char *colon = strchr(optarg, ':');
if (colon) {
static char yw_host_buf[64];
int n = colon - optarg;
if (n >= (int)sizeof(yw_host_buf)) n = sizeof(yw_host_buf) - 1;
memcpy(yw_host_buf, optarg, n);
yw_host_buf[n] = '\0';
yw_mqtt_host = yw_host_buf;
yw_mqtt_port = atoi(colon + 1);
}
break;
}
case 'Q': yw_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 'x': mqtt_overlays_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';
char *p = buf, *q;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; strncpy(detcells[num_detcells].key, p, 31); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; strncpy(detcells[num_detcells].camera, p, 47); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; detcells[num_detcells].dx = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; detcells[num_detcells].dy = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; detcells[num_detcells].dw = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; detcells[num_detcells].dh = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --detection-cell\n"); return 1; }
*q = '\0'; detcells[num_detcells].detect_w = atoi(p); p = q + 1;
/* detect_h — может быть последним полем или иметь ',<zones>' после. */
q = strchr(p, ',');
if (q) {
*q = '\0'; detcells[num_detcells].detect_h = atoi(p); p = q + 1;
/* Парсим zones — colon-separated whitelist. */
detcells[num_detcells].num_zones = 0;
char *z = p;
while (z && *z && detcells[num_detcells].num_zones < DETCELL_ZONE_MAX) {
char *sep = strchr(z, ':');
int len = sep ? (int)(sep - z) : (int)strlen(z);
if (len > 31) len = 31;
int idx = detcells[num_detcells].num_zones;
memcpy(detcells[num_detcells].zone_storage[idx], z, len);
detcells[num_detcells].zone_storage[idx][len] = '\0';
detcells[num_detcells].zone_ptrs[idx] =
detcells[num_detcells].zone_storage[idx];
detcells[num_detcells].num_zones++;
z = sep ? sep + 1 : NULL;
}
} else {
detcells[num_detcells].detect_h = atoi(p);
}
num_detcells++;
break;
}
case 't': {
if (num_texts >= MAX_CELLS) { fprintf(stderr, "max %d texts\n", MAX_CELLS); return 1; }
/* Опциональный prefix "id=NAME:" — задаёт control-plane ID. */
const char *spec = optarg;
char id_buf[32] = { 0 };
if (!strncmp(spec, "id=", 3)) {
const char *colon = strchr(spec, ':');
if (colon) {
int n = colon - (spec + 3);
if (n >= (int)sizeof(id_buf)) n = sizeof(id_buf) - 1;
memcpy(id_buf, spec + 3, n);
id_buf[n] = '\0';
spec = colon + 1;
}
}
strncpy(texts[num_texts].id, id_buf, sizeof(texts[num_texts].id) - 1);
/* font,size,r,g,b,x,y,text — text идёт до конца строки (может
* содержать запятые и пробелы). */
static char text_font[MAX_CELLS][256];
static char text_body[MAX_CELLS][256];
char buf[512];
strncpy(buf, spec, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *p = buf;
char *q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; strncpy(text_font[num_texts], p, 255); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].size = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].r = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].g = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].b = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].x = atoi(p); p = q + 1;
q = strchr(p, ','); if (!q) { fprintf(stderr, "bad --text\n"); return 1; }
*q = '\0'; texts[num_texts].y = atoi(p); p = q + 1;
/* Остаток — text body. */
strncpy(text_body[num_texts], p, 255);
texts[num_texts].font = text_font[num_texts];
texts[num_texts].text = text_body[num_texts];
num_texts++;
break;
}
case 'i': {
if (num_icons >= MAX_CELLS) {
fprintf(stderr, "max %d icons\n", MAX_CELLS);
return 1;
}
/* Парсим path,x,y[,alpha] аналогично --cell.
* Спецификация хранится статично — указатель уйдёт в overlay. */
static char icon_paths[MAX_CELLS][256];
char buf[300]; strncpy(buf, optarg, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ","); if (!tok) { fprintf(stderr, "bad --icon\n"); return 1; }
strncpy(icon_paths[num_icons], tok, 255);
icons[num_icons].path = icon_paths[num_icons];
tok = strtok(NULL, ","); if (!tok) { fprintf(stderr, "bad --icon\n"); return 1; }
icons[num_icons].x = atoi(tok);
tok = strtok(NULL, ","); if (!tok) { fprintf(stderr, "bad --icon\n"); return 1; }
icons[num_icons].y = atoi(tok);
tok = strtok(NULL, ",");
icons[num_icons].alpha = tok ? atoi(tok) : 255;
num_icons++;
break;
}
default: return 1;
}
}
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"
" --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; }
CUdevice dev;
cuDeviceGet(&dev, 0);
CUcontext ctx;
cuDevicePrimaryCtxRetain(&ctx, dev);
cuCtxPushCurrent(ctx);
/* Composer. */
cfc_composer_config_t ccfg = {
.width = out_w,
.height = out_h,
.cells = cells,
.num_cells = num_cells,
.cuda_device = 0,
.consumer_prefix = mqtt_instance, /* уникальный namespace на каждый composer */
};
cfc_composer_t *comp = NULL;
if (cfc_composer_create(&ccfg, &comp) != 0) {
fprintf(stderr, "cfc_composer_create failed\n");
return 1;
}
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);
}
/* Глобальные MQTT-driven overlays (температура и т.п.) — JSON-конфиг.
* Каждая запись = MQTT subscribe + persistent text overlay. См.
* include/cuframes_composer/cpp/mqtt_overlay.hpp для schema. */
if (mqtt_overlays_path) {
extern int cfc_mqtt_overlays_load(cfc_composer_t *, const char *,
const char *, int,
const char *, const char *,
int, int);
int n = cfc_mqtt_overlays_load(comp, mqtt_overlays_path,
mqtt_host ? mqtt_host : "cctv-mosquitto", mqtt_port,
mqtt_user, mqtt_pass,
out_w, out_h);
fprintf(stderr, "[grid_record] mqtt overlays loaded: %d\n", n);
}
/* --layout NAME → applies named layout поверх --cell координат. Удобно
* как default для ONVIF PTZ-управляемого composer'а (старт в quad,
* далее set_layout через ZMQ). В motion-mode не работает (relayout
* перетирает на каждом кадре). */
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 = {
.font_path = texts[i].font,
.text = texts[i].text,
.pixel_size = texts[i].size,
.x = texts[i].x, .y = texts[i].y,
.r = texts[i].r, .g = texts[i].g, .b = texts[i].b,
.extra_alpha = 255,
.visible = 1,
};
cfc_overlay_t *ov = NULL;
if (cfc_overlay_create_text(&tc, &ov) != 0) {
fprintf(stderr, "[grid_record] text overlay create failed: '%s'\n",
texts[i].text);
continue;
}
if (texts[i].id[0]) cfc_overlay_set_id(ov, texts[i].id);
int tw = 0, th = 0;
cfc_overlay_text_size(ov, &tw, &th);
cfc_composer_add_overlay(comp, ov);
fprintf(stderr,
"[grid_record] text @ (%d,%d) %dx%d size=%d color=(%d,%d,%d) id='%s' '%s'\n",
texts[i].x, texts[i].y, tw, th, texts[i].size,
texts[i].r, texts[i].g, texts[i].b,
texts[i].id[0] ? texts[i].id : "", texts[i].text);
}
/* MQTT health publisher. */
cfc_health_t *hpub = NULL;
if (mqtt_host) {
cfc_health_config_t hc = {
.host = mqtt_host, .port = mqtt_port,
.username = mqtt_user, .password = mqtt_pass,
.topic_prefix = "composer",
.instance = mqtt_instance,
.interval_sec = 10,
.composer = comp,
.publish_discovery = 1,
};
cfc_health_create(&hc, &hpub);
}
/* Control plane. */
cfc_control_t *ctl = NULL;
if (control_endpoint) {
cfc_control_config_t cc = {
.bind_endpoint = control_endpoint,
.composer = comp,
.cuda_ctx = ctx,
};
if (cfc_control_create(&cc, &ctl) != 0) {
fprintf(stderr, "[grid_record] cfc_control_create failed\n");
}
}
/* Detection-box overlay'и (Phase 7 task #190). По одному на каждый
* --detection-cell. Цвет — насыщенный жёлто-зелёный (BT.709 limited). */
cfc_overlay_t *detbox_overlays[MAX_CELLS] = { 0 };
for (int i = 0; i < num_detcells; i++) {
cfc_overlay_detbox_config_t dc = {
.camera_key = detcells[i].camera,
.detect_w = detcells[i].detect_w,
.detect_h = detcells[i].detect_h,
.cell_x = detcells[i].dx, .cell_y = detcells[i].dy,
.cell_w = detcells[i].dw, .cell_h = detcells[i].dh,
.thickness = 6,
.color_y = 210, .color_u = 50, .color_v = 100, /* кислотно-зелёный */
.alpha = 240,
.stale_ms = 8000,
.required_zones = detcells[i].num_zones ? detcells[i].zone_ptrs : NULL,
.required_zones_count = detcells[i].num_zones,
/* Label+score pill — белым текстом на полупрозрачном зелёном
* фоне (color_y/u/v). Шрифт DejaVu mounted из /fonts (см. compose). */
.font_path = "/fonts/DejaVuSans-Bold.ttf",
.font_size = 16,
.label_bg_alpha = 200,
};
if (cfc_overlay_create_detection_boxes(&dc, &detbox_overlays[i]) != 0) {
fprintf(stderr, "[grid_record] detbox create failed для '%s'\n",
detcells[i].camera);
continue;
}
cfc_composer_add_overlay(comp, detbox_overlays[i]);
fprintf(stderr, "[grid_record] detbox '%s' → cell %s (%d,%d %dx%d), detect %dx%d, zones=%d",
detcells[i].camera, detcells[i].key,
detcells[i].dx, detcells[i].dy, detcells[i].dw, detcells[i].dh,
detcells[i].detect_w, detcells[i].detect_h,
detcells[i].num_zones);
for (int z = 0; z < detcells[i].num_zones; z++)
fprintf(stderr, " %s", detcells[i].zone_storage[z]);
fprintf(stderr, "\n");
}
/* YOLO-World detection-box overlays — параллельный набор для второго
* subscriber'а. Magenta цвет (BT.709 limited Y=105 U=212 V=234). Те же
* detection-cells (camera/zones), но bbox рисуется magenta. На один
* frame можно увидеть зелёный bbox от Frigate И magenta от YOLO-World
* — если оба детектят. yolo-world-detector публикует в MQTT topic
* yoloworld/events/<camera> с Frigate-compat envelope. */
cfc_overlay_t *yw_detbox_overlays[MAX_CELLS] = { 0 };
if (yw_mqtt_host) {
for (int i = 0; i < num_detcells; i++) {
cfc_overlay_detbox_config_t yc = {
.camera_key = detcells[i].camera,
.detect_w = detcells[i].detect_w,
.detect_h = detcells[i].detect_h,
.cell_x = detcells[i].dx, .cell_y = detcells[i].dy,
.cell_w = detcells[i].dw, .cell_h = detcells[i].dh,
.thickness = 6,
.color_y = 105, .color_u = 212, .color_v = 234, /* magenta */
.alpha = 240,
.stale_ms = 8000,
.required_zones = detcells[i].num_zones ? detcells[i].zone_ptrs : NULL,
.required_zones_count = detcells[i].num_zones,
.font_path = "/fonts/DejaVuSans-Bold.ttf",
.font_size = 16,
.label_bg_alpha = 200,
};
if (cfc_overlay_create_detection_boxes(&yc, &yw_detbox_overlays[i]) != 0) {
fprintf(stderr, "[grid_record] yw detbox create failed для '%s'\n",
detcells[i].camera);
continue;
}
cfc_composer_add_overlay(comp, yw_detbox_overlays[i]);
fprintf(stderr, "[grid_record] yw detbox '%s' → cell %s (magenta)\n",
detcells[i].camera, detcells[i].key);
}
}
/* 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 || 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++) {
if (detbox_overlays[i]) {
cfc_frigate_mqtt_register_overlay(frigate, detbox_overlays[i]);
}
}
cfc_frigate_mqtt_start(frigate);
} else {
fprintf(stderr, "[grid_record] frigate_mqtt create failed\n");
}
}
/* YOLO-World MQTT subscriber — параллельный поток detection-events с
* yoloworld/events/<camera>. Использует тот же envelope как Frigate
* (cfc_frigate_mqtt парсер совместим), но рендерит на yw_detbox_overlays
* (magenta). motion-pulse'ы НЕ шлёт (composer NULL), композитор
* управляется только Frigate motion-pulse'ами. */
cfc_frigate_mqtt_t *yw_mqtt = NULL;
if (yw_mqtt_host && num_detcells > 0) {
cfc_frigate_mqtt_config_t yc = {
.host = yw_mqtt_host, .port = yw_mqtt_port,
.username = mqtt_user, .password = mqtt_pass,
.topic = yw_topic,
.composer = NULL, /* yolo-world не управляет motion-layout */
};
if (cfc_frigate_mqtt_create(&yc, &yw_mqtt) == 0) {
for (int i = 0; i < num_detcells; i++) {
if (yw_detbox_overlays[i]) {
cfc_frigate_mqtt_register_overlay(yw_mqtt, yw_detbox_overlays[i]);
}
}
cfc_frigate_mqtt_start(yw_mqtt);
fprintf(stderr, "[grid_record] yw_mqtt started → %s:%d topic=%s\n",
yw_mqtt_host, yw_mqtt_port, yw_topic);
} else {
fprintf(stderr, "[grid_record] yw_mqtt create failed\n");
}
}
/* PNG иконки. */
for (int i = 0; i < num_icons; i++) {
cfc_overlay_png_config_t pc = {
.path = icons[i].path,
.x = icons[i].x,
.y = icons[i].y,
.extra_alpha = icons[i].alpha,
.visible = 1,
};
cfc_overlay_t *ov = NULL;
if (cfc_overlay_create_png(&pc, &ov) != 0) {
fprintf(stderr, "[grid_record] PNG '%s' load failed\n", icons[i].path);
continue;
}
int iw = 0, ih = 0;
cfc_overlay_png_size(ov, &iw, &ih);
cfc_composer_add_overlay(comp, ov);
fprintf(stderr, "[grid_record] icon '%s' %dx%d @ (%d,%d) alpha=%d\n",
icons[i].path, iw, ih, icons[i].x, icons[i].y, icons[i].alpha);
}
/* Border'ы вокруг каждой ячейки если --border задан.
* Цвет: серо-голубой (BT.709 limited): Y=180, U=120, V=110. */
if (border_thickness > 0) {
for (int i = 0; i < num_cells; i++) {
cfc_overlay_border_config_t bc = {
.x = cells[i].x, .y = cells[i].y,
.w = cells[i].w, .h = cells[i].h,
.thickness = border_thickness,
.color_y = 180, .color_u = 120, .color_v = 110,
.alpha = 220,
.visible = 1,
};
cfc_overlay_t *ov = NULL;
if (cfc_overlay_create_border(&bc, &ov) == 0) {
cfc_composer_add_overlay(comp, ov);
}
}
fprintf(stderr, "[grid_record] добавлены border'ы (толщина %d) для %d ячеек\n",
border_thickness, num_cells);
}
/* Encoder. */
cfc_encoder_config_t ecfg = {
.cuda_ctx = ctx,
.width = out_w,
.height = out_h,
.fps_num = fps,
.fps_den = 1,
.bitrate_kbps = bitrate,
.gop_size = fps,
.num_b_frames = 0,
.preset = "ll",
.intra_refresh = intra_refresh,
.intra_refresh_period = fps, /* полный цикл за 1 секунду */
};
if (intra_refresh) {
fprintf(stderr, "[grid_record] intra refresh ON (period=%d кадров)\n", fps);
}
cfc_encoder_t *enc = NULL;
if (cfc_encoder_create(&ecfg, &enc) != 0) {
fprintf(stderr, "cfc_encoder_create failed\n");
cfc_composer_destroy(comp);
return 1;
}
/* Audio consumer (опциональный, Phase 7). Запускаем РАНЬШЕ writer'а
* чтобы успеть получить codec params (sample_rate, channels, extradata)
* до avformat_write_header — иначе audio stream'у не будет правильного
* setup'а. Polling до 5 секунд. */
cfc_audio_t *audio = NULL;
int audio_sample_rate = 0, audio_channels = 0;
const uint8_t *audio_extradata = NULL;
size_t audio_extradata_size = 0;
if (audio_source) {
cfc_audio_config_t acfg = { .rtsp_url = audio_source };
if (cfc_audio_create(&acfg, &audio) != 0) {
fprintf(stderr, "[grid_record] audio create failed, продолжаю без audio\n");
} else {
fprintf(stderr, "[grid_record] жду audio codec params от %s ...\n", audio_source);
/* 30 секунд polling — audio source (cuda-grid-audio) может ещё
* подниматься после recreate стeка. Audio thread сам retry'ится
* с exp backoff. */
for (int i = 0; i < 300; i++) { /* 300 × 100ms = 30s */
if (cfc_audio_get_codec_params(audio, &audio_sample_rate,
&audio_channels, &audio_extradata,
&audio_extradata_size) == 0) {
fprintf(stderr,
"[grid_record] audio готов: AAC %dHz %dch extradata=%zub\n",
audio_sample_rate, audio_channels, audio_extradata_size);
break;
}
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 * 1000 * 1000 };
nanosleep(&ts, NULL);
}
if (audio_sample_rate == 0) {
fprintf(stderr, "[grid_record] audio params не получены за 30с, без audio\n");
cfc_audio_destroy(audio); audio = NULL;
}
}
}
/* Writer: mpegts с video + опциональным audio. */
uint8_t spspps[256]; size_t spspps_len = sizeof(spspps);
cfc_encoder_get_sequence_params(enc, spspps, &spspps_len);
cfc_writer_config_t wcfg = {
.path = out_path,
.format = out_format,
.width = out_w,
.height = out_h,
.fps_num = fps,
.fps_den = 1,
.bitrate_kbps = bitrate,
.extradata = spspps,
.extradata_size = spspps_len,
.has_audio = audio ? 1 : 0,
.audio_sample_rate = audio_sample_rate,
.audio_channels = audio_channels,
.audio_extradata = audio_extradata,
.audio_extradata_size = audio_extradata_size,
};
write_ctx_t wctx = { 0 };
if (cfc_writer_create(&wcfg, &wctx.writer) != 0) {
fprintf(stderr, "cfc_writer_create(%s, %s) failed\n", out_path, out_format);
if (audio) cfc_audio_destroy(audio);
cfc_encoder_destroy(enc);
cfc_composer_destroy(comp);
return 1;
}
fprintf(stderr, "[grid_record] начало записи в %s [format=%s%s] (Ctrl+C для остановки)\n",
out_path, out_format, audio ? "+audio" : "");
/* Main loop — frame cadence по wall clock'у. */
struct timespec ts_start;
clock_gettime(CLOCK_MONOTONIC, &ts_start);
int64_t start_us = (int64_t)ts_start.tv_sec * 1000000 + ts_start.tv_nsec / 1000;
int64_t frame_us = 1000000 / fps;
int64_t next_us = start_us;
while (!g_stop) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int64_t now_us = (int64_t)now.tv_sec * 1000000 + now.tv_nsec / 1000;
if (now_us < next_us) {
int64_t sleep_us = next_us - now_us;
if (sleep_us > 1000000) sleep_us = 1000000;
struct timespec ts = {
.tv_sec = sleep_us / 1000000,
.tv_nsec = (sleep_us % 1000000) * 1000,
};
nanosleep(&ts, NULL);
continue;
}
next_us += frame_us;
CUdeviceptr out_y = 0;
int out_pitch = 0, oW = 0, oH = 0;
if (cfc_composer_compose(comp, &out_y, &out_pitch, &oW, &oH) != 0) {
fprintf(stderr, "[grid_record] compose failed\n");
break;
}
int64_t pts_ns = (now_us - start_us) * 1000;
/* Не break'аем при encode/write failure — это обычно временно
* (mediamtx reconnect, socket broken). Просто логируем и продолжаем,
* следующая encode/write попытается заново. */
if (cfc_encoder_encode_frame(enc, out_y, out_pitch, pts_ns,
on_bitstream, &wctx) != 0) {
static int warned = 0;
if (!warned) { fprintf(stderr, "[grid_record] encode failed (продолжаю)\n"); warned = 1; }
}
/* Drain audio packets — пишем сразу после video frame. */
if (audio) cfc_audio_drain(audio, wctx.writer, 8);
if (wctx.frames_encoded > 0 && wctx.frames_encoded % 50 == 0) {
double elapsed = (now_us - start_us) / 1e6;
cfc_composer_health_t h;
cfc_composer_get_health(comp, &h);
fprintf(stderr,
"[grid_record] %llu кадров, %llu IDR, %.1f МБ за %.1fс (%.1f fps) | "
"src active=%d stale=%d dead=%d\n",
(unsigned long long)wctx.frames_encoded,
(unsigned long long)wctx.idr_count,
wctx.bytes_written / 1048576.0,
elapsed,
wctx.frames_encoded / elapsed,
h.active, h.stale, h.dead);
}
if (max_seconds > 0 && (now_us - start_us) / 1000000 >= max_seconds) {
fprintf(stderr, "[grid_record] лимит %dс\n", max_seconds);
break;
}
}
fprintf(stderr, "[grid_record] flush encoder\n");
cfc_encoder_flush(enc, on_bitstream, &wctx);
fprintf(stderr,
"[grid_record] итого: %llu кадров, %llu IDR, %.2f МБ\n",
(unsigned long long)wctx.frames_encoded,
(unsigned long long)wctx.idr_count,
wctx.bytes_written / 1048576.0);
cfc_writer_close(wctx.writer);
if (frigate) cfc_frigate_mqtt_destroy(frigate);
if (yw_mqtt) cfc_frigate_mqtt_destroy(yw_mqtt);
if (audio) cfc_audio_destroy(audio);
if (ctl) cfc_control_destroy(ctl);
if (hpub) cfc_health_destroy(hpub);
cfc_encoder_destroy(enc);
cfc_composer_destroy(comp);
cuCtxPopCurrent(NULL);
cuDevicePrimaryCtxRelease(dev);
return 0;
}