b8661f4017
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.
510 lines
20 KiB
C
510 lines
20 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 <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;
|
||
}
|