filter + demuxer: per-cell placeholder + cuframes auto-reconnect

vf_cuda_grid: placeholder branch теперь ищет per-cell icon "<base>_<pad>.png"
сначала, fallback к "<base>.png". Controller рендерит per-cell PNGs с camera
labels из FrigateBridge config (placeholder_renderer.py).

cuframesdec: + try_reconnect() — на CUFRAMES_ERR_DISCONNECTED не возвращаем
EOF (которое kill'ит весь pipeline), а пытаемся re-subscribe каждые 2 sec.
EAGAIN tells ffmpeg "try later". Когда publisher container recreate'нут
(new IPC namespace), pipeline auto-reconnects к нему без ffmpeg restart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gx
2026-05-25 12:00:24 +01:00
parent 4795b7a7f1
commit 169a4b2c14
2 changed files with 83 additions and 7 deletions
+13 -5
View File
@@ -244,7 +244,7 @@ typedef struct CudaGridContext {
int max_cells; /* Phase 7: число input pads creates в init() */
int aspect_mode; /* 0=stretch, 1=fit (letterbox/pillarbox) */
int placeholder_timeout_ms; /* Stale input → blit placeholder. 0=disable */
char *placeholder_icon; /* icon name (resolved via icon_dir) для placeholder */
char *placeholder_icon; /* icon base name (controller generates "<base>_<pad>.png" per-cell) */
/* Per-input stale detection (wall-clock based) */
int64_t last_frame_pts[MAX_CELLS];
@@ -1363,10 +1363,18 @@ static int cuda_grid_compose(FFFrameSync *fs)
DIV_UP(half_cw, BLOCKX), DIV_UP(half_ch, BLOCKY), 1,
BLOCKX, BLOCKY, 1, 0, s->cu_stream, args, NULL));
}
/* Blit placeholder centered (если loaded). Используем существующий
* icon atlas machinery — placeholder = icon с именем s->placeholder_icon. */
/* Per-cell placeholder сначала ("<base>_<pad>"), fallback к generic.
* Иconки: offline_0.png = "ПАРКИНГ", offline_1.png = "ПЕРЕДНИЙ ДВОР", etc.
* Если per-cell не нашли → загружаем generic с именем s->placeholder_icon. */
if (s->placeholder_icon && s->placeholder_icon[0]) {
if (ensure_icon_atlas(ctx, s->placeholder_icon, &pa) == 0 && pa) {
char per_cell_name[64];
snprintf(per_cell_name, sizeof(per_cell_name), "%s_%d",
s->placeholder_icon, pad);
if (ensure_icon_atlas(ctx, per_cell_name, &pa) != 0 || !pa) {
pa = NULL;
ensure_icon_atlas(ctx, s->placeholder_icon, &pa);
}
if (pa) {
int px = cx + (cw - pa->w) / 2;
int py = cy + (ch - pa->h) / 2;
px &= ~1; py &= ~1;
@@ -1945,7 +1953,7 @@ static const AVOption cuda_grid_options[] = {
OFFSET(aspect_mode), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, FLAGS },
{ "placeholder_timeout_ms", "если input pad не дал new frame N ms — blit placeholder (default 2000, 0=disable)",
OFFSET(placeholder_timeout_ms), AV_OPT_TYPE_INT, { .i64 = 2000 }, 0, 60000, FLAGS },
{ "placeholder_icon", "icon name (через icon_dir) для placeholder при stale input. Default 'offline'",
{ "placeholder_icon", "icon base name (controller рендерит <name>_<pad>.png per-cell с labels; fallback <name>.png)",
OFFSET(placeholder_icon), AV_OPT_TYPE_STRING, { .str = "offline" }, 0, 0, FLAGS },
{ NULL }
};
+70 -2
View File
@@ -20,6 +20,7 @@
#include "libavutil/avstring.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
#include "libavutil/time.h"
#include <cuda_runtime.h>
#include <cuframes/cuframes.h>
@@ -42,6 +43,12 @@ typedef struct CuframesDemuxerContext {
* сохраняем его для первого read_packet */
int pending_first_frame;
cuframes_frame_t *first_frame;
/* Reconnect state — publisher container restart = new IPC namespace,
* old subscriber становится DISCONNECTED. Без reconnect логики input
* pad навсегда EOF → filter показывает placeholder forever даже когда
* publisher восстановился. */
char saved_key[80];
int64_t last_reconnect_us;
} CuframesDemuxerContext;
#define OFFSET(x) offsetof(CuframesDemuxerContext, x)
@@ -133,6 +140,8 @@ static int cuframes_read_header(AVFormatContext *s)
key, cuframes_strerror(rc));
return AVERROR_EXTERNAL;
}
/* Save key для reconnect attempts. */
av_strlcpy(c->saved_key, key, sizeof(c->saved_key));
/* Получаем первый кадр чтобы узнать width/height. */
cuframes_frame_t *frame = NULL;
@@ -175,6 +184,34 @@ static int cuframes_read_header(AVFormatContext *s)
return 0;
}
/* Attempts re-subscribe after publisher disconnect. Rate-limited к 1 try / 2 sec. */
static void try_reconnect(AVFormatContext *s)
{
CuframesDemuxerContext *c = s->priv_data;
int64_t now = av_gettime();
if (now - c->last_reconnect_us < 2000000) return;
c->last_reconnect_us = now;
if (c->sub) {
cuframes_subscriber_destroy(c->sub);
c->sub = NULL;
}
cuframes_subscriber_config_t rcfg = {0};
rcfg.key = c->saved_key;
rcfg.consumer_name = NULL;
rcfg.mode = CUFRAMES_MODE_NEWEST_ONLY;
rcfg.cuda_device = c->cuda_device;
rcfg.connect_timeout_ms = 1000;
int rrc = cuframes_subscriber_create(&rcfg, &c->sub);
if (rrc == CUFRAMES_OK) {
av_log(s, AV_LOG_INFO, "cuframes: reconnected to '%s'\n", c->saved_key);
} else {
c->sub = NULL;
av_log(s, AV_LOG_DEBUG, "cuframes: reconnect '%s' fail: %s\n",
c->saved_key, cuframes_strerror(rrc));
}
}
static int cuframes_read_packet(AVFormatContext *s, AVPacket *pkt)
{
CuframesDemuxerContext *c = s->priv_data;
@@ -185,11 +222,42 @@ static int cuframes_read_packet(AVFormatContext *s, AVPacket *pkt)
c->first_frame = NULL;
c->pending_first_frame = 0;
} else {
/* Guard — subscriber может быть NULL после failed reconnect attempt */
if (!c->sub) {
try_reconnect(s);
return AVERROR(EAGAIN);
}
int rc = cuframes_subscriber_next(c->sub, c->cuda_stream, &frame, 5000);
if (rc == CUFRAMES_ERR_TIMEOUT || rc == CUFRAMES_ERR_WOULD_BLOCK)
return AVERROR(EAGAIN);
if (rc == CUFRAMES_ERR_DISCONNECTED)
return AVERROR_EOF;
if (rc == CUFRAMES_ERR_DISCONNECTED) {
/* Publisher container died / recreated. Try reconnect — rate-limited
* к одному attempt каждые 2 sec чтобы не spam'ить unix socket.
* Возвращаем EAGAIN (ffmpeg retries) вместо EOF (ffmpeg stops). */
int64_t now = av_gettime();
if (now - c->last_reconnect_us > 2000000) {
c->last_reconnect_us = now;
if (c->sub) {
cuframes_subscriber_destroy(c->sub);
c->sub = NULL;
}
cuframes_subscriber_config_t rcfg = {0};
rcfg.key = c->saved_key;
rcfg.consumer_name = NULL;
rcfg.mode = CUFRAMES_MODE_NEWEST_ONLY;
rcfg.cuda_device = c->cuda_device;
rcfg.connect_timeout_ms = 1000;
int rrc = cuframes_subscriber_create(&rcfg, &c->sub);
if (rrc == CUFRAMES_OK) {
av_log(s, AV_LOG_INFO, "cuframes: reconnected to '%s'\n",
c->saved_key);
} else {
av_log(s, AV_LOG_DEBUG, "cuframes: reconnect к '%s' fail: %s\n",
c->saved_key, cuframes_strerror(rrc));
}
}
return AVERROR(EAGAIN);
}
if (rc != CUFRAMES_OK || !frame) {
av_log(s, AV_LOG_ERROR, "cuframes: next: %s\n", cuframes_strerror(rc));
return AVERROR_EXTERNAL;