Files
vf-cuda-grid/controller/cuda_grid_controller/zmq_client.py
T
gx 96e6048b64 controller: Phase 4b end-to-end working — wire format fix + FrigateBridge auto-overlay
Fixed pipeline (live verified с ffmpeg-vf-cuda-grid:phase4b-icon):

zmq_client.send_command — wrap args в single-quotes
  FFmpeg's zmq filter parses через av_get_token (libavfilter/f_zmq.c) — берёт
  ОДИН token как arg. Без quotes filter получает только id, parse fail.

dispatch._serialize_overlay_to_zmq — translation pydantic → filter wire:
  color "#RRGGBB"     → r=N g=N b=N
  opacity 0..1.0      → opacity 0..255
  border_only + width → thickness (0=filled, иначе width)
  dim_factor 0..1.0   → amount 0..255
  text/icon_name      → URL-encoded (%20 для space)

frigate_bridge — Phase 4b auto-rendering:
  motion ON       → add RectOverlay (orange border весь cell)
  motion OFF      → remove
  event new/update → add RectOverlay (bbox) + TextOverlay (label + score%)
  event end       → remove оба
  Bbox px → normalized через configurable camera_width/height (default 1920x1080).
  Опциональные flags motion_indicator/bbox_overlay в mapping.
  Constructor принимает dispatcher для dispatch.handle("overlay.add"/"overlay.remove").

mqtt_loop._handle_message — await frigate_bridge.handle_message (теперь async).
__main__.py — передаёт dispatcher в FrigateBridge constructor.

Verified end-to-end через test pipeline:
  pydantic Overlay → _serialize_overlay_to_zmq → send_command quoted →
  ZMQ → filter parse_overlay_args → CUDA kernel render
4× add_overlay (rect border, text, dim, rect filled) — все 4 visible в output frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:19:47 +01:00

74 lines
2.8 KiB
Python

"""ZMQ клиент к FFmpeg's `zmq` filter.
FFmpeg zmq filter принимает строки формата:
`target command [args]`
Например для cuda_grid в filter graph:
`cuda_grid@livingroom_tv set_layout quad`
См. https://ffmpeg.org/ffmpeg-filters.html#zmq_002c-azmq
"""
from __future__ import annotations
import structlog
import zmq
import zmq.asyncio
log = structlog.get_logger()
class FFmpegZmqClient:
"""REQ-socket к FFmpeg zmq filter. Один client = один FFmpeg pipeline."""
def __init__(self, endpoint: str, request_timeout_ms: int = 2000) -> None:
self.endpoint = endpoint
self.request_timeout_ms = request_timeout_ms
self._ctx = zmq.asyncio.Context.instance()
self._sock: zmq.asyncio.Socket | None = None
async def connect(self) -> None:
if self._sock is not None:
return
self._sock = self._ctx.socket(zmq.REQ)
self._sock.setsockopt(zmq.LINGER, 0)
self._sock.setsockopt(zmq.RCVTIMEO, self.request_timeout_ms)
self._sock.setsockopt(zmq.SNDTIMEO, self.request_timeout_ms)
self._sock.connect(self.endpoint)
log.info("zmq.connected", endpoint=self.endpoint)
async def send_command(self, target: str, command: str, args: str | None = None) -> str:
"""Отправить команду filter'у. Возвращает ответ от ffmpeg ('0 Success' / error string).
FFmpeg's zmq filter parses через `av_get_token` (libavfilter/f_zmq.c) — берёт
ОДИН token как arg. Если arg содержит пробелы (наш case для add_overlay),
нужно обернуть в single-quotes. av_get_token honours quoting + escape '\\''.
"""
if self._sock is None:
await self.connect()
assert self._sock is not None
cmd_str = f"{target} {command}"
if args:
# Escape embedded single-quotes (rare для наших args, но safe).
escaped = args.replace("'", r"\'")
cmd_str = f"{cmd_str} '{escaped}'"
log.debug("zmq.send", endpoint=self.endpoint, cmd=cmd_str)
try:
await self._sock.send_string(cmd_str)
reply = await self._sock.recv_string()
log.debug("zmq.reply", reply=reply)
return reply
except zmq.error.Again:
log.warning("zmq.timeout", endpoint=self.endpoint, cmd=cmd_str)
# Reset REQ socket state — после timeout REQ нельзя re-use
self._sock.close(linger=0)
self._sock = None
raise TimeoutError(f"zmq command timeout: {cmd_str}")
async def close(self) -> None:
if self._sock is not None:
self._sock.close(linger=0)
self._sock = None