Files
cuframes-composer/examples/grid_record.c
T
gx b8661f4017 nvenc: intra refresh для low-latency multi-source push
Phase 5e revisited: visual artefacts в bottom rows 16-cell grid'а оказались
не race condition в cuframes ring buffer (как первоначальная гипотеза),
а burstiness IDR. Сложный grid (много границ между ячейками) генерит
огромные IDR (~400 КБ), которые переполняют mediamtx writeQueueSize=256
и discard'ятся → VLC видит покалеченный bitstream.

Правильное решение — canonical low-latency streaming pattern: вместо
периодических IDR использовать NVENC intra refresh. Вместо одного gigant'а
intra-кадра раз в секунду, кодируем N столбцов intra-блоков в каждом кадре.
За intra_refresh_period кадров — полный refresh. Bitstream становится
почти ровным — все кадры одинакового размера, никаких spike'ов.

Это индустриальный стандарт для low-latency: WebRTC, Twitch low-latency
mode, GeForce NOW, Zoom — все используют intra refresh без IDR.

Trade-off: новый клиент ждёт ~1 период (1 сек) для построения reference
frame. Для CCTV приемлемо. ffmpeg snapshot one-shot не работает на таких
потоках (нужен полный warmup), но VLC/TV/Frigate handles штатно.

Содержимое:

- cfc_encoder_config_t: добавлены intra_refresh + intra_refresh_period.
- nvenc.c: при enableIntraRefresh=1 устанавливается
  intraRefreshPeriod/intraRefreshCnt и идуs IDR period отключается через
  NVENC_INFINITE_GOPLENGTH.
- examples/grid_record: флаг --intra-refresh (период = fps).

Live-validated: rtsp://192.168.88.23:554/load16ir
- 16-cell 1080p 4×4 6Mbps intra refresh ON
- mediamtx tишина: ни одного 'reader is too slow' warning'а
- VLC connect → чистая картинка во всех 16 ячейках
- 100 кадров логи: '1 IDR' (только начальный) — после стартового никаких
  больше IDR не было, ровный bitstream

Этот flag не default для случая single-source (Phase 1 simple_record) —
там IDR-based GOP всё ещё лучше (полный keyframe = быстрый connect).
Включать осознанно для multi-source grid'ов через --intra-refresh.
2026-06-03 07:05:35 +01:00

510 lines
20 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 <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 {
FILE *fp;
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)
{
(void)pts_ns;
write_ctx_t *ctx = (write_ctx_t *)user;
if (fwrite(bs, 1, size, ctx->fp) == size) {
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;
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'},
{0, 0, 0, 0},
};
int c;
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:t:C:M:I:U:P:R", 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 '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) {
fprintf(stderr,
"Использование: %s --out <file.h264> --cell key,x,y,w,h [--cell ...]\n"
" [--width 3840] [--height 2160] [--fps 25]\n"
" [--bitrate 10000] [--seconds N]\n",
argv[0]);
return 1;
}
signal(SIGINT, on_sig);
signal(SIGTERM, on_sig);
/* 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,
};
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);
/* 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");
}
}
/* 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;
}
/* Output: "-" / "/dev/stdout" / "pipe:1" = stdout (для pipe в ffmpeg).
* stdout не закрывается через fclose чтобы не убивать дочерний процесс
* raньше времени. */
write_ctx_t wctx = { 0 };
int is_stdout = (!strcmp(out_path, "-") || !strcmp(out_path, "pipe:1") ||
!strcmp(out_path, "/dev/stdout"));
if (is_stdout) {
wctx.fp = stdout;
/* line-buffer'инг disabled — пишем full-buffered для производительности.
* Caller'у нужно flush при exit. */
setvbuf(stdout, NULL, _IOFBF, 1024 * 1024);
} else {
wctx.fp = fopen(out_path, "wb");
if (!wctx.fp) {
fprintf(stderr, "fopen(%s): %s\n", out_path, strerror(errno));
cfc_encoder_destroy(enc);
cfc_composer_destroy(comp);
return 1;
}
}
fprintf(stderr, "[grid_record] начало записи в %s (Ctrl+C для остановки)\n",
out_path);
/* 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;
if (cfc_encoder_encode_frame(enc, out_y, out_pitch, pts_ns,
on_bitstream, &wctx) != 0) {
fprintf(stderr, "[grid_record] encode failed\n");
break;
}
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);
fflush(wctx.fp);
if (!is_stdout) fclose(wctx.fp);
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;
}