/* 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 #include #include #include #include #include #include #include #include #include #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 — может быть последним полем или иметь ',' после. */ 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 --cell key,x,y,w,h [--cell ...]\n" " ИЛИ: %s --out --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, }; 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/ с 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, }; 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/. Использует тот же 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; }