Files
cuframes-composer/examples/grid_record.c
T
gx 8cc5a5acfb Phase 3b: PNG icon overlays через libpng + alpha_blit_rgba_nv12
Live-validated на rtsp://192.168.88.23:554/cfc-grid с иконками
temp_outside.png и offline.png из cuda_grid_icons volume.

Содержимое:

- cugrid.h/cugrid.cu — cfc_cugrid_blit_rgba_nv12 (Y+UV α-blend) +
  два новых kernel'я blit_rgba_y/uv (BT.709 limited-range RGB→YUV
  conversion, 4:2:0 chroma 2x2 averaging).
- overlay.h — cfc_overlay_create_png + cfc_overlay_png_size +
  cfc_overlay_update_png.
- overlay.c — libpng decode (paletted/gray/16-bit → 8-bit RGBA),
  cuMemAlloc atlas в VRAM, cuMemcpyHtoD один раз, draw_png через
  cfc_cugrid_blit_rgba_nv12.
- CMakeLists.txt — find_package(PNG REQUIRED) + PNG::PNG в link.
- examples/grid_record — флаг --icon path,x,y[,alpha] (несколько раз
  можно). Атлас грузится один раз при старте.

PNG из cuda_grid_icons volume переиспользуются (offline, temp_outside,
grafana_gpu, и пр.). PNG decode'ятся одинаково — paletted, RGBA, gray.

Phase 3c (text rendering через FreeType + font atlas) — отдельный
commit. Атлас text'а тоже окажется в VRAM как RGBA через тот же
cfc_cugrid_blit_rgba_nv12 — kernels уже готовы.
2026-06-03 05:34:07 +01:00

357 lines
12 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 <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;
int fps = 25, bitrate = 10000, max_seconds = 0;
int out_w = 3840, out_h = 2160;
int border_thickness = 0; /* 0 = без border'ов */
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;
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] */
{0, 0, 0, 0},
};
int c;
while ((c = getopt_long(argc, argv, "o:c:f:b:W:H:s:r:i:", 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 '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);
/* 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",
};
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);
cfc_encoder_destroy(enc);
cfc_composer_destroy(comp);
cuCtxPopCurrent(NULL);
cuDevicePrimaryCtxRelease(dev);
return 0;
}