From b8661f4017bd2d1fd27b376e8a10a3276b64a66a Mon Sep 17 00:00:00 2001 From: Evgeny Demchenko Date: Wed, 3 Jun 2026 07:05:35 +0100 Subject: [PATCH] =?UTF-8?q?nvenc:=20intra=20refresh=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20low-latency=20multi-source=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/grid_record.c | 10 +++++++++- include/cuframes_composer/nvenc.h | 7 +++++++ src/nvenc.c | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/examples/grid_record.c b/examples/grid_record.c index 525a4ca..03609eb 100644 --- a/examples/grid_record.c +++ b/examples/grid_record.c @@ -95,6 +95,7 @@ int main(int argc, char **argv) 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; @@ -135,10 +136,11 @@ int main(int argc, char **argv) {"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:", opts, NULL)) != -1) { + 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': @@ -175,6 +177,7 @@ int main(int argc, char **argv) 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. */ @@ -391,7 +394,12 @@ int main(int argc, char **argv) .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"); diff --git a/include/cuframes_composer/nvenc.h b/include/cuframes_composer/nvenc.h index d40fbf4..4be270a 100644 --- a/include/cuframes_composer/nvenc.h +++ b/include/cuframes_composer/nvenc.h @@ -47,6 +47,13 @@ typedef struct cfc_encoder_config { /* Пресет — соответствует NV_ENC_TUNING_INFO + preset GUID. * "ll" = low-latency, "p4" = preset P4 (баланс), "p7" = highest quality. */ const char *preset; /* "ll", "p4", "p7" — по умолчанию "ll" */ + + /* Intra refresh — вместо периодических IDR кодирует N столбцов intra-блоков + * в каждом кадре, за period кадров полный refresh. Ровный bitrate без + * spike'ов, идеально для low-latency multi-source RTSP push. + * 0 = выключено (GOP-based с IDR раз в gop_size). */ + int32_t intra_refresh; /* 0 = off, 1 = on */ + int32_t intra_refresh_period; /* кадров на полный цикл (например 25 = 1 сек @ 25fps) */ } cfc_encoder_config_t; typedef struct cfc_encoder cfc_encoder_t; diff --git a/src/nvenc.c b/src/nvenc.c index b606a2e..951c933 100644 --- a/src/nvenc.c +++ b/src/nvenc.c @@ -268,6 +268,22 @@ int cfc_encoder_create(const cfc_encoder_config_t *cfg, cfc_encoder_t **out) ec.encodeCodecConfig.h264Config.repeatSPSPPS = 1; ec.encodeCodecConfig.h264Config.idrPeriod = ec.gopLength; + /* Intra refresh — отключает периодические IDR, вместо этого N столбцов + * пикселей кодируются как intra в каждом кадре. За intra_refresh_period + * кадров полный refresh. Результат — ровный bitrate без spike'ов, идеально + * для multi-source RTSP push в mediamtx с маленьким writeQueueSize. + * Trade-off: новый клиент ждёт ~1 период для построения reference frame + * (для CCTV приемлемо). См. также NVENC SDK §Picture Encoding/Intra Refresh. */ + if (cfg->intra_refresh) { + int period = cfg->intra_refresh_period > 0 ? cfg->intra_refresh_period : 25; + ec.encodeCodecConfig.h264Config.enableIntraRefresh = 1; + ec.encodeCodecConfig.h264Config.intraRefreshPeriod = period; + ec.encodeCodecConfig.h264Config.intraRefreshCnt = period; + /* Отключаем периодические IDR — они не нужны при intra refresh. */ + ec.encodeCodecConfig.h264Config.idrPeriod = NVENC_INFINITE_GOPLENGTH; + ec.gopLength = NVENC_INFINITE_GOPLENGTH; + } + /* 4) Init encoder. */ NV_ENC_INITIALIZE_PARAMS ip = { 0 }; ip.version = NV_ENC_INITIALIZE_PARAMS_VER;