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:
@@ -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 }
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user