Bug: client_id format "composer-frigate-%d" использовал только now_ms().
Когда создаются 2 subscriber'а подряд (frigate + yoloworld) — оба
получают один client_id за тот же мс, mosquitto kick'ает старый при
connect нового → infinite reconnect loop, ни один не получает events
стабильно.
Fix: static _Atomic int instance_seq counter — tie-break суффикс.
Format: "composer-frigate-%d-%d".
Verify mosquitto logs:
composer-frigate--2117451292-0 (frigate subscriber)
composer-frigate--2117451292-1 (yoloworld subscriber)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Параллельный MQTT subscriber на отдельный topic (default yoloworld/events)
от yolo-world-detector сервиса. Использует те же --detection-cell что
Frigate, но рендерит bbox magenta цветом (Y=105 U=212 V=234) вместо
зелёного (Y=210 U=50 V=100).
На одной картинке композитор может одновременно показать:
- зелёный bbox от Frigate (person/car/...)
- magenta bbox от yolo-world (fox/dog/drone/...)
Backward compat 100% — без --yw-mqtt никаких изменений в поведении.
Архитектурно:
- Второй cfc_frigate_mqtt_t (тот же envelope schema через
cfc_frigate_mqtt parser — yolo-world-detector публикует
Frigate-compat events)
- Параллельный yw_detbox_overlays[] с magenta colors
- yolo-world subscriber не управляет motion-layout (composer=NULL),
только Frigate шлёт motion-pulse'ы
Verify build:
cmake --build build # все 4 target'а (grid_record, simple_record,
# grid_record_cpp, cuframes_composer_static)
Live test пока через docker recreate в Phase 5 deploy, после того
как yolo-world publish'ит в реальный MQTT.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Было: tpl_3 = main + 2 preview + temp_chart (rs=4) + ha_chat.
При 4 active композитор выбирал tpl_3 (waste=1 vs tpl_4 waste=0 — wait,
tpl_3 имел 3 cam cells, при 4 active брал tpl_4=quad без виджетов).
Стало: tpl_3 = main + 3 preview + temp_chart (rs=2, низ-право) + ha_chat.
4 cam cells, при 4 active best-fit выбирает tpl_3 (waste=0, раньше в registry
чем tpl_4 → tie-breaker за tpl_3). Виджеты остаются видимыми.
Geometry validated: 36+4+4+4+4+12=64 микроячеек, no overlaps.
Hot-reloaded via reload_templates ZMQ в проде.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Conversation events HA Assist не приходят (архитектурное ограничение).
Чтобы placeholders ❯ Вы: ... и ✎ HA: ... не висели на экране без
данных — overlay'и убраны до решения вопроса с источником диалога.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Подписка на assist/conversation/text + /response (plain-text retained,
schema см. home-assistant/.../assist_mqtt.md).
Позиция: левый-нижний угол стопкой —
🤖 ответ внизу (margin_y=32)
🗣 вопрос выше (margin_y=78)
Полупрозрачная подложка (bg_alpha=180), placeholder с эмодзи пока
turn'а не было.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User: "при движении объект оборачивается в рамку, но функционал не
учитывает что сетки могут переключаться и координаты ячеек меняются".
Был баг: detbox-overlay хранил cell_x/y/w/h из --detection-cell CLI
(заданы при старте), при смене layout рамки рисовались по старым
координатам — мимо камеры.
Изменения:
- overlay.h/.c: новый API cfc_overlay_detbox_set_cell_geom(ov,x,y,w,h).
Mutex-защищённое обновление detbox config'а — composer вызывает
перед каждым draw.
- CameraCell: добавлено поле source_key (хранит cuframes-key камеры,
рендерящейся в этом cell). Layout::apply передаёт его из pool entry.
- Layout::find_camera_cell_rect(key) — возвращает Rect текущей cell
для камеры с заданным cuframes-key (или nullptr если её нет в layout).
- SourcePool::by_frigate_camera(name) — lookup pool-entry по
Frigate-camera-key (frigate event'ы приходят с этим именем).
- Composer::compose_frame: перед draw каждого DETECTION_BOXES overlay'я
— lookup frigate→cuframes_key→layout cell rect, обновляет detbox geom.
Если камера не в layout сейчас — cell_w/h=0, detbox draw skip'ает.
Теперь bbox от Frigate переезжает за камерой:
- tpl_1 → bbox в full screen 1920×1080
- tpl_3 → bbox в main 1440×810
- tpl_4 → bbox в quad ячейке 960×540
- камера не в layout → bbox скрыт
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User: "у него есть полупрозрачная подложка? у него правильный z-order?"
Z-order был ок (overlays draw'аются после layout.render). Но без bg
текст плохо читался на пёстром кадре — и до прихода MQTT overlay был
visible=0 (не виден вообще).
Изменения:
- overlay.h: cfc_overlay_text_config_t расширена bg_alpha / bg_y/u/v / bg_pad.
bg_alpha=0 — фон отключён (default).
- overlay.c draw_text: если bg_alpha>0, перед blit'ом текста рисуем fill
rect (atlas_w+2*pad) × (atlas_h+2*pad) с заданным цветом и alpha.
- overlay.c update_text: пробрасывает bg-поля при апдейте.
- mqtt_overlay: MqttOverlayCfg + JSON loader научились читать bg_alpha,
bg_y/u/v, bg_pad, placeholder. Default bg = чёрный 160 alpha, pad 10.
- MqttOverlayItem::start: overlay сразу visible=1 с placeholder (default "—"),
reposition_overlay вызывается до получения MQTT — placeholder
позиционируется в anchor сразу.
User'у теперь видна тёмная подложка с текстом в правом-нижнем углу даже
если sensor молчит.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User: "в предыдущих версиях у нас показывалась температура, можешь
сделать в нижнем правом углу оверлей не привязанный к сеткам?". Восстановили
прежнее поведение из vf-cuda-grid/controller (dynamic_overlays.py).
Новый класс cfc::TempMqttOverlay:
- libmosquitto subscriber в отдельном thread'е, auto-reconnect 1→30s
- json-c для parse JSON payload — extract поле .temperature (double)
- Формат: "%+.1f°C" (например "+18.5°C")
- Persistent FreeType text overlay (cfc_overlay_create_text), kept в
Composer::overlays_[] backward-compat листе — рендерится поверх Layout
- reposition_overlay() — пересчёт x/y после text_size() (right-bottom anchor)
C-shim (composer_c_api.cpp) для grid_record.c:
- cfc_temp_overlay_start(composer, host, port, user, pw, topic, W, H)
- singleton (один temp overlay на процесс, прода-композитору хватит)
CLI: --temp-topic="zigbee2mqtt/Температура на улице". MQTT credentials
переиспользуются из --mqtt-host/--mqtt-user/--mqtt-pass.
Compose override (localhost-infra):
--temp-topic=zigbee2mqtt/Температура на улице
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User: "ячейки не имеют границ, сделай их серым цветом". В Layout::apply()
для каждого Cell (CameraCell/BlankCell/WidgetCell) добавлен BorderDecoration:
- thickness 2px
- Y=180, U=128, V=128 (нейтральный серый в BT.709 limited)
- alpha 220 (слегка полупрозрачный — видно контент за рамкой)
Decoration рисуется поверх content (после draw_content) — поверх любого
кадра/widget'а.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User: "смущает чёрная ячейка в сетке". Причина: asymmetric templates
имели widget cells (placeholder тёмно-серый Y=40) + при недостатке active
camera cells оставались BlankCell (чёрный).
Два изменения:
1. templates.json — оставили только 16:9 layouts (tpl_1/tpl_4/tpl_9/tpl_16).
Все camera-cells, никаких widget-областей. Cells full 16:9 (cs==rs
микроячейки), полностью покрывают output 1920×1080 без чёрных полос.
Asymmetric layouts (main + satellites) удалены — вернуть в Phase 12
когда widget'ы будут реальными (HA-chat, temperature graph).
2. composer::maybe_relayout — заполнить свободные camera-cells остальными
drawable камерами из pool (по priority), если template имеет больше
cells чем motion-active. Условие: cap > active.size().
Производство при 4 источниках в pool:
- 1 motion → tpl_1 (1 cell full screen)
- 2 motion → tpl_4 (2 motion + 2 not-active drawable = 4 cells заняты)
- 4 motion → tpl_4 (все 4 motion)
- При добавлении новых камер (до 16) — tpl_9 при 5..9, tpl_16 при 10..16
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В прод деплоен gx/cuframes-composer:0.11b-step1 — C++ ядро
работает через ABI shim, старые C-callers (grid_record.c, control.c,
frigate_mqtt.c) использует те же cfc_composer_* функции.
Что в этом коммите:
- src/cpp/composer_c_api.cpp: extern "C" обёртки над cfc::Composer
методами. Полный набор: _create/_destroy/_compose/_add_overlay/
_find_overlay/_set_layout/_current_layout/_add_pool_source/
_set_motion_mode/_get_motion_mode/_motion_pulse/_get_health.
- src/cpp/layouts_c_api.cpp: extern "C" обёртки над template_loader
для cfc_layout_find/_all/_load_file/_reload/_loaded_path/_to_pixels.
- cpp/template_loader: global registry (current_templates / set_*
/ load_into_current) — единый источник истины. Composer и C ABI
shim читают один и тот же mutex-защищённый vector<LayoutTemplate>.
Hot-reload через ZMQ cfc_layout_load_file подхватывается composer'ом
на следующем кадре без рестарта.
- cpp/composer: pick_best_fit, set_layout, maybe_relayout читают
current_templates() вместо локального snapshot.
- cpp/composer: backward-compat overlay list (add_overlay/find_overlay)
+ manual cells support (для C API без motion-mode).
- cpp/composer compose_frame: после Layout.render() рендерит overlays
(CLI text/icon/border + Frigate detbox) поверх.
- Удалены: src/composer.c (заменён composer_c_api.cpp + composer.cpp),
src/layouts.c (заменён layouts_c_api.cpp + template_loader.cpp).
- Оставлено как есть: src/overlay.c (PNG/text/border/detbox CLI overlays
— реализация не меняется, доступ через cfc_overlay_*).
- src/CMakeLists.txt: COMPOSER_SOURCES_C минус composer.c, layouts.c,
COMPOSER_SOURCES_CPP плюс composer_c_api.cpp, layouts_c_api.cpp.
Production smoke (R9-88.23):
[cfc/loader] /opt/templates.json: loaded 7 templates
[cfc/composer] templates loaded: 7 (path='/opt/templates.json')
[cfc/composer] pool+ cam-parking prio=100 / cam-gate_lpr prio=90 / ...
[cfc/composer] motion_mode=1 ttl=45000ms pool=4
[cfc/composer] grow → template='tpl_1' active=1
PASS.
Refs: #195 (Phase 11b C++ refactor).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Что в этом коммите:
Decoration реализации:
- cpp/label_decoration.hpp/.cpp — FreeType atlas + cugrid_blit_rgba_nv12.
UTF-8 декодер, atlas в VRAM (RAII через CudaBuffer), rebuild при set_text.
- cpp/border_decoration.hpp/.cpp — 4 cugrid_fill_nv12 (top/bottom/left/right).
Cell реализации:
- cpp/camera_cell.hpp/.cpp — cfc_source_get_latest + cugrid_resize_nv12.
Non-owning указатель на cfc_source_t (pool владеет).
- cpp/widget_cell.hpp/.cpp — тёмный fill placeholder.
- cpp/blank_cell.hpp/.cpp — BT.709 black fill.
Layout и Template:
- cpp/template.hpp — LayoutTemplate { name, cells[], priority }.
8×8 микро-сетка (kGridCols=kGridRows=8). to_pixels() переводит в Rect.
- cpp/layout.hpp/.cpp — vector<unique_ptr<Cell>>, apply() создаёт
CameraCell/WidgetCell/BlankCell + Decorations (Label с "{key} prio={N}").
- cpp/template_loader.hpp/.cpp — JSON → vector<LayoutTemplate> через json-c.
builtin_templates() = { tpl_1, tpl_4 } как fallback.
SourcePool:
- cpp/source_pool.hpp/.cpp — owner cfc_source_t*, motion state атомарный,
zone-filter в motion_pulse. Pool entries — non-copyable unique_ptr.
Composer:
- cpp/composer.hpp/.cpp — owner SourcePool + templates + Layout + output.
Алгоритмы: pick_best_fit (min nb_camera_cells >= need + priority tie-break),
collect_active (drawable AND motion_within_TTL), asymmetric hysteresis
(рост сразу через std::includes, сжатие — wait shrink_hysteresis_ms).
Public C++ API: set_motion_mode / set_layout / load_templates / compose_frame.
ООП-гипотеза smoke:
- examples/grid_record_cpp.cpp — минимальный smoke без NVENC. Init composer,
compose_frame N раз, dump NV12 в файл. Проверяет что C++ модель
компилируется, линкуется с C-кодом (source.c, nvenc.c остались на C через
extern "C"), и реально рисует кадр.
Производительность сохранена:
- Один output буфер VMM, передаётся как NV12Ref (read-write reference) во все
cells/decorations — НИКАКИХ memcpy на cells boundary.
- Virtual call overhead: 1 indirect call per cell per frame. Negligible.
- Heap allocations только при apply_template (раз в N секунд при relayout).
Build:
- CMakeLists.txt: CXX language, C++17.
- src/CMakeLists.txt: COMPOSER_SOURCES_CPP добавлен в lib.
- examples/CMakeLists.txt: grid_record_cpp.
Smoke test run jammy:
[cfc/loader] docker/templates.json: loaded 7 templates
[smoke] composer 1920x1080 templates=7 sources=0 motion=0
[smoke] wrote 3317760 bytes (Y=2211840 UV=1105920) to /out/blank.nv12
Build PASS, init PASS, compose PASS, dump PASS.
Что НЕ сделано:
- extern "C" ABI shim для control.c / grid_record.c (старый C-композитор
всё ещё единственный для prod stack).
- Удаление старых composer.c / overlay.c / layouts.c.
- Live deploy в прод (Step 1-3 функциональность).
- JSON ZMQ hot-reload (был в Step 3 C-version, восстановить в C++).
Refs: #195 (Phase 11b C++ refactor).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Объединённое состояние работ:
- Phase 10: source pool, motion-driven layout, Frigate motion_pulse,
zone-filter в pool, --source / --motion-mode CLI
- Phase 11 Steps 1-3c: 8×8 микро-сетка, JSON templates с asymmetric
layouts (tpl_1/3/4/5/6/7/8), reload через ZMQ, auto-labels per camera
В прод отдеплоено и откачено: используем gx/cuframes-composer:0.10 как
baseline. Phase 11 продолжится в branch phase11b-cpp как C++ refactor
с ООП-моделью Cell/Layout/Decoration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 predefined layouts (single, dual_horizontal, dual_vertical, quad,
main_plus_preview, six_grid, nine_grid, sixteen_grid, panoramic,
main_with_strip) — normalized [0..1] координаты, port из vf_cuda_grid.c
старого FFmpeg patch'а. Применяются к фактическому output разрешению
композитора через cfc_composer_set_layout(name).
Source pool НЕ пересоздаётся: sources привязаны к индексам cells, layout
меняет только геометрию (cell.x/y/w/h). Это даёт zero-disruption switch
без потери накопленного state и без re-subscribe к cuframes publishers.
ZMQ verbs: set_layout / list_layouts / get_layout. CLI: --layout=NAME
перетирает --cell координаты на старте.
Используется ONVIF wrapper'ом (gx/cctv-onvif:0.1) для PTZ presets:
GotoPreset(token) → ZMQ set_layout(token).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frigate 0.17 schema отвергает objects.filters.<obj>.required_zones
("Extra inputs not permitted" → safe mode), поэтому фильтрацию по зонам
делаем на стороне composer'а. cfc_overlay_detbox_match_zones() сверяет
after.current_zones события с whitelist'ом overlay'я. end-события
вызываются безусловно (no-op если рамки нет).
CLI: --detection-cell key,camera,dx,dy,dw,dh,detect_w,detect_h[,zone1:zone2:...]
9-е поле опционально (colon-separated). Без него filter выключен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Robust pattern из docs/RESEARCH-frigate-overlay-integration.md:
композитор сам подписывается на frigate/events, парсит JSON, рисует
bounding rectangles в управляемый overlay-список с TTL. Никаких
sidecar'ов — root cause "перестало обновляться" в старом
cuda-grid-controller'е был именно two-hop ZMQ + state leak.
Содержимое:
- include/cuframes_composer/overlay.h — новый тип CFC_OVERLAY_DETECTION_BOXES
+ cfc_overlay_detbox_config_t (camera_key, detect_w/h, cell rect,
thickness, color, TTL stale_ms) + API _create / _upsert / _end /
_camera_key.
- src/overlay.c — реализация: массив 16 active детектов под mutex,
draw как 4 fill_nv12 на каждый valid box (TTL фильтрация по now-update).
Coordinate mapping detect→cell в момент draw — layout switch безопасен.
- include/cuframes_composer/frigate_mqtt.h + src/frigate_mqtt.c — MQTT
subscriber на libmosquitto. Parses Frigate event JSON через json-c
(after.camera, after.id, after.box, after.label). Lookup overlay по
camera_key. Mosquitto reconnect_delay_set 1s→30s exp backoff,
disconnect log rate-limited (раз в 30 сек).
- examples/grid_record.c — CLI флаги --frigate-mqtt host[:port],
--frigate-topic frigate/events, --detection-cell key,camera,dx,dy,
dw,dh,detect_w,detect_h. По одному --detection-cell создаётся overlay
+ регистрируется в MQTT subscriber.
- src/CMakeLists.txt — добавлен frigate_mqtt.c.
Параллельная подготовка на Frigate стороне:
- Frigate config /home/claude/cctv/frigate-config/config.yml — добавлен
parking_overview.objects.filters.{car,person,bicycle,motorcycle}.
required_zones=[canopy,parking_zone,private_area]. Это фильтрация
на tracker уровне (review.required_zones влиял только на Frigate
Review system). После: ~2 события/сутки реально с parking, вместо
1000+ от проезжей части.
Live-test:
- Synthetic mosquitto_pub frigate/events с box [200,200,400,360]
в detect 640×480 → cfc-grid render'ит зелёную рамку в parking cell
правильной геометрии (300,225, 300×180 в 1920×1080 output).
- TTL 8 сек — рамка пропадает если update не пришёл.
- restarts=0, 12650 кадров stable 25 fps — frigate_mqtt thread
не блокирует video.
Image gx/cuframes-composer:0.7 deployed.
Чеклист защиты от "перестало обновляться" (из research §6.4):
✅ MQTT auto-reconnect через mosquitto built-in backoff
✅ TTL независим от end event (страховка от потери MQTT)
✅ State полностью в composer-процессе (один источник истины)
✅ Mapping coords в момент draw (layout switch safe)
✅ Frigate-фильтр на источнике (objects.filters.required_zones)
⏳ Health-метрики (frigate_mqtt_connected, events_received) — TODO
Несколько сессий попыток реализовать audio mixing в композитор'е.
Не достигнуто sub-секундной latency со стабильным video+audio.
Откатано на parallel mode (cfc-grid video-only, live от pipeline с audio).
Полный набор выводов и pitfall'ов — docs/LESSONS-audio-mixing-attempts.md.
Главные lesson'ы для будущей попытки:
- mpegts mux libavformat авто-инсёртит h264_mp4toannexb BSF которому
не нравится Annex-B + inline SPS/PPS — NVENC OUTPUT_SPSPPS per-frame ломает
- SPSC ring drop newest при full, не oldest (consumer's domain)
- av_new_packet (не av_malloc) для av_interleaved_write_frame ownership
- Monotonic PTS на counter (frame_idx, total_samples) — не wallclock
- mediamtx env-var path names не должны иметь '-' (parser limitation)
- Default mediamtx ReadTimeout=10s короткий для burst write'ов
Изменения в repo сохранены для будущей доработки:
- src/writer.c — mpegts backend с audio stream support
- src/audio.c — RTSP AAC consumer + lock-free SPSC ring
- include/cuframes_composer/{writer,audio}.h — public API
- examples/grid_record.c — --format=mpegts + --audio-source flags
- include/cuframes_composer/composer.h — consumer_prefix field
- docker/Dockerfile — libavformat-dev добавлен в builder/runtime
cfc-grid composer стабильно работает на видео (substantially лучше
монолитного pipeline'а с audio bag'ом). TV рекомендуется использовать
rtsp://...:554/cfc-grid + опционально rtsp://...:554/live-audio
parallel.
netcat-openbsd добавлен в runtime layer (~50KB). HEALTHCHECK probe'ит
listening на 5599 (ZMQ control). Full functional ping через nc не сработает
(ZMTP требует binary greeting handshake), но liveness через TCP-проверку
достаточен для docker-compose restart-on-unhealthy логики.
Functional проверки делегированы external monitors:
- MQTT health uptime_s растёт = composer не зависает
- HA discovery sensor.composer_cfc-grid_active = camera-feed health
- Phase 7 (Prometheus) добавит composition_time_us / fps metrics
Live-validated: cfc-grid container статус 'healthy' через 25с после
recreate, healthcheck Log shows exit 0.
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.
Результат:
46+ секунд работы, 1150 кадров, 0 drops
FPS = 25.0 СТАБИЛЬНО
GPU util = 47% (большие запасы для масштабирования)
16 active, 0 stale, 0 dead — все подписки удерживаются
ffprobe видит валидный h264 3840x2160 25/1 на mediamtx
Visual artifact в rows 2-4 (вертикальные полосы) — гипотеза:
race condition при 4 cfc_source_t подписках на одного publisher'а
(16 consumers vs 16-slot cuframes ring buffer = tight fit).
FPS не пострадало, но визуально половина ячеек испорчена.
Phase 5e-fix planned: composer должен detect одинаковые source_key
и переиспользовать один cfc_source_t для N ячеек. Это устранит
multi-consumer race и снизит memory footprint.
Подробности + снимок в docs/LOADTEST-PHASE-5e.md +
docs/phase5e-load16.jpg.
Composer настроен на 4 несуществующих cuframes-ключа. Live результат:
health: {total:4, active:0, stale:0, dead:4}
FPS: 25.2 СТАБИЛЬНО
Encoder рендерит чёрный grid с красными NO_SIGNAL подписями
+ серо-голубыми border'ами
ffprobe видит валидный h264 1920x1080 25/1 на mediamtx
Это самый жёсткий adversarial scenario — полная потеря сети камер.
Composer не падает, encoder работает на холостом ходу, RTSP-publish
непрерывен, MQTT health корректно отдаёт dead=4 (HA-алерты через
expire_after=30s).
Доказан главный production threat: обесточивание PoE-свитча /
uplink-отрезание не валит весь pipeline. TV видит NO_SIGNAL вместо
разрыва соединения, HA-оператор уведомляется.
Полный отчёт + снимок в docs/ADVERSARIAL-TEST-PHASE-5b.md +
phase5b-all-dead.jpg.
docker/Dockerfile — multi-stage build:
builder: nvidia/cuda:12.6.0-devel-ubuntu24.04 + apt build-deps + cmake
runtime: nvidia/cuda:12.6.0-runtime-ubuntu24.04 + apt runtime-deps
+ grid_record/simple_record + libcuframes.so.0 + libcuframes_composer.so.0
NVIDIA_DRIVER_CAPABILITIES=compute,utility,video в env image'а — обязательно
для NVENC dlopen libnvidia-encode.so.1.
docker/docker-compose.example.yml — образец для интеграции в
localhost-infra/hosts/R9-88.23/docker/cctv/. Два сервиса:
cfc-grid — композитор + control plane + MQTT health
cfc-grid-ffmpeg — pipe→RTSP push (использует существующий
ffmpeg-vf-cuda-grid image для совместимости glibc)
Сообщение в docs/docker/docker-compose.example.yml объясняет зависимости
(cuframes-ipc-anchor, cctv-mosquitto и т.д.) и нужный MQTT user.
Phase 6 deploy (intent перевести production):
- Создать MQTT user composer + добавить в localhost-infra mosquitto passwd
- Запустить cfc-grid параллельно с cuda-grid-pipeline (разные RTSP paths,
не конфликтуют)
- После проверки на проде — retire старый cuda-grid-pipeline
- Обновить TV-плейлисты на новый rtsp://...:554/cfc-grid
Эти шаги требуют отдельных commit'ов в localhost-infra repo с явным
user approval, поэтому здесь только prep.
Live-validated сценарий: stop cuframes-pub-back_yard, наблюдение
через ZMQ /health.
Timeline:
t=0s: 4 active, 0 stale, 0 dead baseline
t+2s: 3 active, 1 stale STALE через 500мс threshold
t+10s: 3 active, 1 dead DEAD через 5000мс threshold
FPS = 25.0 СТАБИЛЬНО
ячейка → blackout
restart pub:
t+~13s: 4 active, 0 stale, 0 dead auto-reconnect отработал
Главный threat model подтверждён: отключение источника не валит
композитор, остальные 3 ячейки продолжают работу, RTSP не прерывается.
Полный отчёт в docs/ADVERSARIAL-TEST-PHASE-5a.md.
Phase 5b/5c (multi-source disconnect, mediamtx restart, 24h memory
test, 16-source load test) — отдельные сессии.
Phase 4a — control plane через ZMQ REP socket с JSON-командами.
Позволяет операторам менять text overlay'и runtime'ом без рестарта.
Live-validated: команды set_text label-parking → "⚠ ВНИМАНИЕ ⚠"
и set_text label-gate → "Машина у ворот" отрабатывают, текст
обновляется в потоке RTSP без перерывов.
Phase 4b — MQTT health publisher через libmosquitto. Каждые 10с в
composer/<instance>/health публикуется JSON {active,stale,dead,total,
uptime_s} с retain=true. Опционально публикуется HA MQTT discovery
config — четыре сенсора (active/stale/dead/total) появляются в HA
автоматически с expire_after=30s.
Содержимое:
- include/cuframes_composer/overlay.h — cfc_overlay_set_id/get_id/get_type
для lookup'а через control plane.
- include/cuframes_composer/composer.h — cfc_composer_find_overlay(id).
- include/cuframes_composer/control.h — cfc_control_config_t (endpoint +
composer + cuda_ctx для text rebuild в worker thread).
- src/control.c — ZMQ REP socket в фоновом потоке + zmq_poll с 200мс
timeout'ом для проверки stop_flag. JSON-диспатчер для команд ping /
health / set_text / set_visible / list_overlays. cuCtxSetCurrent
на старте worker'а — без этого update_text валится на cuMemAlloc.
- include/cuframes_composer/health.h — cfc_health_config_t (host/port/
user/pass + topic_prefix/instance + interval + publish_discovery).
- src/health.c — mosquitto_connect_async + loop_start + background
thread с publish health каждые N секунд + один разовый publish_discovery
для HA.
- examples/grid_record — флаги --control tcp://0.0.0.0:5599,
--mqtt host[:port], --mqtt-instance NAME, --mqtt-user/--mqtt-pass.
Для text overlay'ев — prefix "id=NAME:" в --text задаёт control-plane ID.
CMakeLists.txt — find_library(zmq), find_library(json-c),
find_library(mosquitto) + linkage всех трёх.
Production TODO: создать отдельного MQTT user'а для composer'а вместо
переиспользования frigate creds (используется только в smoke).
Phase 3c — динамический текст через libfreetype6: открывается font
(.ttf/.otf), измеряется bounding box строки, рендерится в RGBA-атлас
с anti-aliased alpha из FT bitmap, заливается в VRAM. На каждом
кадре блитится через cfc_cugrid_blit_rgba_nv12 (kernels уже есть).
Поддержка UTF-8 через простой inline-decoder (1/2/3/4-byte).
cfc_overlay_update_text() переподдерживает re-render atlas (text /
color change) — для Phase 4 ZMQ control plane (динамическое изменение
NO SIGNAL / RECORDING / timestamp'ов).
Адаптация под GTX 1050 (Pascal) в проде:
- CMAKE_CUDA_ARCHITECTURES = 61;75;86;89;120 (Pascal + Turing +
Ampere + Ada + Blackwell, покрывает все production-кейсы)
- grid_record default снижен с 4K@10Mbps на 1080p@4Mbps. 4K требует
явных --width 3840 --height 2160 (Pascal NVENC 4K H.264 на грани).
- Ссылка на research-документ + NVENC §Input Buffers рядом со staging
buffer'ом в nvenc.c с цитатой "the client is required to use buffers
allocated using the cuMemAlloc family of APIs".
Содержимое Phase 3c:
- overlay.h — cfc_overlay_create_text + update_text + text_size.
- overlay.c — utf8_next decoder, text_measure (ascender/descender),
text_render (alpha-over blend в RGBA), text_rebuild_atlas (VRAM
cycle), full FreeType lifecycle.
- CMakeLists.txt — find_package(Freetype REQUIRED) + link
Freetype::Freetype.
- examples/grid_record — флаг --text font,size,r,g,b,x,y,text.
Live-validated на rtsp://192.168.88.23:554/cfc-grid с иконками
temp_outside.png и offline.png из cuda_grid_icons volume.
Содержимое:
- cugrid.h/cugrid.cu — cfc_cugrid_blit_rgba_nv12 (Y+UV α-blend) +
два новых kernel'я blit_rgba_y/uv (BT.709 limited-range RGB→YUV
conversion, 4:2:0 chroma 2x2 averaging).
- overlay.h — cfc_overlay_create_png + cfc_overlay_png_size +
cfc_overlay_update_png.
- overlay.c — libpng decode (paletted/gray/16-bit → 8-bit RGBA),
cuMemAlloc atlas в VRAM, cuMemcpyHtoD один раз, draw_png через
cfc_cugrid_blit_rgba_nv12.
- CMakeLists.txt — find_package(PNG REQUIRED) + PNG::PNG в link.
- examples/grid_record — флаг --icon path,x,y[,alpha] (несколько раз
можно). Атлас грузится один раз при старте.
PNG из cuda_grid_icons volume переиспользуются (offline, temp_outside,
grafana_gpu, и пр.). PNG decode'ятся одинаково — paletted, RGBA, gray.
Phase 3c (text rendering через FreeType + font atlas) — отдельный
commit. Атлас text'а тоже окажется в VRAM как RGBA через тот же
cfc_cugrid_blit_rgba_nv12 — kernels уже готовы.
Live-validated на rtsp://192.168.88.23:554/cfc-grid: цветные рамки
вокруг каждой ячейки реализованы через 4 cfc_cugrid_fill_nv12
(top/bottom/left/right) — отдельного kernel'я не понадобилось,
переиспользуем existing fill_nv12 для region α-blend.
Содержимое:
- include/cuframes_composer/overlay.h — opaque cfc_overlay_t + типы
(BORDER реализован, PNG/TEXT skeleton'ы для Phase 3b/3c).
- src/overlay.c — реализация BORDER: clamp rect в границы кадра,
выравнивание координат на чётные (4:2:0 chroma требование),
thickness clamp если 2*thickness > w/h.
- composer.c — список overlays (max 64), z-order = порядок добавления,
draw поверх grid'а перед stream sync.
- examples/grid_record — флаг --border N (толщина пикселей) добавляет
серо-голубой border (Y=180,U=120,V=110 alpha=220) для каждой
ячейки автоматически.
Phase 3b (PNG icons через stb_image + cugrid alpha_blit_rgba) и Phase
3c (text через FreeType + font atlas) — отдельные commit'ы.
Подтверждено live-тестом end-to-end в продакшен-условиях:
grid_record --out - → ffmpeg -i pipe:0 -c copy -f rtsp -rtsp_transport tcp →
mediamtx rtsp://...:554/cfc-grid → VLC на MacBook
VLC принимает поток штатно, картинка не тормозит — лучше чем у
предыдущего цельного ffmpeg-pipeline (vf-cuda-grid). 1920x1080
H.264 High@4.0 4Mbps, 4 источника active.
Изменения тривиальные: stdout не закрывается через fclose, чтобы
пайп оставался открытым для дочернего ffmpeg-процесса.
Multi-source композитор работает на 4K @ 25fps стабильно. Live-тест с
4 камерами (parking, back_yard, front_yard, gate_lpr): все 4 active,
350 кадров за 14с, 27.6 МБ H.264 файл, кадр декодируется ffmpeg'ом
с корректным 2x2 layout'ом.
Содержимое:
- include/cuframes_composer/cugrid.h — публичный API libcugrid:
cfc_cugrid_fill_nv12 (region fill с alpha blend),
cfc_cugrid_resize_nv12 (bilinear scale в rect).
- src/cugrid/cugrid.cu — извлечённые из vf_cuda_grid kernel'ы
(Y+UV fill + bilinear resize), объединены с C launcher'ом в одном
.cu файле, под LGPL-2.1+.
- include/cuframes_composer/composer.h — публичный API композитора:
cfc_composer_cell_t для layout, get_health для observability.
- src/composer.c — manager N cfc_source_t + единый NV12 output buffer
(cuMemAlloc, переиспользуется на каждом compose'е). compose_clear
fillит фон BT.709-чёрным, compose_cell делает resize ACTIVE
источника или оставляет blackout для DEAD/STALE/CONNECTING.
- examples/grid_record — Phase 2 smoke test: N --cell ключ,x,y,w,h
→ grid composer → NVENC → file.
Сборка: добавлен LANGUAGES CUDA и CMAKE_CUDA_ARCHITECTURES 89;120
(Ada + Blackwell). Compile options раздельные для C и CUDA
(-Wpedantic не подходит для .cu).
Phase 2 RTSP push отложен на отдельный commit — будет через pipe-out
к локальному ffmpeg'у, который публикует в mediamtx (вариант
утверждён в Q2 дизайн-документа).
После live-теста на R9-88.23 с реальным publisher'ом cam-parking
(Dahua 1920x1080 @ 25fps) выявлены и поправлены два блокера:
1) source.c: release_current_frame ПЕРЕД cuframes_subscriber_next.
cuframes валидирует frame_busy в начале next (consumer.c:334)
и возвращает CUFRAMES_ERR_INVALID_ARG если предыдущий frame
не release'нут. Прошлая реализация делала release ПОСЛЕ next —
получалось много invalid_arg и каждые ~1с поток re-subscribe'ился,
throughput падал до 0.87 fps. После фикса — стабильные 25.1 fps.
2) nvenc.c: добавлен staging buffer cuMemAlloc + cuMemcpy2D в encode_frame.
NVENC nvEncRegisterResource возвращает RESOURCE_REGISTER_FAILED для
VMM-mapped указателей cuframes v0.4 (CUdeviceptr из cuMemMap).
Это известное ограничение NVENC SDK для VMM-памяти. Pre-copy в
собственный cuMemAlloc-буфер решает проблему и сохраняет всё в VRAM
(cudaMemcpyDeviceToDevice, без CPU bounce). Phase 1.1 будет research
как получить настоящий zero-copy (cuMemExportToShareableHandle?).
Smoke test результаты (cuframes-dev image + NVIDIA_DRIVER_CAPABILITIES
video, /run/cuframes mount + cuframes-ipc-anchor IPC namespace):
300 кадров, 12 IDR, 6.9 МБ за 11.9с (25.1 fps)
достигнут лимит 15с
flush encoder
ffprobe: 377 кадров, 1920x1080 yuv420p H.264 High@4.0
ffmpeg decode → PNG/JPG → реальное изображение с камеры
Скелет проекта cuframes-composer (LGPL-2.1+) и MVP кодирования
одного источника в файл H.264.
Что включает Phase 1:
- LICENSE (LGPL-2.1+), README с поэтапным планом, корневой CMake
- Подмодули: cuframes v0.4 (pinned), nv-codec-headers (n12.2.72.0)
- include/cuframes_composer/source.h — публичный API источника
с явной машиной состояний (DISCONNECTED → CONNECTING → ACTIVE →
STALE → DEAD) и snapshot-паттерном для чтения без блокировки
- include/cuframes_composer/nvenc.h — публичный API кодировщика
на CUdeviceptr-вход (zero-copy через VMM-mapped указатели)
- src/nvenc_loader.{h,c} — dlopen libnvidia-encode.so.1 и инициализация
таблицы функций NVENC через NvEncodeAPICreateInstance. Идёт через
pthread_once. Сделано отдельно чтобы держать LGPL-совместимость:
проприетарный SDK не статически линкуется
- src/nvenc.c — обвязка над NVENC: open session, init encoder, кеш
registered resources, encode/lock/unlock, flush с EOS, поддержка
H.264 CBR low-latency, preset GUID p1/p4/p7
- src/source.c — обвязка над cuframes_subscriber c фоновым потоком,
exponential backoff reconnect (1с → 30с), и переходами по таймаутам
для stale/dead-детекта
- examples/simple_record — smoke-test программа: подписка на cuframes,
кодирование, запись в .h264 файл, корректное завершение по SIGINT