265c5c9503
Раньше 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>
899 lines
40 KiB
C
899 lines
40 KiB
C
/* 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;
|
||
}
|