controller: persistent ffmpeg snapshot keeper — /snapshot latency 5s → 4ms

SnapshotKeeper class — single long-running ffmpeg subprocess на каждый
instance, читает output_rtsp_url непрерывно и dumps latest frame в файл
(/tmp/snapshot-keeper/<instance>.png) каждые 500 ms через ffmpeg fps=2
-update 1. /snapshot endpoint просто serves файл — disk read ~4 ms (было
~5 sec на cold ffmpeg start + RTSP negotiate + keyframe wait).

Auto-restart с exponential backoff при exit (RTSP source перезапустился,
network glitch). Cold ffmpeg fallback остаётся в endpoint — если keeper
ещё не успел dump первый PNG (первые ~1-2 sec после controller start).

UI: snapshot poll interval 700 → 250 ms (4 fps preview, было ~1.4 fps).
Keeper dump rate сейчас 2 fps — practical limit. При желании поднять
до 4 fps — fps=4 в snapshot_keeper.py (~5% доп CPU на ffmpeg PNG encode).

Применение: основной user-facing path = visual overlay editor http://controller:8083/.
Раньше polling 700 ms показывал тот же frame несколько раз пока ffmpeg
запускался. Сейчас preview почти real-time, drag-and-drop reference точный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gx
2026-05-26 22:16:45 +01:00
parent 0e9a353d75
commit d0e34c9d31
4 changed files with 135 additions and 2 deletions
+14 -1
View File
@@ -41,6 +41,7 @@ class AudioOutputReq(BaseModel):
def create_app(
cfg: Config, state: ControllerState, dispatcher: CommandDispatcher,
snapshot_history: SnapshotHistory | None = None,
snapshot_keeper=None,
frigate_bridge=None,
) -> FastAPI:
app = FastAPI(
@@ -221,11 +222,23 @@ def create_app(
},
)
async def snapshot(instance: str) -> Response:
"""Capture single frame от output_rtsp_url. Returns PNG bytes (image/png)."""
"""Capture single frame от output_rtsp_url. Returns PNG bytes (image/png).
Fast path: persistent ffmpeg-keeper subprocess дампит latest frame в
файл каждые 500 ms — endpoint просто читает (latency ~10 ms). Cold path:
fork ffmpeg на каждый request (~5 sec) — fallback если keeper ещё не
создал файл (первые 1-2 sec после controller start) или у инстанса
нет output_rtsp_url.
"""
inst = _check_instance(instance)
if not inst.output_rtsp_url:
raise HTTPException(400, f"instance '{instance}' has no output_rtsp_url configured")
if snapshot_keeper:
path = snapshot_keeper.path(instance)
if path:
return FileResponse(path, media_type="image/png")
proc = await asyncio.create_subprocess_exec(
"ffmpeg", "-hide_banner", "-loglevel", "error",
"-rtsp_transport", "tcp",