8cc5a5acfb
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 уже готовы.
357 lines
12 KiB
C
357 lines
12 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 <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;
|
||
}
|