docs: руководства пользователя / разработчика / operations (RU+EN)
Полный комплект документации к Phase 11b:
docs/ru/user.md — для админа инсталляции (motion-mode, PTZ,
templates.json, mqtt_overlays.json, ZMQ verbs)
docs/ru/developer.md — архитектура (Cell / Layout / Decoration),
как добавить новый Cell/Decoration, ABI shim,
algorithms (best-fit + asymmetric hysteresis)
docs/ru/operations.md — build (host + jammy + incremental bake),
deploy, logs/telemetry, troubleshooting
(broken pipe, MQTT-overlay, motion-mode)
docs/en/*.md — английская версия всех трёх
README.md — переписан с overview + ссылками на docs/
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,55 +1,105 @@
|
||||
# cuframes-composer
|
||||
|
||||
Стандалонный композитор-демон для multi-source видео grid через CUDA + NVENC + RTSP.
|
||||
CUDA-композитор multi-source видео в один H.264 RTSP-поток с
|
||||
авто-раскладкой по motion и runtime-управлением через ONVIF/ZMQ.
|
||||
|
||||
Заменяет монолитный ffmpeg-конвейер (`ffmpeg + vf_cuda_grid` фильтр) для случаев, когда нужно:
|
||||
Заменяет монолитный ffmpeg-конвейер (`ffmpeg + vf_cuda_grid` фильтр) для
|
||||
случаев, когда нужно:
|
||||
|
||||
- Поток продолжает работать при потере любого числа источников (graceful degradation)
|
||||
- Композитор сам управляет частотой кадров и обработкой ошибок без зависимости от семантики ffmpeg-демухера
|
||||
- Минимум перемещений данных: zero-copy CUDA от источника `cuframes` напрямую в NVENC
|
||||
- Поток продолжает работать при потере любого числа источников
|
||||
(graceful degradation, blank cells вместо crash'а)
|
||||
- Композитор сам управляет частотой кадров без зависимости от ffmpeg-демухера
|
||||
- Минимум перемещений данных: zero-copy CUDA от cuframes-publisher до NVENC
|
||||
- Auto-layout по движению (Frigate-driven), без оператора
|
||||
- Управление с TV через ONVIF PTZ
|
||||
|
||||
## Что внутри
|
||||
|
||||
- **CUDA-композитор** (C++ ООП-ядро + extern "C" ABI, Phase 11b)
|
||||
- **Auto-layout** с asymmetric hysteresis + best-fit selection
|
||||
- **PTZ-override** через ONVIF (с auto-возвратом в motion-mode)
|
||||
- **MQTT-driven text overlays** (температура, статусы, etc.)
|
||||
- **Detection box overlay** от Frigate, следует за камерой при смене layout
|
||||
- **ZMQ control plane** для runtime-управления (set_layout, set_text, ...)
|
||||
- **NVENC** через `dlopen` (LGPL-совместимая интеграция)
|
||||
|
||||
## Документация
|
||||
|
||||
| | Русский | English |
|
||||
|---|---|---|
|
||||
| Для пользователя | [docs/ru/user.md](docs/ru/user.md) | [docs/en/user.md](docs/en/user.md) |
|
||||
| Для разработчика | [docs/ru/developer.md](docs/ru/developer.md) | [docs/en/developer.md](docs/en/developer.md) |
|
||||
| Operations / deploy | [docs/ru/operations.md](docs/ru/operations.md) | [docs/en/operations.md](docs/en/operations.md) |
|
||||
|
||||
## Статус
|
||||
|
||||
**Phase 1 — MVP.** В разработке. Не для боевой эксплуатации.
|
||||
**Phase 11b — production.** Развёрнут на R9-88.23 в составе CCTV-стека.
|
||||
|
||||
См. [дизайн-документ](https://git.goldix.org/gx/cuframes/raw/branch/main/docs/DESIGN-composer-daemon.md) для архитектурных решений и поэтапного плана.
|
||||
См. [STATE.md](../../localhost-infra/STATE.md) для текущего состояния
|
||||
infra и git-history `main` для эволюции по фазам.
|
||||
|
||||
## Зависимости
|
||||
|
||||
- [cuframes](https://git.goldix.org/gx/cuframes) — библиотека zero-copy передачи кадров. Подключена как git submodule.
|
||||
- [nv-codec-headers](https://github.com/FFmpeg/nv-codec-headers) — MIT-licensed заголовки NVENC API. Подключена как git submodule. Сама библиотека `libnvidia-encode.so` грузится через `dlopen` при старте (это даёт LGPL-совместимость — см. дизайн-документ часть 1.6).
|
||||
- CUDA Toolkit 12.x+ (для cuda runtime и компиляции)
|
||||
- NVIDIA драйвер 525+ (для NVENC и `cuMemCreate` POSIX FD)
|
||||
- Linux 64-bit (POSIX shm, SCM_RIGHTS)
|
||||
- [cuframes](https://git.goldix.org/gx/cuframes) — zero-copy frame
|
||||
delivery от RTSP-publisher'а к composer'у. Подключена как git submodule.
|
||||
- [nv-codec-headers](https://github.com/FFmpeg/nv-codec-headers) —
|
||||
MIT-licensed заголовки NVENC API (submodule). `libnvidia-encode.so`
|
||||
грузится через `dlopen` в runtime для LGPL-совместимости.
|
||||
- CUDA Toolkit 12.x+ (cudart, nvcc, driver API)
|
||||
- NVIDIA driver 525+ (NVENC, cuMemCreate POSIX FD)
|
||||
- FreeType, libpng, libzmq, libjson-c, libmosquitto, libavformat/avcodec/avutil
|
||||
- Linux 64-bit (POSIX shm, SCM_RIGHTS, named pipes)
|
||||
|
||||
Дополнительно по фазам:
|
||||
- Phase 3: `libfreetype` (текст), `lodepng` через submodule (PNG-декодирование)
|
||||
- Phase 4: `libzmq` (управление)
|
||||
|
||||
## Сборка
|
||||
## Quick start (host build)
|
||||
|
||||
```bash
|
||||
git clone --recursive git@git.goldix.org:gx/cuframes-composer.git
|
||||
cd cuframes-composer
|
||||
cmake -B build -G Ninja
|
||||
ninja -C build
|
||||
git submodule update --init --recursive
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
## Поэтапный план
|
||||
Артефакты:
|
||||
- `build/src/libcuframes_composer.so` — shared library
|
||||
- `build/examples/grid_record` — main CLI entry (C)
|
||||
- `build/examples/grid_record_cpp` — C++ smoke test
|
||||
|
||||
| фаза | срок | результат |
|
||||
|---|---|---|
|
||||
| 1 | 1 неделя | один источник → NVENC → файл .h264 (доказательство zero-copy) |
|
||||
| 2 | 2 недели | четыре источника + композиция через `libcugrid` |
|
||||
| 3 | 2 недели | оверлеи + RTSP push к mediamtx + AAC passthrough из `/live-audio` |
|
||||
| 4 | 1 неделя | паритет ZMQ-управления с фильтром `vf_cuda_grid` |
|
||||
| 5 | 1 неделя | боевое развёртывание + MQTT health + watchdog |
|
||||
| 6 | 2 недели | тесты + бенчмарки + документация |
|
||||
Запуск (1 камера, motion-mode, JSON-templates):
|
||||
```bash
|
||||
build/examples/grid_record \
|
||||
--out=/tmp/grid.h264 --fps=25 --bitrate=6000 \
|
||||
--width=1920 --height=1080 \
|
||||
--source=cam-parking,frigate=parking_overview,priority=100 \
|
||||
--motion-mode --motion-ttl=45000 \
|
||||
--templates=docker/templates.json \
|
||||
--mqtt-overlays=docker/mqtt_overlays.json
|
||||
```
|
||||
|
||||
Итого ~9 недель для одного разработчика.
|
||||
Production deploy и jammy-build — см.
|
||||
[docs/ru/operations.md](docs/ru/operations.md).
|
||||
|
||||
## Архитектура одной картинкой
|
||||
|
||||
```
|
||||
Frigate ──MQTT events──→ frigate_mqtt subscriber
|
||||
│
|
||||
↓ motion_pulse
|
||||
cuframes-pub-* ──VMM──→ Composer (C++ Cell/Layout/Decoration)
|
||||
│
|
||||
↓ best-fit + hysteresis
|
||||
Layout::apply()
|
||||
│
|
||||
↓ NV12 zero-copy
|
||||
NVENC ──→ H.264 pipe
|
||||
↓
|
||||
cfc-grid-ffmpeg
|
||||
↓ RTSP push
|
||||
mediamtx ──→ TV/VLC/HLS/WebRTC
|
||||
↑
|
||||
cctv-onvif (PTZ → ZMQ set_layout)
|
||||
```
|
||||
|
||||
Подробности — [docs/ru/developer.md](docs/ru/developer.md) §1.
|
||||
|
||||
## Лицензия
|
||||
|
||||
LGPL-2.1-or-later. См. файл [LICENSE](LICENSE).
|
||||
|
||||
NVENC SDK headers (`third_party/nv-codec-headers`) — MIT license, совместима с LGPL.
|
||||
LGPL-2.1+
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
# cfc-grid — developer guide
|
||||
|
||||
> Audience: developer who edits composer C++ code, adds new
|
||||
> Cell/Decoration/overlay types, or changes auto-layout logic.
|
||||
>
|
||||
> If you're a user — see [user.md](user.md). For deploy/troubleshooting
|
||||
> see [operations.md](operations.md).
|
||||
|
||||
## 1. Architecture (40000-foot view)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ cfc::Composer (C++) │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │SourcePool│ │ Layout │ │OutputSurface │ │
|
||||
│ │ (pool of │ │ vector< │ │ CudaBuffer │ │
|
||||
│ │ cfc_ │ │ unique_ │ │ (VMM NV12) │ │
|
||||
│ │source_t*)│ │ ptr<Cell>│ │ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────┘ │
|
||||
│ ↓ ↓ ↑ │
|
||||
│ motion_pulse apply() NVENC │
|
||||
└──────────────────────────────────────────────┘
|
||||
↑ ↓
|
||||
extern "C" ABI shim H.264 pipe
|
||||
(composer_c_api.cpp, ↓
|
||||
layouts_c_api.cpp, ffmpeg-relay
|
||||
mqtt_overlay_c_api.cpp) ↓
|
||||
↑ mediamtx
|
||||
┌────────┴────────┐ ↓
|
||||
│ │ ↓
|
||||
grid_record.c control.c clients
|
||||
(C, CLI parsing) (ZMQ verbs)
|
||||
↑ ↑
|
||||
│ │
|
||||
frigate_mqtt.c health.c, audio.c, writer.c, source.c
|
||||
(Frigate events) (C-only modules)
|
||||
```
|
||||
|
||||
**Principles:**
|
||||
|
||||
- **C++17 host-side**, CUDA kernels remain on C
|
||||
- **Zero-copy:** output VMM buffer is passed as `NV12Ref` to all
|
||||
cells/decorations (read-write reference, not copy)
|
||||
- **Coexistence:** C modules (`source.c`, `nvenc.c`, `frigate_mqtt.c`,
|
||||
`health.c`, `writer.c`, `audio.c`, `overlay.c`) are kept, accessed via
|
||||
`extern "C"`. Only `composer.c` and `layouts.c` were removed (replaced
|
||||
by C++ + ABI shim).
|
||||
- **`grid_record.c`** (CLI entry point) remains in C, uses public
|
||||
`cfc_composer_*` API unchanged
|
||||
|
||||
## 2. Source tree
|
||||
|
||||
```
|
||||
include/cuframes_composer/
|
||||
├── *.h # Public C API (extern "C")
|
||||
│ ├── composer.h # cfc_composer_* ABI
|
||||
│ ├── layouts.h # cfc_layout_* ABI
|
||||
│ ├── overlay.h # cfc_overlay_* (text/png/border/detbox)
|
||||
│ ├── source.h, nvenc.h, frigate_mqtt.h, ...
|
||||
│ └── ...
|
||||
└── cpp/ # C++ headers — public classes
|
||||
├── cuda_raii.hpp # CudaBuffer, CudaStream (RAII)
|
||||
├── types.hpp # Rect, NV12Ref
|
||||
├── cell.hpp # abstract Cell + draw()
|
||||
├── camera_cell.hpp # CameraCell : Cell
|
||||
├── widget_cell.hpp # WidgetCell : Cell
|
||||
├── blank_cell.hpp # BlankCell : Cell
|
||||
├── decoration.hpp # abstract Decoration
|
||||
├── border_decoration.hpp # BorderDecoration
|
||||
├── label_decoration.hpp # LabelDecoration (FreeType internal)
|
||||
├── template.hpp # LayoutTemplate, CellTemplate, kGridCols/Rows
|
||||
├── template_loader.hpp # JSON parser, current_templates()
|
||||
├── source_pool.hpp # SourcePool, PoolEntry
|
||||
├── layout.hpp # Layout
|
||||
├── composer.hpp # Composer (main class)
|
||||
└── mqtt_overlay.hpp # MqttOverlayItem + Manager
|
||||
|
||||
src/
|
||||
├── *.c # C modules (source/nvenc/health/...)
|
||||
├── overlay.c # All cfc_overlay_t types
|
||||
├── frigate_mqtt.c # Subscribe + motion_pulse + zone-filter
|
||||
├── cugrid/cugrid.cu # CUDA kernels (resize, fill, blit)
|
||||
└── cpp/ # C++ implementations
|
||||
├── camera_cell.cpp, widget_cell.cpp, blank_cell.cpp
|
||||
├── border_decoration.cpp, label_decoration.cpp
|
||||
├── source_pool.cpp
|
||||
├── template_loader.cpp
|
||||
├── layout.cpp
|
||||
├── composer.cpp
|
||||
├── composer_c_api.cpp # extern "C" shim for composer
|
||||
├── layouts_c_api.cpp # extern "C" shim for layouts
|
||||
├── mqtt_overlay.cpp
|
||||
└── mqtt_overlay_c_api.cpp
|
||||
|
||||
examples/
|
||||
├── simple_record.c # 1-source → H.264 file
|
||||
├── grid_record.c # Main CLI entry point (C)
|
||||
└── grid_record_cpp.cpp # C++ smoke (uses cfc::Composer directly)
|
||||
```
|
||||
|
||||
## 3. Single frame lifecycle
|
||||
|
||||
```cpp
|
||||
// In compose loop (grid_record.c, via cfc_composer_compose):
|
||||
NV12Ref ref = composer.compose_frame();
|
||||
// 1. maybe_relayout(): motion-mode? best-fit template; apply Layout
|
||||
// 2. compose_clear(): fill output buffer BT.709 black
|
||||
// 3. Layout::render(): for each cell: draw_content() + decorations[]
|
||||
// 4. for each overlay: sync detbox geom + cfc_overlay_draw()
|
||||
// cudaStreamSynchronize(0);
|
||||
// nvenc.encode(ref.y_ptr, ref.pitch_y, ref.uv_ptr, ref.pitch_uv, ...);
|
||||
```
|
||||
|
||||
All operations execute on CUDA default stream. Zero-copy:
|
||||
`ref.y_ptr` / `ref.uv_ptr` are the same `CUdeviceptr`s inside
|
||||
`composer.output_` (RAII `CudaBuffer`).
|
||||
|
||||
## 4. Cell — hierarchy and extension
|
||||
|
||||
### 4.1 Abstraction
|
||||
|
||||
```cpp
|
||||
class Cell {
|
||||
public:
|
||||
explicit Cell(const Rect& geom);
|
||||
virtual ~Cell() = default;
|
||||
Cell(const Cell&) = delete;
|
||||
|
||||
const Rect& geometry() const noexcept;
|
||||
void set_geometry(const Rect& r) noexcept;
|
||||
void add_decoration(std::unique_ptr<Decoration>);
|
||||
|
||||
void draw(CUstream stream, NV12Ref& dst); // public — calls draw_content() + decorations
|
||||
|
||||
protected:
|
||||
virtual void draw_content(CUstream stream, NV12Ref& dst) = 0;
|
||||
Rect geom_;
|
||||
std::vector<std::unique_ptr<Decoration>> decorations_;
|
||||
};
|
||||
```
|
||||
|
||||
`Cell::draw()` is final (non-virtual — NVI pattern):
|
||||
```cpp
|
||||
void Cell::draw(CUstream stream, NV12Ref& dst) {
|
||||
if (geom_.empty()) return;
|
||||
draw_content(stream, dst); // subclass renders content
|
||||
for (auto& d : decorations_) {
|
||||
d->draw(stream, dst, geom_); // overlay decorations on top
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Existing subclasses
|
||||
|
||||
| Class | draw_content implementation |
|
||||
|---|---|
|
||||
| `CameraCell` | `cfc_source_get_latest()` + `cfc_cugrid_resize_nv12` |
|
||||
| `WidgetCell` | `cfc_cugrid_fill_nv12` dark grey Y=40 (placeholder) |
|
||||
| `BlankCell` | `cfc_cugrid_fill_nv12` BT.709 black Y=16 |
|
||||
|
||||
### 4.3 How to add a new Cell type
|
||||
|
||||
Example: `GraphCell` — draws scrolling chart from MQTT subscription.
|
||||
|
||||
1. Create `include/cuframes_composer/cpp/graph_cell.hpp`:
|
||||
|
||||
```cpp
|
||||
#ifndef CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP
|
||||
#define CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP
|
||||
#include "cell.hpp"
|
||||
#include <deque>
|
||||
|
||||
namespace cfc {
|
||||
|
||||
class GraphCell : public Cell {
|
||||
public:
|
||||
GraphCell(const Rect& geom, std::size_t max_points = 60)
|
||||
: Cell(geom), max_points_(max_points) {}
|
||||
|
||||
void push_value(double v) {
|
||||
if (values_.size() >= max_points_) values_.pop_front();
|
||||
values_.push_back(v);
|
||||
}
|
||||
|
||||
protected:
|
||||
void draw_content(CUstream stream, NV12Ref& dst) override;
|
||||
|
||||
private:
|
||||
std::deque<double> values_;
|
||||
std::size_t max_points_;
|
||||
};
|
||||
|
||||
} // namespace cfc
|
||||
#endif
|
||||
```
|
||||
|
||||
2. Implementation `src/cpp/graph_cell.cpp`:
|
||||
|
||||
```cpp
|
||||
#include "../../include/cuframes_composer/cpp/graph_cell.hpp"
|
||||
#include "../../include/cuframes_composer/cugrid.h"
|
||||
|
||||
namespace cfc {
|
||||
|
||||
void GraphCell::draw_content(CUstream stream, NV12Ref& dst) {
|
||||
if (geom_.empty() || values_.empty()) return;
|
||||
|
||||
// 1. BG fill
|
||||
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
|
||||
geom_.x, geom_.y, geom_.w, geom_.h,
|
||||
/*Y*/ 30, 128, 128, 255);
|
||||
|
||||
// 2. Compute min/max
|
||||
double mn = *std::min_element(values_.begin(), values_.end());
|
||||
double mx = *std::max_element(values_.begin(), values_.end());
|
||||
if (mx == mn) mx = mn + 1.0;
|
||||
|
||||
// 3. Render as thin filled rects (lazy, no bitmap kernel)
|
||||
int n = static_cast<int>(values_.size());
|
||||
int dx = geom_.w / n;
|
||||
for (int i = 0; i < n; i++) {
|
||||
double norm = (values_[i] - mn) / (mx - mn);
|
||||
int y_px = geom_.y + geom_.h - 2 - (int)(norm * (geom_.h - 4));
|
||||
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
|
||||
geom_.x + i * dx, y_px, dx & ~1, 2,
|
||||
/*Y*/ 235, 128, 64, 255); // limited-range bright
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cfc
|
||||
```
|
||||
|
||||
3. Add to `src/CMakeLists.txt`:
|
||||
```cmake
|
||||
set(COMPOSER_SOURCES_CPP
|
||||
...
|
||||
cpp/graph_cell.cpp
|
||||
)
|
||||
```
|
||||
|
||||
4. Use in `Layout::apply()`:
|
||||
|
||||
```cpp
|
||||
// In layout.cpp, widget cells handling:
|
||||
if (wt->widget == "graph_temp") {
|
||||
cells_.push_back(std::make_unique<GraphCell>(r, /*max_points=*/120));
|
||||
} else {
|
||||
cells_.push_back(std::make_unique<WidgetCell>(r, wt->widget));
|
||||
}
|
||||
```
|
||||
|
||||
5. Wire MQTT → `GraphCell::push_value()` — either via MqttOverlayManager
|
||||
extension, or new API `cfc::Composer::register_graph_feed()`.
|
||||
|
||||
## 5. Decoration — composition in Cell
|
||||
|
||||
### 5.1 Abstraction
|
||||
|
||||
```cpp
|
||||
class Decoration {
|
||||
public:
|
||||
virtual ~Decoration() = default;
|
||||
virtual void draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
Decoration knows only parent cell's pixel-rect — positions **relative
|
||||
to it**. Has no access to Layout/Composer.
|
||||
|
||||
### 5.2 Existing subclasses
|
||||
|
||||
- **`LabelDecoration`** — FreeType atlas + `cfc_cugrid_blit_rgba_nv12`.
|
||||
Persistent VRAM (rebuild on `set_text`). Supports background fill
|
||||
(via `cfc_overlay_text_config_t.bg_*` parameters).
|
||||
- **`BorderDecoration`** — 4 calls of `cfc_cugrid_fill_nv12` (top, bottom,
|
||||
left, right rects by `thickness`).
|
||||
|
||||
### 5.3 How to add a new Decoration type
|
||||
|
||||
Example: `BadgeDecoration` — corner icon (e.g. red "recording" dot).
|
||||
|
||||
1. Header `badge_decoration.hpp`:
|
||||
|
||||
```cpp
|
||||
class BadgeDecoration : public Decoration {
|
||||
public:
|
||||
struct Style {
|
||||
int corner = 0; // 0=top-left, 1=top-right, 2=bottom-right, 3=bottom-left
|
||||
int size = 12;
|
||||
int margin = 8;
|
||||
int color_y = 80, color_u = 90, color_v = 240; // red-ish
|
||||
int alpha = 240;
|
||||
bool visible = true;
|
||||
};
|
||||
explicit BadgeDecoration(const Style& s) : style_(s) {}
|
||||
void set_visible(bool v) noexcept { style_.visible = v; }
|
||||
void draw(CUstream stream, NV12Ref& dst, const Rect& p) override;
|
||||
private:
|
||||
Style style_;
|
||||
};
|
||||
```
|
||||
|
||||
2. Implementation:
|
||||
|
||||
```cpp
|
||||
void BadgeDecoration::draw(CUstream s, NV12Ref& dst, const Rect& p) {
|
||||
if (!style_.visible || style_.alpha <= 0) return;
|
||||
int x, y;
|
||||
switch (style_.corner) {
|
||||
case 0: x = p.x + style_.margin; y = p.y + style_.margin; break;
|
||||
case 1: x = p.x + p.w - style_.size - style_.margin; y = p.y + style_.margin; break;
|
||||
case 2: x = p.x + p.w - style_.size - style_.margin; y = p.y + p.h - style_.size - style_.margin; break;
|
||||
case 3: x = p.x + style_.margin; y = p.y + p.h - style_.size - style_.margin; break;
|
||||
}
|
||||
x &= ~1; y &= ~1;
|
||||
cfc_cugrid_fill_nv12(s, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
|
||||
x, y, style_.size, style_.size,
|
||||
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
|
||||
}
|
||||
```
|
||||
|
||||
3. Wire in `Layout::apply()`:
|
||||
|
||||
```cpp
|
||||
if (cc->source()->state() == CFC_SOURCE_STALE) {
|
||||
BadgeDecoration::Style bs;
|
||||
bs.corner = 1; bs.color_y = 180; bs.color_v = 100; // yellow
|
||||
cell->add_decoration(std::make_unique<BadgeDecoration>(bs));
|
||||
}
|
||||
```
|
||||
|
||||
## 6. SourcePool and motion state
|
||||
|
||||
```cpp
|
||||
struct PoolEntry {
|
||||
std::string cuframes_key;
|
||||
std::string frigate_camera;
|
||||
int priority;
|
||||
cfc_source_t* source;
|
||||
std::atomic<int64_t> last_motion_ms;
|
||||
std::vector<std::string> required_zones;
|
||||
|
||||
cfc_source_state_t state() const; // calls cfc_source_get_latest
|
||||
bool drawable() const; // ACTIVE || STALE
|
||||
};
|
||||
|
||||
class SourcePool {
|
||||
public:
|
||||
int add(key, frigate_camera, priority, zones, SubscribeOpts);
|
||||
PoolEntry* by_key(const std::string&);
|
||||
PoolEntry* by_frigate_camera(const std::string&);
|
||||
template<typename F> void for_each(F&&);
|
||||
void motion_pulse(const std::string& frigate_camera,
|
||||
const std::vector<std::string>& current_zones);
|
||||
};
|
||||
```
|
||||
|
||||
`motion_pulse` is called from `frigate_mqtt.c` for each event. If
|
||||
`required_zones` is non-empty — match by intersection with
|
||||
`event.current_zones`, otherwise accept all.
|
||||
|
||||
## 7. Auto-layout algorithms
|
||||
|
||||
See `cfc::Composer::maybe_relayout()` (src/cpp/composer.cpp).
|
||||
|
||||
### 7.1 Best-fit selection
|
||||
|
||||
```cpp
|
||||
const LayoutTemplate* Composer::pick_best_fit(int need) const {
|
||||
const auto& reg = current_templates();
|
||||
const LayoutTemplate* best = nullptr;
|
||||
int best_waste = -1, best_prio = -1;
|
||||
for (auto& t : reg) {
|
||||
int n = t.nb_camera_cells();
|
||||
if (n < need) continue;
|
||||
int waste = n - need;
|
||||
if (!best || waste < best_waste ||
|
||||
(waste == best_waste && t.priority > best_prio)) {
|
||||
best = &t; best_waste = waste; best_prio = t.priority;
|
||||
}
|
||||
}
|
||||
if (best) return best;
|
||||
// overflow → largest
|
||||
for (auto& t : reg) {
|
||||
if (!best || t.nb_camera_cells() > best->nb_camera_cells()) best = &t;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Collect active
|
||||
|
||||
```cpp
|
||||
std::vector<PoolEntry*> Composer::collect_active() const {
|
||||
std::vector<PoolEntry*> active;
|
||||
int64_t now = now_ms_mono();
|
||||
pool_.for_each([&](PoolEntry& e) {
|
||||
if (!e.drawable()) return; // DEAD/CONNECTING — skip
|
||||
int64_t last = e.last_motion_ms.load();
|
||||
if (last == 0) return; // never had motion
|
||||
if (now - last > cfg_.motion_ttl_ms) return; // TTL expired
|
||||
active.push_back(&e);
|
||||
});
|
||||
// idle fallback: top-priority drawable
|
||||
if (active.empty()) {
|
||||
PoolEntry* best = nullptr;
|
||||
pool_.for_each([&](PoolEntry& e) {
|
||||
if (!e.drawable()) return;
|
||||
if (!best || e.priority > best->priority) best = &e;
|
||||
});
|
||||
if (best) active.push_back(best);
|
||||
}
|
||||
std::sort(active.begin(), active.end(),
|
||||
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
|
||||
return active;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Asymmetric hysteresis
|
||||
|
||||
Signature `template_name + "|" + sorted_keys` is remembered. If new
|
||||
set ⊇ committed (growth) → commit immediately. Otherwise wait
|
||||
`shrink_hysteresis_ms` (default 3000) → commit.
|
||||
|
||||
```cpp
|
||||
bool is_grow = std::includes(nkeys.begin(), nkeys.end(),
|
||||
ckeys.begin(), ckeys.end());
|
||||
if (is_grow && nkeys.size() == ckeys.size()) is_grow = false;
|
||||
if (is_grow) { /* commit */ } else {
|
||||
if (sig != pending_signature_) {
|
||||
pending_signature_ = sig;
|
||||
pending_first_seen_ms_ = now;
|
||||
}
|
||||
if (now - pending_first_seen_ms_ < cfg_.shrink_hysteresis_ms) return;
|
||||
/* commit */
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 Fill free cells
|
||||
|
||||
After picking template and capping by `nb_camera_cells`:
|
||||
|
||||
```cpp
|
||||
if (active.size() < cap) {
|
||||
std::vector<PoolEntry*> extras;
|
||||
pool_.for_each([&](PoolEntry& e) {
|
||||
if (!e.drawable()) return;
|
||||
if (e ∈ already_active) return;
|
||||
extras.push_back(&e);
|
||||
});
|
||||
std::sort(extras.begin(), extras.end(), priority_desc);
|
||||
while (active.size() < cap && !extras.empty()) {
|
||||
active.push_back(extras.front());
|
||||
extras.erase(extras.begin());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.5 Manual PTZ override
|
||||
|
||||
`Composer::set_layout(name)` in motion-mode:
|
||||
```cpp
|
||||
manual_override_until_ms_ = now + manual_override_duration_ms_; // default 60s
|
||||
```
|
||||
|
||||
`maybe_relayout()` skips work while `now < manual_override_until_ms_`.
|
||||
After expiry — `committed_signature_.clear()` → forced relayout.
|
||||
|
||||
## 8. extern "C" ABI shim
|
||||
|
||||
`composer_c_api.cpp` — thin wrapper:
|
||||
|
||||
```cpp
|
||||
extern "C" int cfc_composer_create(const cfc_composer_config_t* cfg,
|
||||
cfc_composer_t** out)
|
||||
{
|
||||
cfc::ComposerConfig cpp_cfg = {/* ... */};
|
||||
auto* comp = new (std::nothrow) cfc::Composer(cpp_cfg);
|
||||
if (!comp->ok()) { delete comp; return -1; }
|
||||
// manual_cells from cfg->cells → set_manual_cells()
|
||||
*out = reinterpret_cast<cfc_composer_t*>(comp);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
`cfc_composer_t` is opaque in C, `reinterpret_cast` to/from `cfc::Composer*`
|
||||
in shim.
|
||||
|
||||
`layouts_c_api.cpp` is analogous for `cfc_layout_*`. Holds static cache
|
||||
`vector<cfc_layout_t>` resynced with `cfc::current_templates()` on reload.
|
||||
|
||||
## 9. Build
|
||||
|
||||
### 9.1 CMake
|
||||
|
||||
```cmake
|
||||
project(cuframes-composer LANGUAGES C CXX CUDA)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
```
|
||||
|
||||
`COMPOSER_SOURCES_CPP` in `src/CMakeLists.txt` lists all .cpp files.
|
||||
|
||||
### 9.2 Host build (for CI / dev machine Ubuntu 24.04)
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/cuframes-composer
|
||||
mkdir -p build && cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### 9.3 Production build (Ubuntu 22.04 jammy)
|
||||
|
||||
See [operations.md](operations.md). Briefly:
|
||||
```bash
|
||||
docker run --rm --gpus all \
|
||||
-v "$PWD":/src -w /src/build-jammy \
|
||||
cuframes-composer-builder:cached \
|
||||
bash -c 'cmake .. && make -j$(nproc)'
|
||||
```
|
||||
|
||||
## 10. Performance
|
||||
|
||||
### 10.1 Zero-copy guarantees
|
||||
|
||||
- One `CudaBuffer output_` per `Composer`, passed as `NV12Ref` to all
|
||||
cells/decorations
|
||||
- Cell does not create VRAM allocations per frame (only `Decoration::draw`
|
||||
may do FreeType rebuild on `set_text` — that's offline)
|
||||
- Layout::apply() recreates `vector<unique_ptr<Cell>>` only on template
|
||||
change; typically every N seconds
|
||||
|
||||
### 10.2 Virtual call overhead
|
||||
|
||||
`Cell::draw()` → `draw_content()` virtual call: 1 indirect call per cell
|
||||
per frame. At 25 fps × 16 cells = **400 calls/sec** — irrelevant.
|
||||
|
||||
### 10.3 Hot path
|
||||
|
||||
CUDA kernels (`cugrid.cu`):
|
||||
- `fill_nv12` — 1 kernel launch per rect
|
||||
- `resize_nv12` — bilinear via 2 kernels (Y plane + UV plane)
|
||||
- `blit_rgba_nv12` — 1 kernel, RGBA → NV12 + alpha-blend
|
||||
|
||||
**All Cell operations convert to N×fill_nv12 + 1×resize_nv12 +
|
||||
M×blit_rgba_nv12** — batching not yet needed (GPU is not sticky on
|
||||
current load).
|
||||
|
||||
## 11. Where to edit common tasks
|
||||
|
||||
| I want to… | File |
|
||||
|---|---|
|
||||
| Change hysteresis | `composer.hpp` → `ComposerConfig::shrink_hysteresis_ms` |
|
||||
| Change cell border color | `layout.cpp` → `border_style.color_y/u/v` |
|
||||
| Change label font | `mqtt_overlay.cpp` → `LabelDecoration` style + `LabelStyle::font_path` |
|
||||
| Add ZMQ verb | `control.c::dispatch()` + new `cmd_*` function |
|
||||
| Change manual override duration | `composer.hpp` → `manual_override_duration_ms_` |
|
||||
| Add new MQTT-overlay anchor | `mqtt_overlay.cpp::reposition_overlay()` switch |
|
||||
| Support color emoji | `overlay.c::text_rebuild_atlas()` — handle `FT_LOAD_COLOR` + bitmap BGRA → blit as RGBA |
|
||||
|
||||
## 12. Testing
|
||||
|
||||
### 12.1 Unit (not implemented)
|
||||
|
||||
Planned: catch2 tests for `pick_best_fit`, `collect_active`, hysteresis.
|
||||
CUDA dependency — mock via `cfc_source_get_latest` shim.
|
||||
|
||||
### 12.2 Integration smoke (`examples/grid_record_cpp.cpp`)
|
||||
|
||||
Minimal C++ smoke: init Composer, compose loop, dump NV12 → file. Does
|
||||
not use NVENC (tests composition only).
|
||||
|
||||
```bash
|
||||
build/examples/grid_record_cpp \
|
||||
--out=/tmp/dump.nv12 --frames=10 --width=1920 --height=1080 \
|
||||
--templates=docker/templates.json \
|
||||
--source=cam-parking,frigate=parking_overview,priority=100 \
|
||||
--motion-mode
|
||||
```
|
||||
|
||||
### 12.3 Production smoke
|
||||
|
||||
`docker logs cfc-grid | grep -E "loaded|template|motion|grow|shrink"` —
|
||||
live composer telemetry.
|
||||
|
||||
## 13. Known pitfalls
|
||||
|
||||
- **`cfc_overlay_t` is not RAII** — managed via `cfc_composer_add_overlay`
|
||||
/ `cfc_overlay_destroy` (Composer.cpp::~Composer destroys all).
|
||||
- **`pthread_mutex_t` in SourcePool** vs `std::mutex` — chose `std::mutex`
|
||||
for C++ layer, `pthread_mutex_t` for C layer (PoolEntry::last_motion_ms
|
||||
uses `std::atomic`).
|
||||
- **Compose loop is NOT blocking** on CUDA ops — `cudaStreamSynchronize`
|
||||
called by caller (grid_record.c) before NVENC.
|
||||
- **Frigate event may arrive BEFORE Layout::apply** — `motion_pulse`
|
||||
updates `last_motion_ms`, but cell for that camera may not exist yet.
|
||||
On next frame `maybe_relayout` recalculates.
|
||||
- **`dynamic_cast<CameraCell*>` in `Layout::find_camera_cell_rect`** —
|
||||
uses RTTI. Enabled `-frtti` by default in g++/nvcc.
|
||||
|
||||
## 14. Phase task change workflow
|
||||
|
||||
1. Branch `feature/<phase>-<feature>` off `main`
|
||||
2. Implementation, host build PASS, jammy build PASS (see operations.md)
|
||||
3. Bake image `gx/cuframes-composer:<phase>-stepN`
|
||||
4. Deploy to dev target → smoke verify via VLC/logs
|
||||
5. Commit + push branch
|
||||
6. (If needed) — merge to `main` via `--no-ff` PR-style
|
||||
|
||||
For multi-commit phase: one WIP merge commit on main describing key
|
||||
changes.
|
||||
@@ -0,0 +1,402 @@
|
||||
# cfc-grid — operations / deploy / troubleshooting
|
||||
|
||||
> Audience: who builds, deploys, monitors cfc-grid in production.
|
||||
>
|
||||
> If you're a user — see [user.md](user.md). For developer documentation
|
||||
> — see [developer.md](developer.md).
|
||||
|
||||
## 1. Production setup (R9-88.23)
|
||||
|
||||
### 1.1 Stack
|
||||
|
||||
```
|
||||
docker compose -f docker-compose.yml \
|
||||
-f cuda-grid/docker-compose.override.yml \
|
||||
-f cuframes-composer/docker-compose.override.yml \
|
||||
-f onvif/docker-compose.override.yml \
|
||||
up -d
|
||||
```
|
||||
|
||||
Files — in `localhost-infra/hosts/R9-88.23/docker/cctv/`.
|
||||
|
||||
| Service | Image | Purpose |
|
||||
|---|---|---|
|
||||
| `cuframes-ipc-anchor` | `gx/cuframes:0.4` | Shared VMM IPC anchor for cuframes |
|
||||
| `cuframes-pub-*` (parking/back_yard/front_yard/gate_lpr) | `gx/cuframes:0.4` | RTSP → cuframes per-camera publishers |
|
||||
| `cuda-grid-mediamtx` | `bluenviron/mediamtx` | RTSP/HLS/WebRTC gateway |
|
||||
| `cctv-mosquitto` | `eclipse-mosquitto` | MQTT broker (+bridge to 192.168.88.4) |
|
||||
| **`cfc-grid`** | `gx/cuframes-composer:0.11b-step1` | Composer (main service) |
|
||||
| `cfc-grid-ffmpeg` | `ffmpeg-vf-cuda-grid:phase4b-final` | H.264 pipe → RTSP push |
|
||||
| `cfc-grid-watchdog` | `gx/cuda-grid-watchdog:0.4` | Restart cfc-grid on stuck inboundBytes |
|
||||
| `cctv-onvif` | `gx/cctv-onvif:0.6` | ONVIF discovery + PTZ → ZMQ |
|
||||
| `cctv-frigate` | `ghcr.io/blakeblackshear/frigate` | Object detection → MQTT events |
|
||||
|
||||
### 1.2 Frame flow
|
||||
|
||||
```
|
||||
cuframes-pub-X ──VMM──┐
|
||||
cuframes-pub-Y ──VMM──┼──→ cfc-grid (composer)
|
||||
cuframes-pub-Z ──VMM──┘ │
|
||||
│ H.264 NVENC
|
||||
↓ named pipe /tmp/cfc-pipe-dir/grid.h264
|
||||
cfc-grid-ffmpeg (re-mux)
|
||||
│ RTSP push
|
||||
↓
|
||||
cuda-grid-mediamtx
|
||||
rtsp://*/cfc-grid (TCP/UDP)
|
||||
http://*:8888/cfc-grid (HLS)
|
||||
http://*:8889/cfc-grid (WebRTC)
|
||||
```
|
||||
|
||||
### 1.3 Networks
|
||||
|
||||
- Internal docker network: `cctv`
|
||||
- External ports on R9-88.23:
|
||||
- `554/tcp` — RTSP (mediamtx)
|
||||
- `8888/tcp` — HLS (mediamtx)
|
||||
- `8889/tcp` — WebRTC (mediamtx)
|
||||
- `5599/tcp` — ZMQ composer control plane
|
||||
- `8085/tcp` — ONVIF SOAP (cctv-onvif)
|
||||
- `3702/udp` — WS-Discovery multicast (cctv-onvif)
|
||||
|
||||
## 2. Build
|
||||
|
||||
### 2.1 Local host build (Ubuntu 24.04, dev machine)
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/cuframes-composer
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
Artifacts in `build/src/libcuframes_composer.so` and `build/examples/grid_record`.
|
||||
|
||||
**IMPORTANT:** host binary (Ubuntu 24.04, glibc 2.39, libavformat60) is
|
||||
**incompatible** with runtime container (Ubuntu 22.04 jammy, glibc 2.35,
|
||||
libavformat58). See memory `incremental-ffmpeg-rebuild`.
|
||||
|
||||
### 2.2 Jammy build (for production image)
|
||||
|
||||
Uses cached builder container `cuframes-composer-builder:cached`
|
||||
(Ubuntu 22.04 + nvidia/cuda:12.4.1-devel + apt-deps):
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/cuframes-composer
|
||||
|
||||
# If builder not yet cached:
|
||||
docker image inspect cuframes-composer-builder:cached >/dev/null 2>&1 || {
|
||||
docker run -d --name cfc-builder-tmp \
|
||||
nvidia/cuda:12.4.1-devel-ubuntu22.04 sleep 3600
|
||||
docker exec cfc-builder-tmp bash -c '
|
||||
apt-get update -qq && apt-get install -y -qq --no-install-recommends \
|
||||
build-essential cmake git pkg-config \
|
||||
libpng-dev libfreetype-dev \
|
||||
libzmq3-dev libjson-c-dev libmosquitto-dev \
|
||||
libavformat-dev libavcodec-dev libavutil-dev'
|
||||
docker commit cfc-builder-tmp cuframes-composer-builder:cached
|
||||
docker rm -f cfc-builder-tmp
|
||||
}
|
||||
|
||||
# Actual build:
|
||||
docker run --rm --gpus all -v "$PWD":/src -w /src/build-jammy \
|
||||
cuframes-composer-builder:cached \
|
||||
bash -c 'cmake -DCMAKE_BUILD_TYPE=Release .. && make -j$(nproc)'
|
||||
```
|
||||
|
||||
Artifacts in `build-jammy/`.
|
||||
|
||||
### 2.3 Bake image (incremental — without `docker build`)
|
||||
|
||||
We don't use `docker build` (4GB CUDA pull on cache miss). Instead:
|
||||
|
||||
```bash
|
||||
docker rmi gx/cuframes-composer:0.11b-step1 -f 2>/dev/null
|
||||
CID=$(docker create gx/cuframes-composer:0.10)
|
||||
docker cp build-jammy/examples/grid_record "$CID":/usr/local/bin/grid_record
|
||||
docker cp build-jammy/src/libcuframes_composer.so.0.1.0 \
|
||||
"$CID":/usr/lib/x86_64-linux-gnu/libcuframes_composer.so.0
|
||||
docker cp docker/templates.json "$CID":/opt/templates.json
|
||||
docker cp docker/mqtt_overlays.json "$CID":/opt/mqtt_overlays.json
|
||||
docker commit \
|
||||
--change 'ENTRYPOINT ["/usr/local/bin/grid_record"]' \
|
||||
--change 'CMD ["--help"]' \
|
||||
--change 'ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,video' \
|
||||
"$CID" gx/cuframes-composer:0.11b-step1
|
||||
docker rm "$CID"
|
||||
```
|
||||
|
||||
Uses `gx/cuframes-composer:0.10` as base (with runtime deps already
|
||||
installed) + overlays fresh artifacts. Faster and no network traffic.
|
||||
|
||||
### 2.4 Build ONVIF image
|
||||
|
||||
```bash
|
||||
cd hosts/R9-88.23/docker/cctv/onvif
|
||||
docker build -t gx/cctv-onvif:0.6 -f Dockerfile .
|
||||
```
|
||||
|
||||
Python image, lightweight. If you change `server.py` — rebuild image
|
||||
(bump tag) + update `docker-compose.override.yml`.
|
||||
|
||||
## 3. Deploy
|
||||
|
||||
### 3.1 Production (R9-88.23)
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/localhost-infra/hosts/R9-88.23/docker/cctv
|
||||
docker compose -f docker-compose.yml \
|
||||
-f cuda-grid/docker-compose.override.yml \
|
||||
-f cuframes-composer/docker-compose.override.yml \
|
||||
-f onvif/docker-compose.override.yml \
|
||||
up -d cfc-grid
|
||||
```
|
||||
|
||||
Compose auto-recreates container if image tag changed in
|
||||
`docker-compose.override.yml`.
|
||||
|
||||
### 3.2 Verify post-deploy
|
||||
|
||||
```bash
|
||||
docker logs --tail 30 cfc-grid 2>&1 | grep -iE "loaded|template|pool|motion"
|
||||
|
||||
# Expect something like:
|
||||
# [cfc/loader] /opt/templates.json: loaded 9 templates
|
||||
# [cfc/composer] templates loaded: 9 (path='/opt/templates.json')
|
||||
# [cfc/composer] pool+ 'cam-parking' (frigate=parking_overview prio=100)
|
||||
# [cfc/composer] motion_mode=1 ttl=45000ms pool=4
|
||||
# [cfc/composer] grow → template='tpl_3' active=3
|
||||
```
|
||||
|
||||
### 3.3 Rollback
|
||||
|
||||
```bash
|
||||
sed -i 's|gx/cuframes-composer:0.11b-step1|gx/cuframes-composer:0.10|' \
|
||||
hosts/R9-88.23/docker/cctv/cuframes-composer/docker-compose.override.yml
|
||||
docker compose ... up -d cfc-grid
|
||||
```
|
||||
|
||||
## 4. Logs
|
||||
|
||||
### 4.1 Live tail
|
||||
|
||||
```bash
|
||||
docker logs -f --tail 50 cfc-grid
|
||||
docker logs -f --tail 30 cfc-grid-ffmpeg
|
||||
docker logs -f --tail 30 cuda-grid-mediamtx
|
||||
docker logs -f --tail 30 cctv-onvif
|
||||
docker logs -f --tail 30 cctv-frigate
|
||||
```
|
||||
|
||||
### 4.2 Telemetry patterns
|
||||
|
||||
| Marker | Meaning |
|
||||
|---|---|
|
||||
| `[grid_record] N kadrov, M IDR, X MB ...` | Composer successfully encoding every ~50 frames |
|
||||
| `[cfc/composer] grow → template='X'` | New template applied (growth, immediate) |
|
||||
| `[cfc/composer] shrink → template='X'` | New template applied after hysteresis (shrink) |
|
||||
| `[cfc/composer] manual override 'X' до +60000ms` | PTZ via ONVIF |
|
||||
| `[cfc/composer] manual override expired, возврат в motion-mode` | Auto-return after 60s |
|
||||
| `[cfc/mqtt-overlay/<id>] '<text>'` | MQTT-overlay received/rendered new text |
|
||||
| `[cfc/frigate] connected, subscribe 'frigate/events'` | Frigate subscriber connected |
|
||||
|
||||
### 4.3 When something breaks
|
||||
|
||||
| Symptom | Where to look |
|
||||
|---|---|
|
||||
| `src active=0 stale=0 dead=4` | cuframes-pub-* containers; check `docker ps` and network access to cameras |
|
||||
| `overlay 0 draw failed` | `cfc_overlay_text_rebuild_atlas` — usually invalid font or text |
|
||||
| RTSP stream not delivering | `cfc-grid-ffmpeg` logs; see §6.1 |
|
||||
| TV/ONVIF can't find | `cctv-onvif` logs; check multicast WS-Discovery in LAN |
|
||||
|
||||
## 5. Monitoring
|
||||
|
||||
### 5.1 MQTT health
|
||||
|
||||
`cfc-grid` publishes health to `cuda_grid/health/composer/cfc-grid`
|
||||
every ~10 seconds:
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime_s": 3600,
|
||||
"frames_encoded": 90000,
|
||||
"fps_actual": 25.0,
|
||||
"bitrate_kbps": 6000,
|
||||
"src_active": 4,
|
||||
"src_stale": 0,
|
||||
"src_dead": 0,
|
||||
"idr_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
PW=$(grep '^COMPOSER_MQTT_PASSWORD=' \
|
||||
/home/claude/projects/localhost-infra/hosts/R9-88.23/docker/cctv/.env | cut -d= -f2)
|
||||
mosquitto_sub -h 192.168.88.23 -u composer -P "$PW" \
|
||||
-t 'cuda_grid/health/composer/cfc-grid' -v
|
||||
```
|
||||
|
||||
### 5.2 Watchdog
|
||||
|
||||
`cfc-grid-watchdog` is a separate service, monitors mediamtx
|
||||
`inboundBytes` for `cfc-grid` path. If **30 seconds of silence** —
|
||||
`docker restart cfc-grid`.
|
||||
|
||||
Watchdog logs:
|
||||
```bash
|
||||
docker logs --tail 30 cfc-grid-watchdog
|
||||
```
|
||||
|
||||
On trigger — publishes to `cuda_grid/health/watchdog/cfc-grid`.
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
### 6.1 RTSP not delivering / `cfc-grid-ffmpeg` "Broken pipe"
|
||||
|
||||
**Symptom:** `docker logs cfc-grid-ffmpeg` shows
|
||||
`[out#0/rtsp] Task finished with error code: -32 (Broken pipe)`.
|
||||
|
||||
**Cause:** `--intra-refresh` in composer (no IDR bursts), mediamtx
|
||||
tears RTSP publisher when it can't deliver start-frame to a new client.
|
||||
|
||||
**Treatment:**
|
||||
- Full pipeline restart:
|
||||
```bash
|
||||
docker compose ... restart cfc-grid-ffmpeg cfc-grid cuda-grid-mediamtx
|
||||
```
|
||||
- If recurring — disable `--intra-refresh` in compose override
|
||||
(cost: IDR bursts in bitrate, but more stable for downstream clients
|
||||
with frequent disconnect/reconnect)
|
||||
|
||||
### 6.2 ffmpeg doesn't receive frames from RTSP
|
||||
|
||||
**Symptom:** `ffmpeg -i rtsp://192.168.88.23:554/cfc-grid -frames:v 1 out.jpg`
|
||||
hangs for 30+ seconds.
|
||||
|
||||
**Cause:** Composer writes H.264 without regular IDR (intra-refresh).
|
||||
A new RTSP client waits for a keyframe to start decoding. ffmpeg in
|
||||
default config doesn't wait long enough.
|
||||
|
||||
**Workaround:**
|
||||
```bash
|
||||
ffmpeg -rtsp_transport tcp \
|
||||
-analyzeduration 10000000 -probesize 10000000 \
|
||||
-i rtsp://192.168.88.23:554/cfc-grid \
|
||||
-frames:v 1 -y out.jpg
|
||||
```
|
||||
|
||||
Or use HLS:
|
||||
```bash
|
||||
ffmpeg -i http://192.168.88.23:8888/cfc-grid/index.m3u8 \
|
||||
-frames:v 1 -y out.jpg
|
||||
```
|
||||
|
||||
### 6.3 MQTT-overlay not updating
|
||||
|
||||
**Checklist:**
|
||||
|
||||
1. Bridge to HA broker (192.168.88.4) working?
|
||||
```bash
|
||||
docker logs cctv-mosquitto 2>&1 | grep -i 'bridge'
|
||||
```
|
||||
Look for `Connecting bridge ha-bridge` and connect confirmation.
|
||||
|
||||
2. Required topic in bridge config?
|
||||
```bash
|
||||
docker exec cctv-mosquitto grep 'topic.*in 0' /mosquitto/config/mosquitto.conf
|
||||
```
|
||||
If new prefix — add `topic XXX/# in 0` and restart mosquitto.
|
||||
|
||||
3. Subscriber connected?
|
||||
```bash
|
||||
docker logs cfc-grid 2>&1 | grep 'mqtt-overlay/<id>.*connected'
|
||||
```
|
||||
|
||||
4. Test publish:
|
||||
```bash
|
||||
mosquitto_pub -h 192.168.88.4 -t '<your topic>' -m 'test' -r
|
||||
```
|
||||
In composer logs, should appear `[cfc/mqtt-overlay/<id>] 'test'`.
|
||||
|
||||
### 6.4 Motion-mode not switching layouts
|
||||
|
||||
**Checklist:**
|
||||
|
||||
1. Frigate sending events?
|
||||
```bash
|
||||
mosquitto_sub -h 192.168.88.23 -u composer -P "$PW" \
|
||||
-t 'frigate/events' -C 3
|
||||
```
|
||||
|
||||
2. Composer receiving events?
|
||||
```bash
|
||||
docker logs cfc-grid 2>&1 | grep 'frigate.*started\|grow\|shrink'
|
||||
```
|
||||
|
||||
3. Camera-name matches?
|
||||
`frigate=<name>` in `--source` must match `event.after.camera`.
|
||||
|
||||
4. Zone-filter not blocking?
|
||||
If `zones=A:B:C` in `--source` — check Frigate event `current_zones`.
|
||||
If empty or doesn't intersect — pulse is discarded.
|
||||
|
||||
5. TTL not expired?
|
||||
Logs `motion_ttl=45000` (45 sec) — if events come less frequently —
|
||||
camera drops from active.
|
||||
|
||||
### 6.5 ONVIF PTZ presets empty in TV
|
||||
|
||||
**Cause:** TV cached old `GetPresets` response (Phase 9 names).
|
||||
|
||||
**Treatment:** delete and re-add camera in TV client.
|
||||
|
||||
### 6.6 Templates loaded but motion-mode doesn't use new
|
||||
|
||||
Composer reads global registry `cfc::current_templates()` on every frame
|
||||
— changes via `cfc_layout_load_file` (ZMQ or CLI) should be picked up
|
||||
immediately. If not — check:
|
||||
|
||||
```bash
|
||||
echo '{"cmd":"list_layouts"}' | python3 -c "
|
||||
import zmq,json,sys
|
||||
s = zmq.Context().socket(zmq.REQ)
|
||||
s.connect('tcp://192.168.88.23:5599')
|
||||
s.send_json({'cmd':'list_layouts'})
|
||||
print(json.dumps(s.recv_json(), indent=2, ensure_ascii=False))"
|
||||
```
|
||||
|
||||
`source` field shows currently loaded path. If built-in (only `tpl_1` +
|
||||
`tpl_4`) — JSON didn't load (syntax error, wrong path).
|
||||
|
||||
## 7. Configs in repo
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| templates.json | `cuframes-composer/docker/templates.json` |
|
||||
| mqtt_overlays.json | `cuframes-composer/docker/mqtt_overlays.json` |
|
||||
| compose override | `localhost-infra/hosts/R9-88.23/docker/cctv/cuframes-composer/docker-compose.override.yml` |
|
||||
| ONVIF config | `localhost-infra/.../onvif/onvif.yaml` |
|
||||
| ONVIF server | `localhost-infra/.../onvif/server.py` |
|
||||
| Mosquitto config | `localhost-infra/.../cctv/mosquitto/config/mosquitto.conf` |
|
||||
| .env (passwords) | `localhost-infra/.../cctv/.env` (gitignored) |
|
||||
|
||||
After changing compose override — `docker compose ... up -d cfc-grid`
|
||||
auto-recreates.
|
||||
|
||||
## 8. Known limitations / TODO
|
||||
|
||||
- **`--intra-refresh` ↔ RTSP clients**: trade-off bitrate vs latency
|
||||
(see §6.1)
|
||||
- **Watchdog only for cfc-grid**: cfc-grid-ffmpeg in zombie state not
|
||||
detected directly; only full restart helps
|
||||
- **Hot-reload of mqtt_overlays.json**: no ZMQ verb
|
||||
- **Per-overlay MQTT broker config**: all via single broker; for
|
||||
foreign broker — need to extend `MqttBrokerCfg` per-item
|
||||
|
||||
## 9. See also
|
||||
|
||||
- [user.md](user.md) — composer configuration
|
||||
- [developer.md](developer.md) — internals, adding modules
|
||||
- `memory/host-and-project.md` — general R9-88.23 infra
|
||||
- `memory/project_cfc-grid-deployed.md` — first prod deploy
|
||||
- `memory/project_cfc-grid-cpp-refactor.md` — Phase 11b refactor
|
||||
- `memory/incremental-ffmpeg-rebuild.md` — incremental docker recipe
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
# cfc-grid — user guide
|
||||
|
||||
> Audience: installation admin. Configures cameras, layouts, overlays,
|
||||
> watches the RTSP stream on TV or browser. Does not edit C++ code.
|
||||
>
|
||||
> Developer extending Cell/Decoration/widget types → see
|
||||
> [developer.md](developer.md).
|
||||
|
||||
## 1. What this is
|
||||
|
||||
**cfc-grid** is a CUDA compositor that combines N cameras into a single
|
||||
RTSP stream `rtsp://192.168.88.23:554/cfc-grid` (1920×1080, H.264, NVENC).
|
||||
Layout is selected automatically by motion (from Frigate) or manually via
|
||||
ONVIF presets on the TV.
|
||||
|
||||
In addition to camera frames, the following are drawn on top:
|
||||
|
||||
- **Borders** — grey 2 px borders around each cell
|
||||
- **Labels** — `name prio=N` caption in the corner of each camera
|
||||
- **Detection boxes** — object rectangles from Frigate, tracking camera
|
||||
position when layout changes
|
||||
- **MQTT overlays** — text fields bound to MQTT topics (temperature,
|
||||
statuses, chats)
|
||||
|
||||
## 2. Architecture in one sentence
|
||||
|
||||
```
|
||||
cuframes-pub-* (per camera)
|
||||
↓ shared VMM
|
||||
cfc-grid (composer) ── ZMQ control ──┐
|
||||
↓ pipe (H.264) │
|
||||
cfc-grid-ffmpeg (relay) ─→ mediamtx ─┴─→ TV / VLC / Frigate / ...
|
||||
↑
|
||||
ONVIF discovery from cctv-onvif
|
||||
```
|
||||
|
||||
Frames go through cuframes zero-copy (a single VMM buffer shared between
|
||||
publisher and composer). The composer takes the NV12 surface, resizes/blits
|
||||
into its output, adds decorations, hands off to NVENC, NVENC writes H.264
|
||||
into a pipe, `cfc-grid-ffmpeg` re-muxes the pipe → RTSP push to mediamtx.
|
||||
|
||||
## 3. Motion mode — the main operating mode
|
||||
|
||||
### 3.1 What happens
|
||||
|
||||
Each frame the composer:
|
||||
|
||||
1. Reads `last_motion_ms` for each camera (updated from Frigate MQTT
|
||||
`frigate/events`)
|
||||
2. Treats as "active" any camera with `(now - last_motion_ms) < motion_ttl_ms`
|
||||
(default **45 seconds**)
|
||||
3. Sorts active by `priority` (integer; higher = more important)
|
||||
4. Selects template from `templates.json` by **best-fit** rule:
|
||||
minimal template with `nb_camera_cells >= active_count`
|
||||
5. If template has more camera-cells than active — extra ones are filled
|
||||
with remaining drawable cameras from pool (by priority)
|
||||
6. Applies **asymmetric hysteresis**: growth in active count switches
|
||||
layout immediately, shrinkage waits 3 seconds (to avoid flicker)
|
||||
|
||||
### 3.2 What "drawable" means
|
||||
|
||||
A camera is **excluded** from pool if its `cfc_source_state_t` =
|
||||
`CONNECTING`, `DISCONNECTED` or `DEAD` (cuframes publisher silent for
|
||||
longer than `dead_threshold_ms`, default 5 seconds).
|
||||
|
||||
`STALE` (frames arrive infrequently) — counts, last available frame is
|
||||
drawn.
|
||||
|
||||
### 3.3 Manual override via PTZ
|
||||
|
||||
In the TV, ONVIF PTZ presets list template names (`tpl_1, tpl_3, tpl_4,
|
||||
..., tpl_16`). Pressing `GotoPreset` or movement keys:
|
||||
|
||||
- Applies the chosen layout immediately
|
||||
- **Freezes** motion-mode for 60 seconds
|
||||
- After expiry — returns to auto mode
|
||||
|
||||
ContinuousMove (arrows): pan/tilt cycles through preset list, zoom-in =
|
||||
`tpl_1` (full screen), zoom-out = `tpl_16` (4×4 grid).
|
||||
|
||||
## 4. templates.json — screen layout
|
||||
|
||||
### 4.1 Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"grid_cols": 8,
|
||||
"grid_rows": 8,
|
||||
"templates": [
|
||||
{
|
||||
"name": "tpl_N",
|
||||
"_desc": "Description",
|
||||
"priority": 0,
|
||||
"cells": [
|
||||
{
|
||||
"col": 0, "row": 0,
|
||||
"cs": 4, "rs": 4,
|
||||
"role": "camera",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"col": 6, "row": 0,
|
||||
"cs": 2, "rs": 6,
|
||||
"role": "widget",
|
||||
"widget": "temp_chart"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Grid: 8×8 microcells** (240×135 px each = 16:9 on 1920×1080 output).
|
||||
Any N×N square of microcells is also 16:9.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `col`, `row` | Top-left corner of cell in microcells (0..7) |
|
||||
| `cs`, `rs` | Size in microcells |
|
||||
| `role` | `camera` or `widget` |
|
||||
| `order` | For `camera`: placement order for active cameras (0 = main, usually the largest cell) |
|
||||
| `widget` | For `widget`: placeholder name (caption text) |
|
||||
|
||||
### 4.2 Best-fit selection
|
||||
|
||||
The composer selects a template for current active count:
|
||||
|
||||
```
|
||||
candidates = [t for t in templates if t.nb_camera_cells >= n_active]
|
||||
pick = min(candidates, key=lambda t: t.nb_camera_cells - n_active)
|
||||
# On tie: higher priority wins
|
||||
```
|
||||
|
||||
If active count exceeds the largest template's cells — the largest is
|
||||
taken, extra cameras are dropped (lowest priority).
|
||||
|
||||
### 4.3 Built-in templates
|
||||
|
||||
By default 9 templates in `/opt/templates.json`:
|
||||
|
||||
| Name | Cells | Description |
|
||||
|---|---|---|
|
||||
| `tpl_1` | 1 cam | One camera fullscreen |
|
||||
| `tpl_3` | 3 cam + 2 widgets | Main 1440×810 + 2 previews + 2 widget areas |
|
||||
| `tpl_4` | 4 cam | Quad 2×2, 960×540 each |
|
||||
| `tpl_5` | 5 cam + 1 widget | Main + 4 previews stacked right + widget bottom |
|
||||
| `tpl_6` | 6 cam + 1 widget | Main + 3 right + 2 bottom + widget |
|
||||
| `tpl_7` | 7 cam + 1 widget | Main + 3 right + 3 bottom + widget |
|
||||
| `tpl_8` | 8 cam (1+3+4) | Main + 3 right + 4 bottom row |
|
||||
| `tpl_9` | 9 cam + 2 widgets | 3×3 mains + widget strips |
|
||||
| `tpl_16` | 16 cam | 4×4 grid, 480×270 each |
|
||||
|
||||
Details — see `docker/templates.json` in the repo.
|
||||
|
||||
### 4.4 Adding your own template
|
||||
|
||||
1. Open `docker/templates.json` (or mounted override)
|
||||
2. Add a block to `"templates": [...]` per schema above
|
||||
3. Restart cfc-grid (or invoke ZMQ when hot-reload is available — Phase 12)
|
||||
|
||||
### 4.5 Coordinate math
|
||||
|
||||
Each microcell = `1920/8 = 240 px` wide, `1080/8 = 135 px` tall (16:9).
|
||||
Cell `{col=2, row=4, cs=4, rs=2}`:
|
||||
|
||||
- pixel x = `2 * 240 = 480`
|
||||
- pixel y = `4 * 135 = 540`
|
||||
- pixel w = `4 * 240 = 960`
|
||||
- pixel h = `2 * 135 = 270`
|
||||
|
||||
Aspect cell = `cs/rs * 16/9`. For 16:9 cells: **cs == rs**.
|
||||
|
||||
## 5. mqtt_overlays.json — text overlays from MQTT
|
||||
|
||||
### 5.1 Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"overlays": [
|
||||
{
|
||||
"id": "temp_outside",
|
||||
"topic": "zigbee2mqtt/Outdoor temp sensor",
|
||||
"json_field": "temperature",
|
||||
"format": "%+.1f°C",
|
||||
"anchor": "right-bottom",
|
||||
"margin_x": 32, "margin_y": 24,
|
||||
"pixel_size": 32,
|
||||
"color": [255, 255, 255],
|
||||
"alpha": 230,
|
||||
"bg_alpha": 160,
|
||||
"bg_y": 16, "bg_u": 128, "bg_v": 128,
|
||||
"bg_pad": 10,
|
||||
"placeholder": "—",
|
||||
"font_path": "/fonts/DejaVuSans-Bold.ttf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `id` | Unique overlay identifier (used by ZMQ for lookup) |
|
||||
| `topic` | MQTT topic to subscribe (via `cctv-mosquitto`) |
|
||||
| `json_field` | If payload is JSON — field name to extract; **empty = raw payload as string** |
|
||||
| `format` | `printf`-style for the formatted value (e.g. `"%+.1f°C"`, `"%s"`) |
|
||||
| `anchor` | Positioning anchor: `right-bottom`, `right-top`, `left-bottom`, `left-top`, `center` |
|
||||
| `margin_x`, `margin_y` | Offset from nearest screen edge (px) |
|
||||
| `pixel_size` | Font size in pixels |
|
||||
| `color` | RGB text color |
|
||||
| `alpha` | Overall text opacity (0..255) |
|
||||
| `bg_alpha` | Background opacity (0..255); **0 = no background** |
|
||||
| `bg_y`, `bg_u`, `bg_v` | BT.709 limited-range background color; default black |
|
||||
| `bg_pad` | Background padding around text (px) |
|
||||
| `placeholder` | What to show before first MQTT message; empty = "—" |
|
||||
| `font_path` | Path to font (.ttf/.otf) inside container |
|
||||
|
||||
### 5.2 Supported symbols
|
||||
|
||||
Font `DejaVuSans-Bold.ttf` — standard from `fonts-dejavu` package.
|
||||
**Covers Basic Multilingual Plane** (latin, cyrillic, basic symbols),
|
||||
including:
|
||||
|
||||
- `❯` (U+276F), `✎` (U+270E), `➤` (U+27A4), `→` (U+2192)
|
||||
- `★` (U+2605), `▶` (U+25B6), `✉` (U+2709)
|
||||
|
||||
**Emoji from Supplementary Multilingual Plane (>U+10000)** — e.g.
|
||||
`🗣` (U+1F5E3), `🤖` (U+1F916), `💬` (U+1F4AC) — **are not rendered**:
|
||||
font lacks those glyphs. Placeholder square is drawn instead.
|
||||
|
||||
To add color emoji — bind Noto Color Emoji and extend renderer for
|
||||
COLR/CPAL/SBIX (see developer.md).
|
||||
|
||||
### 5.3 Examples
|
||||
|
||||
**Plain number**: payload = `"23.5"` (raw), `json_field: ""`,
|
||||
`format: "%s°"`
|
||||
|
||||
**JSON with field**: payload = `{"temperature": 23.5, "humidity": 45}`,
|
||||
`json_field: "temperature"`, `format: "%+.1f°C"`
|
||||
|
||||
**Multiple overlays** stacked top-right: first with `margin_y: 24`,
|
||||
second `margin_y: 72`, third `margin_y: 120`.
|
||||
|
||||
### 5.4 How to add
|
||||
|
||||
1. Open `docker/mqtt_overlays.json` (or mounted override)
|
||||
2. Add block to `"overlays": [...]` array
|
||||
3. Restart cfc-grid
|
||||
|
||||
Hot-reload via ZMQ — Phase 12 (`reload_overlays` verb).
|
||||
|
||||
## 6. Composer CLI flags
|
||||
|
||||
In compose override `docker/cctv/cuframes-composer/docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- "--out=/out/grid.h264" # named pipe for ffmpeg-relay
|
||||
- "--fps=25"
|
||||
- "--bitrate=6000" # kbps
|
||||
- "--width=1920"
|
||||
- "--height=1080"
|
||||
- "--intra-refresh" # instead of IDR-bursts (low-latency)
|
||||
- "--control=tcp://0.0.0.0:5599" # ZMQ control plane
|
||||
- "--mqtt=cctv-mosquitto:1883" # MQTT for health publishing
|
||||
- "--mqtt-instance=cfc-grid"
|
||||
- "--mqtt-user=composer"
|
||||
- "--mqtt-pass=${COMPOSER_MQTT_PASSWORD}"
|
||||
|
||||
# Sources (cameras) — repeatable
|
||||
- "--source=cam-parking,frigate=parking_overview,priority=100,zones=parking_zone:canopy:private_area"
|
||||
- "--source=cam-back_yard,frigate=back_yard,priority=70"
|
||||
# ...
|
||||
|
||||
- "--motion-mode" # enable auto-layout
|
||||
- "--motion-ttl=45000" # ms
|
||||
|
||||
- "--templates=/opt/templates.json"
|
||||
- "--mqtt-overlays=/opt/mqtt_overlays.json"
|
||||
|
||||
# Frigate motion-driver and detection boxes
|
||||
- "--frigate-mqtt=cctv-mosquitto:1883"
|
||||
- "--frigate-topic=frigate/events"
|
||||
- "--detection-cell=parking,parking_overview,0,0,960,540,640,480,parking_zone:canopy:private_area"
|
||||
```
|
||||
|
||||
### 6.1 `--source` syntax
|
||||
|
||||
```
|
||||
--source=<cuframes_key>,frigate=<camera>,priority=<N>[,zones=<z1>:<z2>:...]
|
||||
```
|
||||
|
||||
- `cuframes_key` — name of cuframes publisher (e.g. `cam-parking`)
|
||||
- `frigate=NAME` — camera name in Frigate (for motion event matching)
|
||||
- `priority=N` — integer, higher = more important
|
||||
- `zones=...` — optional whitelist of Frigate zones; motion counts
|
||||
only if `event.current_zones` intersects the list (filters street flood)
|
||||
|
||||
### 6.2 `--detection-cell` syntax
|
||||
|
||||
```
|
||||
--detection-cell=<key>,<frigate_camera>,<x>,<y>,<w>,<h>,<detect_w>,<detect_h>[,zones]
|
||||
```
|
||||
|
||||
- `key` — arbitrary overlay identifier for logs
|
||||
- `frigate_camera` — Frigate name (for `event.camera` matching)
|
||||
- `x,y,w,h` — initial geometry (composer recalculates dynamically)
|
||||
- `detect_w,detect_h` — Frigate detector resolution (e.g. 640×480)
|
||||
- `zones` — bbox whitelist
|
||||
|
||||
## 7. ZMQ control plane
|
||||
|
||||
Default endpoint: `tcp://192.168.88.23:5599`. All verbs — JSON request/reply.
|
||||
|
||||
### 7.1 Verb list
|
||||
|
||||
| Command | Parameters | What it does |
|
||||
|---|---|---|
|
||||
| `ping` | — | Health check |
|
||||
| `health` | — | `{total, active, stale, dead}` over pool |
|
||||
| `set_text` | `id, text, r, g, b, x, y, visible` | Update text overlay (for CLI `--text=...`) |
|
||||
| `set_visible` | `id, visible` | Hide/show overlay |
|
||||
| `list_overlays` | — | List of overlays |
|
||||
| `set_layout` | `name` | Apply named template (manual override 60s in motion-mode) |
|
||||
| `list_layouts` | — | List of templates with cells |
|
||||
| `get_layout` | — | Name of current template |
|
||||
| `set_motion_mode` | `on, ttl_ms` | Toggle motion mode |
|
||||
| `get_motion_mode` | — | Motion mode state |
|
||||
| `get_template` | `name` | Full template JSON |
|
||||
| `reload_templates` | `path?` | Reload templates from file |
|
||||
|
||||
### 7.2 Example
|
||||
|
||||
```bash
|
||||
python3 <<EOF
|
||||
import zmq, json
|
||||
s = zmq.Context().socket(zmq.REQ)
|
||||
s.connect("tcp://192.168.88.23:5599")
|
||||
s.send_json({"cmd": "list_layouts"})
|
||||
print(json.dumps(s.recv_json(), indent=2, ensure_ascii=False))
|
||||
EOF
|
||||
```
|
||||
|
||||
Plain `nc` **does not work** — REP socket expects ZMQ wire-protocol.
|
||||
Use `zmq` Python/Go/JS or `mosquitto_pub` via RPC-bridge (Phase 12).
|
||||
|
||||
## 8. ONVIF and PTZ
|
||||
|
||||
`cctv-onvif` service binds to host network, responds to WS-Discovery
|
||||
multicast (239.255.255.250:3702) and SOAP requests over HTTP `:8085`.
|
||||
|
||||
### 8.1 Adding to TV
|
||||
|
||||
In the client (TV / IP-CamViewer / Synology / Frigate):
|
||||
|
||||
- ONVIF host: `192.168.88.23`
|
||||
- Port: `8085`
|
||||
- User / Password: empty (auth not configured)
|
||||
|
||||
WS-Discovery in LAN 192.168.88.0/24 finds device `cfc-grid (Goldix)`.
|
||||
RTSP URL is automatic — `rtsp://192.168.88.23:554/cfc-grid`.
|
||||
|
||||
### 8.2 PTZ presets
|
||||
|
||||
List (`GetPresets`): `tpl_1, tpl_3, tpl_4, tpl_5, tpl_6, tpl_7, tpl_8,
|
||||
tpl_9, tpl_16`.
|
||||
|
||||
GotoPreset(name) → composer applies template + freezes motion-mode for
|
||||
60 seconds → auto-return.
|
||||
|
||||
### 8.3 PTZ movement (ContinuousMove)
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| Pan right / Tilt down | Next template in list |
|
||||
| Pan left / Tilt up | Previous |
|
||||
| Zoom in (+) | `tpl_1` (full screen) |
|
||||
| Zoom out (−) | `tpl_16` (4×4 grid) |
|
||||
|
||||
## 9. Where to view RTSP
|
||||
|
||||
| Method | URL |
|
||||
|---|---|
|
||||
| VLC / mpv / ffplay | `rtsp://192.168.88.23:554/cfc-grid` |
|
||||
| Browser (HLS) | `http://192.168.88.23:8888/cfc-grid` |
|
||||
| WebRTC | `http://192.168.88.23:8889/cfc-grid` |
|
||||
| OBS / FFmpeg input | `rtsp://192.168.88.23:554/cfc-grid` |
|
||||
| ONVIF clients | via WS-Discovery (see §8) |
|
||||
|
||||
## 10. Known limitations
|
||||
|
||||
- **Color emoji not rendered** (needs Noto Color Emoji + COLR/CPAL
|
||||
support in text renderer — Phase 12)
|
||||
- **Hot-reload `mqtt_overlays.json`** — no ZMQ verb, requires cfc-grid
|
||||
restart
|
||||
- **Per-overlay broker** — all MQTT overlays use the common broker
|
||||
(set as `--mqtt`); subscribing to a foreign broker separately —
|
||||
not yet
|
||||
- **Widget rendering** — placeholder (dark rect + label), real widgets
|
||||
(graph, chat) — Phase 12+
|
||||
|
||||
## 11. FAQ
|
||||
|
||||
**Q: TV shows old layout names (`quad`, `dual_horizontal`). What to do?**
|
||||
|
||||
A: TV cached ONVIF presets. In the client, delete the camera and add
|
||||
again — it will re-read `GetPresets` with current names.
|
||||
|
||||
**Q: Parking camera is DEAD but logs show active=3. Why?**
|
||||
|
||||
A: `cfc_composer_get_health` shows **pool-wide** state, but motion-active
|
||||
counts by `last_motion_ms` independently of source state. DEAD is excluded
|
||||
in `is_camera_drawable()` inside `compose_motion_relayout`.
|
||||
|
||||
**Q: Pressed PTZ on TV, layout switched, but reverted after a minute.**
|
||||
|
||||
A: This is by design — `set_layout` in motion-mode freezes auto for
|
||||
60 seconds (`manual_override_duration_ms`). To pin layout permanently —
|
||||
disable motion-mode entirely via ZMQ:
|
||||
|
||||
```json
|
||||
{"cmd": "set_motion_mode", "on": 0}
|
||||
```
|
||||
|
||||
**Q: I want text overlay backgrounds in different colors.**
|
||||
|
||||
A: `bg_y/bg_u/bg_v` fields accept BT.709 limited-range. For red — Y≈80,
|
||||
U≈90, V≈240. For cyan — Y≈170, U≈170, V≈100. Calculator:
|
||||
https://www.rapidtables.com/convert/color/rgb-to-yuv.html
|
||||
(use BT.709 limited).
|
||||
|
||||
**Q: With motion on 5 cameras, the layout doesn't change, stays at quad.**
|
||||
|
||||
A: Check `docker logs cfc-grid | grep "loaded N templates"` — must be ≥5
|
||||
(should include `tpl_5..tpl_8`, `tpl_9`, `tpl_16`). If not — templates.json
|
||||
didn't load (check syntax via `jq` or `python3 -m json.tool`).
|
||||
|
||||
**Q: Frigate bbox is drawn on the wrong camera.**
|
||||
|
||||
A: Check `--detection-cell` — `frigate_camera` must match
|
||||
`event.after.camera`. Composer binds detbox-overlay to pool-entry via
|
||||
`frigate_camera` (see `cfc_composer::pool::by_frigate_camera`).
|
||||
|
||||
## 12. Next
|
||||
|
||||
- [developer.md](developer.md) — internals, extension
|
||||
- [operations.md](operations.md) — build, deploy, troubleshooting
|
||||
- repo README: brief overview
|
||||
@@ -0,0 +1,615 @@
|
||||
# cfc-grid — руководство разработчика
|
||||
|
||||
> Аудитория: разработчик который правит C++ код композитора, добавляет
|
||||
> новые типы Cell/Decoration/overlay-type, или меняет логику auto-layout.
|
||||
>
|
||||
> Если ты пользователь — см. [user.md](user.md). Если занимаешься
|
||||
> deploy/troubleshooting — см. [operations.md](operations.md).
|
||||
|
||||
## 1. Архитектура (40000-foot view)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ cfc::Composer (C++) │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │SourcePool│ │ Layout │ │OutputSurface │ │
|
||||
│ │ (pool of │ │ vector< │ │ CudaBuffer │ │
|
||||
│ │ cfc_ │ │ unique_ │ │ (VMM NV12) │ │
|
||||
│ │source_t*)│ │ ptr<Cell>│ │ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────┘ │
|
||||
│ ↓ ↓ ↑ │
|
||||
│ motion_pulse apply() NVENC │
|
||||
└──────────────────────────────────────────────┘
|
||||
↑ ↓
|
||||
extern "C" ABI shim H.264 pipe
|
||||
(composer_c_api.cpp, ↓
|
||||
layouts_c_api.cpp, ffmpeg-relay
|
||||
mqtt_overlay_c_api.cpp) ↓
|
||||
↑ mediamtx
|
||||
┌────────┴────────┐ ↓
|
||||
│ │ ↓
|
||||
grid_record.c control.c clients
|
||||
(C, CLI parsing) (ZMQ verbs)
|
||||
↑ ↑
|
||||
│ │
|
||||
frigate_mqtt.c health.c, audio.c, writer.c, source.c
|
||||
(Frigate events) (C-only modules)
|
||||
```
|
||||
|
||||
**Принципы:**
|
||||
|
||||
- **C++17 host-side**, CUDA kernels остались на C
|
||||
- **Zero-copy:** output VMM-буфер передаётся как `NV12Ref` всем
|
||||
cells/decorations (read-write reference, не копия)
|
||||
- **Coexistence:** C-модули (`source.c`, `nvenc.c`, `frigate_mqtt.c`,
|
||||
`health.c`, `writer.c`, `audio.c`, `overlay.c`) сохранены, доступ через
|
||||
`extern "C"`. Только `composer.c` и `layouts.c` удалены (заменены C++ +
|
||||
ABI shim).
|
||||
- **`grid_record.c`** (CLI entry-point) остался на C, использует
|
||||
публичный `cfc_composer_*` API без правок
|
||||
|
||||
## 2. Дерево исходников
|
||||
|
||||
```
|
||||
include/cuframes_composer/
|
||||
├── *.h # Публичный C API (extern "C")
|
||||
│ ├── composer.h # cfc_composer_* ABI
|
||||
│ ├── layouts.h # cfc_layout_* ABI
|
||||
│ ├── overlay.h # cfc_overlay_* (text/png/border/detbox)
|
||||
│ ├── source.h, nvenc.h, frigate_mqtt.h, ...
|
||||
│ └── ...
|
||||
└── cpp/ # C++ headers — публичные классы
|
||||
├── cuda_raii.hpp # CudaBuffer, CudaStream (RAII)
|
||||
├── types.hpp # Rect, NV12Ref
|
||||
├── cell.hpp # абстрактный Cell + draw()
|
||||
├── camera_cell.hpp # CameraCell : Cell
|
||||
├── widget_cell.hpp # WidgetCell : Cell
|
||||
├── blank_cell.hpp # BlankCell : Cell
|
||||
├── decoration.hpp # абстрактный Decoration
|
||||
├── border_decoration.hpp # BorderDecoration
|
||||
├── label_decoration.hpp # LabelDecoration (FreeType internal)
|
||||
├── template.hpp # LayoutTemplate, CellTemplate, kGridCols/Rows
|
||||
├── template_loader.hpp # JSON parser, current_templates()
|
||||
├── source_pool.hpp # SourcePool, PoolEntry
|
||||
├── layout.hpp # Layout
|
||||
├── composer.hpp # Composer (главный класс)
|
||||
└── mqtt_overlay.hpp # MqttOverlayItem + Manager
|
||||
|
||||
src/
|
||||
├── *.c # C-модули (source/nvenc/health/...)
|
||||
├── overlay.c # Все типы cfc_overlay_t
|
||||
├── frigate_mqtt.c # Subscribe + motion_pulse + zone-filter
|
||||
├── cugrid/cugrid.cu # CUDA kernels (resize, fill, blit)
|
||||
└── cpp/ # C++ реализации
|
||||
├── camera_cell.cpp, widget_cell.cpp, blank_cell.cpp
|
||||
├── border_decoration.cpp, label_decoration.cpp
|
||||
├── source_pool.cpp
|
||||
├── template_loader.cpp
|
||||
├── layout.cpp
|
||||
├── composer.cpp
|
||||
├── composer_c_api.cpp # extern "C" shim для composer
|
||||
├── layouts_c_api.cpp # extern "C" shim для layouts
|
||||
├── mqtt_overlay.cpp
|
||||
└── mqtt_overlay_c_api.cpp
|
||||
|
||||
examples/
|
||||
├── simple_record.c # 1-источник → H.264 в файл
|
||||
├── grid_record.c # Main CLI entry-point (C)
|
||||
└── grid_record_cpp.cpp # C++ smoke (использует cfc::Composer напрямую)
|
||||
```
|
||||
|
||||
## 3. Жизненный цикл одного кадра
|
||||
|
||||
```cpp
|
||||
// В compose loop (grid_record.c, через cfc_composer_compose):
|
||||
NV12Ref ref = composer.compose_frame();
|
||||
// 1. maybe_relayout(): motion-mode? best-fit template; apply Layout
|
||||
// 2. compose_clear(): fill output буфера BT.709 black
|
||||
// 3. Layout::render(): for each cell: draw_content() + decorations[]
|
||||
// 4. for each overlay: sync detbox geom + cfc_overlay_draw()
|
||||
// cudaStreamSynchronize(0);
|
||||
// nvenc.encode(ref.y_ptr, ref.pitch_y, ref.uv_ptr, ref.pitch_uv, ...);
|
||||
```
|
||||
|
||||
Все операции выполняются на CUDA default stream. Zero-copy:
|
||||
`ref.y_ptr` / `ref.uv_ptr` — те же `CUdeviceptr`, что внутри
|
||||
`composer.output_` (RAII `CudaBuffer`).
|
||||
|
||||
## 4. Cell — иерархия и расширение
|
||||
|
||||
### 4.1 Абстракция
|
||||
|
||||
```cpp
|
||||
class Cell {
|
||||
public:
|
||||
explicit Cell(const Rect& geom);
|
||||
virtual ~Cell() = default;
|
||||
Cell(const Cell&) = delete;
|
||||
|
||||
const Rect& geometry() const noexcept;
|
||||
void set_geometry(const Rect& r) noexcept;
|
||||
void add_decoration(std::unique_ptr<Decoration>);
|
||||
|
||||
void draw(CUstream stream, NV12Ref& dst); // public — calls draw_content() + decorations
|
||||
|
||||
protected:
|
||||
virtual void draw_content(CUstream stream, NV12Ref& dst) = 0;
|
||||
Rect geom_;
|
||||
std::vector<std::unique_ptr<Decoration>> decorations_;
|
||||
};
|
||||
```
|
||||
|
||||
`Cell::draw()` финальный (не виртуальный — final pattern через NVI):
|
||||
```cpp
|
||||
void Cell::draw(CUstream stream, NV12Ref& dst) {
|
||||
if (geom_.empty()) return;
|
||||
draw_content(stream, dst); // subclass renders content
|
||||
for (auto& d : decorations_) {
|
||||
d->draw(stream, dst, geom_); // overlay decorations on top
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Существующие подклассы
|
||||
|
||||
| Класс | draw_content реализация |
|
||||
|---|---|
|
||||
| `CameraCell` | `cfc_source_get_latest()` + `cfc_cugrid_resize_nv12` |
|
||||
| `WidgetCell` | `cfc_cugrid_fill_nv12` тёмно-серым Y=40 (placeholder) |
|
||||
| `BlankCell` | `cfc_cugrid_fill_nv12` BT.709 черным Y=16 |
|
||||
|
||||
### 4.3 Как добавить новый тип Cell
|
||||
|
||||
Пример: `GraphCell` — рисует scrolling-chart из подписки на MQTT.
|
||||
|
||||
1. Создать `include/cuframes_composer/cpp/graph_cell.hpp`:
|
||||
|
||||
```cpp
|
||||
#ifndef CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP
|
||||
#define CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP
|
||||
#include "cell.hpp"
|
||||
#include <deque>
|
||||
|
||||
namespace cfc {
|
||||
|
||||
class GraphCell : public Cell {
|
||||
public:
|
||||
GraphCell(const Rect& geom, std::size_t max_points = 60)
|
||||
: Cell(geom), max_points_(max_points) {}
|
||||
|
||||
void push_value(double v) {
|
||||
if (values_.size() >= max_points_) values_.pop_front();
|
||||
values_.push_back(v);
|
||||
}
|
||||
|
||||
protected:
|
||||
void draw_content(CUstream stream, NV12Ref& dst) override;
|
||||
|
||||
private:
|
||||
std::deque<double> values_;
|
||||
std::size_t max_points_;
|
||||
};
|
||||
|
||||
} // namespace cfc
|
||||
#endif
|
||||
```
|
||||
|
||||
2. Реализация `src/cpp/graph_cell.cpp`:
|
||||
|
||||
```cpp
|
||||
#include "../../include/cuframes_composer/cpp/graph_cell.hpp"
|
||||
#include "../../include/cuframes_composer/cugrid.h"
|
||||
|
||||
namespace cfc {
|
||||
|
||||
void GraphCell::draw_content(CUstream stream, NV12Ref& dst) {
|
||||
if (geom_.empty() || values_.empty()) return;
|
||||
|
||||
// 1. BG fill
|
||||
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
|
||||
geom_.x, geom_.y, geom_.w, geom_.h,
|
||||
/*Y*/ 30, 128, 128, 255);
|
||||
|
||||
// 2. Compute min/max
|
||||
double mn = *std::min_element(values_.begin(), values_.end());
|
||||
double mx = *std::max_element(values_.begin(), values_.end());
|
||||
if (mx == mn) mx = mn + 1.0;
|
||||
|
||||
// 3. Render line as thin filled rects (lazy, без kernel'я для bitmap'а)
|
||||
int n = static_cast<int>(values_.size());
|
||||
int dx = geom_.w / n;
|
||||
for (int i = 0; i < n; i++) {
|
||||
double norm = (values_[i] - mn) / (mx - mn);
|
||||
int y_px = geom_.y + geom_.h - 2 - (int)(norm * (geom_.h - 4));
|
||||
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
|
||||
geom_.x + i * dx, y_px, dx & ~1, 2,
|
||||
/*Y*/ 235, 128, 64, 255); // limited-range bright
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cfc
|
||||
```
|
||||
|
||||
3. Добавить в `src/CMakeLists.txt`:
|
||||
```cmake
|
||||
set(COMPOSER_SOURCES_CPP
|
||||
...
|
||||
cpp/graph_cell.cpp
|
||||
)
|
||||
```
|
||||
|
||||
4. Использовать в `Layout::apply()`:
|
||||
|
||||
```cpp
|
||||
// В layout.cpp, в обработке widget cells:
|
||||
if (wt->widget == "graph_temp") {
|
||||
cells_.push_back(std::make_unique<GraphCell>(r, /*max_points=*/120));
|
||||
} else {
|
||||
cells_.push_back(std::make_unique<WidgetCell>(r, wt->widget));
|
||||
}
|
||||
```
|
||||
|
||||
5. Подключить MQTT → `GraphCell::push_value()` — либо через MqttOverlayManager
|
||||
расширение, либо через `cfc::Composer::register_graph_feed()` (новый API).
|
||||
|
||||
## 5. Decoration — композиция в Cell
|
||||
|
||||
### 5.1 Абстракция
|
||||
|
||||
```cpp
|
||||
class Decoration {
|
||||
public:
|
||||
virtual ~Decoration() = default;
|
||||
virtual void draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
Decoration знает только pixel-rect parent-cell'а — позиционируется
|
||||
**относительно него**. Не имеет access к Layout/Composer.
|
||||
|
||||
### 5.2 Существующие подклассы
|
||||
|
||||
- **`LabelDecoration`** — FreeType atlas + `cfc_cugrid_blit_rgba_nv12`.
|
||||
Persistent VRAM (rebuild при `set_text`). Поддерживает background fill
|
||||
(через `cfc_overlay_text_config_t.bg_*` параметры).
|
||||
- **`BorderDecoration`** — 4 вызова `cfc_cugrid_fill_nv12` (top, bottom,
|
||||
left, right rect'ы по `thickness`).
|
||||
|
||||
### 5.3 Как добавить новый тип Decoration
|
||||
|
||||
Пример: `BadgeDecoration` — иконка в углу (например красная точка
|
||||
«recording»).
|
||||
|
||||
1. Header `badge_decoration.hpp`:
|
||||
|
||||
```cpp
|
||||
class BadgeDecoration : public Decoration {
|
||||
public:
|
||||
struct Style {
|
||||
int corner = 0; // 0=top-left, 1=top-right, 2=bottom-right, 3=bottom-left
|
||||
int size = 12;
|
||||
int margin = 8;
|
||||
int color_y = 80, color_u = 90, color_v = 240; // red-ish
|
||||
int alpha = 240;
|
||||
bool visible = true;
|
||||
};
|
||||
explicit BadgeDecoration(const Style& s) : style_(s) {}
|
||||
void set_visible(bool v) noexcept { style_.visible = v; }
|
||||
void draw(CUstream stream, NV12Ref& dst, const Rect& p) override;
|
||||
private:
|
||||
Style style_;
|
||||
};
|
||||
```
|
||||
|
||||
2. Реализация:
|
||||
|
||||
```cpp
|
||||
void BadgeDecoration::draw(CUstream s, NV12Ref& dst, const Rect& p) {
|
||||
if (!style_.visible || style_.alpha <= 0) return;
|
||||
int x, y;
|
||||
switch (style_.corner) {
|
||||
case 0: x = p.x + style_.margin; y = p.y + style_.margin; break;
|
||||
case 1: x = p.x + p.w - style_.size - style_.margin; y = p.y + style_.margin; break;
|
||||
case 2: x = p.x + p.w - style_.size - style_.margin; y = p.y + p.h - style_.size - style_.margin; break;
|
||||
case 3: x = p.x + style_.margin; y = p.y + p.h - style_.size - style_.margin; break;
|
||||
}
|
||||
x &= ~1; y &= ~1;
|
||||
cfc_cugrid_fill_nv12(s, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
|
||||
x, y, style_.size, style_.size,
|
||||
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
|
||||
}
|
||||
```
|
||||
|
||||
3. Подключать в `Layout::apply()`:
|
||||
|
||||
```cpp
|
||||
if (cc->source()->state() == CFC_SOURCE_STALE) {
|
||||
BadgeDecoration::Style bs;
|
||||
bs.corner = 1; bs.color_y = 180; bs.color_v = 100; // yellow
|
||||
cell->add_decoration(std::make_unique<BadgeDecoration>(bs));
|
||||
}
|
||||
```
|
||||
|
||||
## 6. SourcePool и motion-state
|
||||
|
||||
```cpp
|
||||
struct PoolEntry {
|
||||
std::string cuframes_key;
|
||||
std::string frigate_camera;
|
||||
int priority;
|
||||
cfc_source_t* source;
|
||||
std::atomic<int64_t> last_motion_ms;
|
||||
std::vector<std::string> required_zones;
|
||||
|
||||
cfc_source_state_t state() const; // вызывает cfc_source_get_latest
|
||||
bool drawable() const; // ACTIVE || STALE
|
||||
};
|
||||
|
||||
class SourcePool {
|
||||
public:
|
||||
int add(key, frigate_camera, priority, zones, SubscribeOpts);
|
||||
PoolEntry* by_key(const std::string&);
|
||||
PoolEntry* by_frigate_camera(const std::string&);
|
||||
template<typename F> void for_each(F&&);
|
||||
void motion_pulse(const std::string& frigate_camera,
|
||||
const std::vector<std::string>& current_zones);
|
||||
};
|
||||
```
|
||||
|
||||
`motion_pulse` вызывается из `frigate_mqtt.c` при каждом event. Если
|
||||
`required_zones` непустой — match по intersection с `event.current_zones`,
|
||||
иначе принимаем всё.
|
||||
|
||||
## 7. Auto-layout алгоритмы
|
||||
|
||||
См. `cfc::Composer::maybe_relayout()` (src/cpp/composer.cpp).
|
||||
|
||||
### 7.1 Best-fit selection
|
||||
|
||||
```cpp
|
||||
const LayoutTemplate* Composer::pick_best_fit(int need) const {
|
||||
const auto& reg = current_templates();
|
||||
const LayoutTemplate* best = nullptr;
|
||||
int best_waste = -1, best_prio = -1;
|
||||
for (auto& t : reg) {
|
||||
int n = t.nb_camera_cells();
|
||||
if (n < need) continue;
|
||||
int waste = n - need;
|
||||
if (!best || waste < best_waste ||
|
||||
(waste == best_waste && t.priority > best_prio)) {
|
||||
best = &t; best_waste = waste; best_prio = t.priority;
|
||||
}
|
||||
}
|
||||
if (best) return best;
|
||||
// overflow → largest
|
||||
for (auto& t : reg) {
|
||||
if (!best || t.nb_camera_cells() > best->nb_camera_cells()) best = &t;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Collect active
|
||||
|
||||
```cpp
|
||||
std::vector<PoolEntry*> Composer::collect_active() const {
|
||||
std::vector<PoolEntry*> active;
|
||||
int64_t now = now_ms_mono();
|
||||
pool_.for_each([&](PoolEntry& e) {
|
||||
if (!e.drawable()) return; // DEAD/CONNECTING — skip
|
||||
int64_t last = e.last_motion_ms.load();
|
||||
if (last == 0) return; // never had motion
|
||||
if (now - last > cfg_.motion_ttl_ms) return; // TTL expired
|
||||
active.push_back(&e);
|
||||
});
|
||||
// idle fallback: top-priority drawable
|
||||
if (active.empty()) {
|
||||
PoolEntry* best = nullptr;
|
||||
pool_.for_each([&](PoolEntry& e) {
|
||||
if (!e.drawable()) return;
|
||||
if (!best || e.priority > best->priority) best = &e;
|
||||
});
|
||||
if (best) active.push_back(best);
|
||||
}
|
||||
std::sort(active.begin(), active.end(),
|
||||
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
|
||||
return active;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Asymmetric hysteresis
|
||||
|
||||
Сигнатура `template_name + "|" + sorted_keys` запоминается. Если новый
|
||||
набор ⊇ committed (рост) → commit мгновенно. Иначе ждём
|
||||
`shrink_hysteresis_ms` (default 3000) → commit.
|
||||
|
||||
```cpp
|
||||
bool is_grow = std::includes(nkeys.begin(), nkeys.end(),
|
||||
ckeys.begin(), ckeys.end());
|
||||
if (is_grow && nkeys.size() == ckeys.size()) is_grow = false;
|
||||
if (is_grow) { /* commit */ } else {
|
||||
if (sig != pending_signature_) {
|
||||
pending_signature_ = sig;
|
||||
pending_first_seen_ms_ = now;
|
||||
}
|
||||
if (now - pending_first_seen_ms_ < cfg_.shrink_hysteresis_ms) return;
|
||||
/* commit */
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 Fill свободных cells
|
||||
|
||||
После выбора template и cap'а по `nb_camera_cells`:
|
||||
|
||||
```cpp
|
||||
if (active.size() < cap) {
|
||||
std::vector<PoolEntry*> extras;
|
||||
pool_.for_each([&](PoolEntry& e) {
|
||||
if (!e.drawable()) return;
|
||||
if (e ∈ already_active) return;
|
||||
extras.push_back(&e);
|
||||
});
|
||||
std::sort(extras.begin(), extras.end(), priority_desc);
|
||||
while (active.size() < cap && !extras.empty()) {
|
||||
active.push_back(extras.front());
|
||||
extras.erase(extras.begin());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.5 Manual PTZ override
|
||||
|
||||
`Composer::set_layout(name)` в motion-mode:
|
||||
```cpp
|
||||
manual_override_until_ms_ = now + manual_override_duration_ms_; // default 60s
|
||||
```
|
||||
|
||||
`maybe_relayout()` пропускает работу пока `now < manual_override_until_ms_`.
|
||||
По истечении — `committed_signature_.clear()` → форс relayout.
|
||||
|
||||
## 8. extern "C" ABI shim
|
||||
|
||||
`composer_c_api.cpp` — тонкая обёртка:
|
||||
|
||||
```cpp
|
||||
extern "C" int cfc_composer_create(const cfc_composer_config_t* cfg,
|
||||
cfc_composer_t** out)
|
||||
{
|
||||
cfc::ComposerConfig cpp_cfg = {/* ... */};
|
||||
auto* comp = new (std::nothrow) cfc::Composer(cpp_cfg);
|
||||
if (!comp->ok()) { delete comp; return -1; }
|
||||
// manual_cells из cfg->cells → set_manual_cells()
|
||||
*out = reinterpret_cast<cfc_composer_t*>(comp);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
`cfc_composer_t` — opaque в C, `reinterpret_cast` к/от `cfc::Composer*`
|
||||
в shim'е.
|
||||
|
||||
`layouts_c_api.cpp` — аналогично для `cfc_layout_*`. Держит static
|
||||
кеш `vector<cfc_layout_t>` который пересинхронизируется с
|
||||
`cfc::current_templates()` при reload.
|
||||
|
||||
## 9. Build
|
||||
|
||||
### 9.1 CMake
|
||||
|
||||
```cmake
|
||||
project(cuframes-composer LANGUAGES C CXX CUDA)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
```
|
||||
|
||||
`COMPOSER_SOURCES_CPP` в `src/CMakeLists.txt` перечисляет все .cpp.
|
||||
|
||||
### 9.2 Host build (для CI / dev-машины Ubuntu 24.04)
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/cuframes-composer
|
||||
mkdir -p build && cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### 9.3 Production build (Ubuntu 22.04 jammy)
|
||||
|
||||
См. [operations.md](operations.md). Кратко:
|
||||
```bash
|
||||
docker run --rm --gpus all \
|
||||
-v "$PWD":/src -w /src/build-jammy \
|
||||
cuframes-composer-builder:cached \
|
||||
bash -c 'cmake .. && make -j$(nproc)'
|
||||
```
|
||||
|
||||
## 10. Производительность
|
||||
|
||||
### 10.1 Гарантии zero-copy
|
||||
|
||||
- Один `CudaBuffer output_` на `Composer`, передаётся как `NV12Ref` всем
|
||||
cells/decorations
|
||||
- Cell не создаёт VRAM-allocation'ов на кадр (только `Decoration::draw`
|
||||
может делать FreeType-rebuild при `set_text` — это offline)
|
||||
- Layout::apply() пересоздаёт `vector<unique_ptr<Cell>>` только при
|
||||
смене template'а; обычно раз в N секунд
|
||||
|
||||
### 10.2 Virtual call overhead
|
||||
|
||||
`Cell::draw()` → `draw_content()` virtual call: 1 indirect call per cell
|
||||
per frame. При 25 fps × 16 cells = **400 calls/sec** — нерелевантно.
|
||||
|
||||
### 10.3 Hot path
|
||||
|
||||
CUDA kernels (`cugrid.cu`):
|
||||
- `fill_nv12` — 1 kernel launch на rect
|
||||
- `resize_nv12` — bilinear через 2 kernels (Y plane + UV plane)
|
||||
- `blit_rgba_nv12` — 1 kernel, RGBA → NV12 + alpha-blend
|
||||
|
||||
**Все Cell-операции конвертируются в N×fill_nv12 + 1×resize_nv12 +
|
||||
M×blit_rgba_nv12** — оптимизировать через batching пока не нужно
|
||||
(GPU не sticky на текущей нагрузке).
|
||||
|
||||
## 11. Где править распространённые задачи
|
||||
|
||||
| Хочу… | Файл |
|
||||
|---|---|
|
||||
| Изменить hysteresis | `composer.hpp` → `ComposerConfig::shrink_hysteresis_ms` |
|
||||
| Поменять цвет border'а cells | `layout.cpp` → `border_style.color_y/u/v` |
|
||||
| Поменять label-font | `mqtt_overlay.cpp` → `LabelDecoration` style + `LabelStyle::font_path` |
|
||||
| Добавить ZMQ-verb | `control.c::dispatch()` + новая `cmd_*` функция |
|
||||
| Поменять manual override длительность | `composer.hpp` → `manual_override_duration_ms_` |
|
||||
| Добавить новый MQTT-overlay anchor | `mqtt_overlay.cpp::reposition_overlay()` switch |
|
||||
| Поддержать color emoji | `overlay.c::text_rebuild_atlas()` — handle `FT_LOAD_COLOR` + bitmap BGRA → blit как RGBA |
|
||||
|
||||
## 12. Тестирование
|
||||
|
||||
### 12.1 Юнит (не реализован)
|
||||
|
||||
Запланировано: catch2-тесты для `pick_best_fit`, `collect_active`,
|
||||
hysteresis. Зависимость на CUDA — mock через `cfc_source_get_latest`
|
||||
шим.
|
||||
|
||||
### 12.2 Integration smoke (`examples/grid_record_cpp.cpp`)
|
||||
|
||||
Минимальный C++ smoke: init Composer, compose loop, dump NV12 → файл.
|
||||
Не использует NVENC (тестирует только композицию).
|
||||
|
||||
```bash
|
||||
build/examples/grid_record_cpp \
|
||||
--out=/tmp/dump.nv12 --frames=10 --width=1920 --height=1080 \
|
||||
--templates=docker/templates.json \
|
||||
--source=cam-parking,frigate=parking_overview,priority=100 \
|
||||
--motion-mode
|
||||
```
|
||||
|
||||
### 12.3 Production smoke
|
||||
|
||||
`docker logs cfc-grid | grep -E "loaded|template|motion|grow|shrink"` —
|
||||
живая телеметрия композитора.
|
||||
|
||||
## 13. Известные подводные камни
|
||||
|
||||
- **`cfc_overlay_t` не RAII** — managed через `cfc_composer_add_overlay` /
|
||||
`cfc_overlay_destroy` (Composer.cpp::~Composer уничтожает все).
|
||||
- **`pthread_mutex_t` в SourcePool** vs `std::mutex` — выбор `std::mutex`
|
||||
для C++ слоя, `pthread_mutex_t` для C-слоя (PoolEntry::last_motion_ms
|
||||
использует `std::atomic`).
|
||||
- **Compose loop НЕ blocking** на CUDA-операциях — `cudaStreamSynchronize`
|
||||
вызывается caller'ом (grid_record.c) перед NVENC.
|
||||
- **Frigate event может прийти ДО Layout::apply** — `motion_pulse`
|
||||
обновляет `last_motion_ms`, но cell для этой камеры ещё может не
|
||||
существовать. На следующем кадре `maybe_relayout` пересчитает.
|
||||
- **`dynamic_cast<CameraCell*>` в `Layout::find_camera_cell_rect`** —
|
||||
использует RTTI. Включён `-frtti` по умолчанию в g++/nvcc.
|
||||
|
||||
## 14. Workflow по изменению Phase-задач
|
||||
|
||||
1. Branch `feature/<phase>-<feature>` от `main`
|
||||
2. Реализация, host build PASS, jammy build PASS (см. operations.md)
|
||||
3. Bake image `gx/cuframes-composer:<phase>-stepN`
|
||||
4. Deploy на dev-target → smoke verify через VLC/logs
|
||||
5. Commit + push branch
|
||||
6. (Если нужно) — merge в `main` через `--no-ff` PR-style
|
||||
|
||||
Для multi-commit фазы: один WIP-merge commit на main с описанием
|
||||
ключевых изменений.
|
||||
@@ -0,0 +1,404 @@
|
||||
# cfc-grid — operations / deploy / troubleshooting
|
||||
|
||||
> Аудитория: тот кто билдит, деплоит, мониторит cfc-grid в проде.
|
||||
>
|
||||
> Если ты пользователь — см. [user.md](user.md). Если разработчик —
|
||||
> см. [developer.md](developer.md).
|
||||
|
||||
## 1. Production setup (R9-88.23)
|
||||
|
||||
### 1.1 Стек
|
||||
|
||||
```
|
||||
docker compose -f docker-compose.yml \
|
||||
-f cuda-grid/docker-compose.override.yml \
|
||||
-f cuframes-composer/docker-compose.override.yml \
|
||||
-f onvif/docker-compose.override.yml \
|
||||
up -d
|
||||
```
|
||||
|
||||
Файлы — в `localhost-infra/hosts/R9-88.23/docker/cctv/`.
|
||||
|
||||
| Сервис | Image | Назначение |
|
||||
|---|---|---|
|
||||
| `cuframes-ipc-anchor` | `gx/cuframes:0.4` | Shared VMM IPC anchor для cuframes |
|
||||
| `cuframes-pub-*` (parking/back_yard/front_yard/gate_lpr) | `gx/cuframes:0.4` | RTSP → cuframes per-camera publishers |
|
||||
| `cuda-grid-mediamtx` | `bluenviron/mediamtx` | RTSP/HLS/WebRTC gateway |
|
||||
| `cctv-mosquitto` | `eclipse-mosquitto` | MQTT broker (+bridge к 192.168.88.4) |
|
||||
| **`cfc-grid`** | `gx/cuframes-composer:0.11b-step1` | Композитор (главный сервис) |
|
||||
| `cfc-grid-ffmpeg` | `ffmpeg-vf-cuda-grid:phase4b-final` | H.264 pipe → RTSP push |
|
||||
| `cfc-grid-watchdog` | `gx/cuda-grid-watchdog:0.4` | Restart cfc-grid при stuck inboundBytes |
|
||||
| `cctv-onvif` | `gx/cctv-onvif:0.6` | ONVIF discovery + PTZ → ZMQ |
|
||||
| `cctv-frigate` | `ghcr.io/blakeblackshear/frigate` | Object detection → MQTT events |
|
||||
|
||||
### 1.2 Поток кадров
|
||||
|
||||
```
|
||||
cuframes-pub-X ──VMM──┐
|
||||
cuframes-pub-Y ──VMM──┼──→ cfc-grid (composer)
|
||||
cuframes-pub-Z ──VMM──┘ │
|
||||
│ H.264 NVENC
|
||||
↓ named pipe /tmp/cfc-pipe-dir/grid.h264
|
||||
cfc-grid-ffmpeg (re-mux)
|
||||
│ RTSP push
|
||||
↓
|
||||
cuda-grid-mediamtx
|
||||
rtsp://*/cfc-grid (TCP/UDP)
|
||||
http://*:8888/cfc-grid (HLS)
|
||||
http://*:8889/cfc-grid (WebRTC)
|
||||
```
|
||||
|
||||
### 1.3 Сети
|
||||
|
||||
- Внутренний docker network: `cctv`
|
||||
- Внешние порты на R9-88.23:
|
||||
- `554/tcp` — RTSP (mediamtx)
|
||||
- `8888/tcp` — HLS (mediamtx)
|
||||
- `8889/tcp` — WebRTC (mediamtx)
|
||||
- `5599/tcp` — ZMQ control plane composer'а
|
||||
- `8085/tcp` — ONVIF SOAP (cctv-onvif)
|
||||
- `3702/udp` — WS-Discovery multicast (cctv-onvif)
|
||||
|
||||
## 2. Build
|
||||
|
||||
### 2.1 Local host build (Ubuntu 24.04, dev машина)
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/cuframes-composer
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
Артефакты в `build/src/libcuframes_composer.so` и `build/examples/grid_record`.
|
||||
|
||||
**ВАЖНО:** host'овый бинарь (Ubuntu 24.04, glibc 2.39, libavformat60)
|
||||
**несовместим** с runtime контейнером (Ubuntu 22.04 jammy, glibc 2.35,
|
||||
libavformat58). См. memory `incremental-ffmpeg-rebuild`.
|
||||
|
||||
### 2.2 Jammy build (для production image)
|
||||
|
||||
Использует кешированный builder-контейнер `cuframes-composer-builder:cached`
|
||||
(Ubuntu 22.04 + nvidia/cuda:12.4.1-devel + apt-deps):
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/cuframes-composer
|
||||
|
||||
# Если builder ещё не закеширован:
|
||||
docker image inspect cuframes-composer-builder:cached >/dev/null 2>&1 || {
|
||||
docker run -d --name cfc-builder-tmp \
|
||||
nvidia/cuda:12.4.1-devel-ubuntu22.04 sleep 3600
|
||||
docker exec cfc-builder-tmp bash -c '
|
||||
apt-get update -qq && apt-get install -y -qq --no-install-recommends \
|
||||
build-essential cmake git pkg-config \
|
||||
libpng-dev libfreetype-dev \
|
||||
libzmq3-dev libjson-c-dev libmosquitto-dev \
|
||||
libavformat-dev libavcodec-dev libavutil-dev'
|
||||
docker commit cfc-builder-tmp cuframes-composer-builder:cached
|
||||
docker rm -f cfc-builder-tmp
|
||||
}
|
||||
|
||||
# Сам build:
|
||||
docker run --rm --gpus all -v "$PWD":/src -w /src/build-jammy \
|
||||
cuframes-composer-builder:cached \
|
||||
bash -c 'cmake -DCMAKE_BUILD_TYPE=Release .. && make -j$(nproc)'
|
||||
```
|
||||
|
||||
Артефакты в `build-jammy/`.
|
||||
|
||||
### 2.3 Bake image (incremental — без `docker build`)
|
||||
|
||||
Не используем `docker build` (4GB CUDA pull при cache miss). Вместо:
|
||||
|
||||
```bash
|
||||
docker rmi gx/cuframes-composer:0.11b-step1 -f 2>/dev/null
|
||||
CID=$(docker create gx/cuframes-composer:0.10)
|
||||
docker cp build-jammy/examples/grid_record "$CID":/usr/local/bin/grid_record
|
||||
docker cp build-jammy/src/libcuframes_composer.so.0.1.0 \
|
||||
"$CID":/usr/lib/x86_64-linux-gnu/libcuframes_composer.so.0
|
||||
docker cp docker/templates.json "$CID":/opt/templates.json
|
||||
docker cp docker/mqtt_overlays.json "$CID":/opt/mqtt_overlays.json
|
||||
docker commit \
|
||||
--change 'ENTRYPOINT ["/usr/local/bin/grid_record"]' \
|
||||
--change 'CMD ["--help"]' \
|
||||
--change 'ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,video' \
|
||||
"$CID" gx/cuframes-composer:0.11b-step1
|
||||
docker rm "$CID"
|
||||
```
|
||||
|
||||
Использует базу `gx/cuframes-composer:0.10` (с уже установленными runtime
|
||||
deps) + накладывает свежие артефакты. Быстрее и без сетевого трафика.
|
||||
|
||||
### 2.4 Build ONVIF image
|
||||
|
||||
```bash
|
||||
cd hosts/R9-88.23/docker/cctv/onvif
|
||||
docker build -t gx/cctv-onvif:0.6 -f Dockerfile .
|
||||
```
|
||||
|
||||
Python image, лёгкий. Если меняешь `server.py` — rebuild image (тег
|
||||
поднимать) + правишь image в `docker-compose.override.yml`.
|
||||
|
||||
## 3. Deploy
|
||||
|
||||
### 3.1 Прод (R9-88.23)
|
||||
|
||||
```bash
|
||||
cd /home/claude/projects/localhost-infra/hosts/R9-88.23/docker/cctv
|
||||
docker compose -f docker-compose.yml \
|
||||
-f cuda-grid/docker-compose.override.yml \
|
||||
-f cuframes-composer/docker-compose.override.yml \
|
||||
-f onvif/docker-compose.override.yml \
|
||||
up -d cfc-grid
|
||||
```
|
||||
|
||||
Compose автоматически recreate'нет контейнер если image tag поменялся в
|
||||
`docker-compose.override.yml`.
|
||||
|
||||
### 3.2 Verify post-deploy
|
||||
|
||||
```bash
|
||||
# Logs композитора
|
||||
docker logs --tail 30 cfc-grid 2>&1 | grep -iE "loaded|template|pool|motion"
|
||||
|
||||
# Ожидаем что-то типа:
|
||||
# [cfc/loader] /opt/templates.json: loaded 9 templates
|
||||
# [cfc/composer] templates loaded: 9 (path='/opt/templates.json')
|
||||
# [cfc/composer] pool+ 'cam-parking' (frigate=parking_overview prio=100)
|
||||
# [cfc/composer] motion_mode=1 ttl=45000ms pool=4
|
||||
# [cfc/composer] grow → template='tpl_3' active=3
|
||||
```
|
||||
|
||||
### 3.3 Rollback
|
||||
|
||||
```bash
|
||||
sed -i 's|gx/cuframes-composer:0.11b-step1|gx/cuframes-composer:0.10|' \
|
||||
hosts/R9-88.23/docker/cctv/cuframes-composer/docker-compose.override.yml
|
||||
docker compose ... up -d cfc-grid
|
||||
```
|
||||
|
||||
## 4. Logs
|
||||
|
||||
### 4.1 Live tail
|
||||
|
||||
```bash
|
||||
docker logs -f --tail 50 cfc-grid
|
||||
docker logs -f --tail 30 cfc-grid-ffmpeg
|
||||
docker logs -f --tail 30 cuda-grid-mediamtx
|
||||
docker logs -f --tail 30 cctv-onvif
|
||||
docker logs -f --tail 30 cctv-frigate
|
||||
```
|
||||
|
||||
### 4.2 Telemetry pattern
|
||||
|
||||
| Маркер | Что значит |
|
||||
|---|---|
|
||||
| `[grid_record] N кадров, M IDR, X МБ за Y.0с (25.0 fps)` | Composer успешно encode'ит каждые ~50 кадров |
|
||||
| `[cfc/composer] grow → template='X'` | Применился новый template (расширение, мгновенно) |
|
||||
| `[cfc/composer] shrink → template='X'` | Применился новый template после hysteresis (сжатие) |
|
||||
| `[cfc/composer] manual override 'X' до +60000ms` | PTZ через ONVIF |
|
||||
| `[cfc/composer] manual override expired, возврат в motion-mode` | Auto-возврат после 60s |
|
||||
| `[cfc/mqtt-overlay/<id>] '<text>'` | MQTT-overlay получил/отрендерил новый text |
|
||||
| `[cfc/frigate] connected, subscribe 'frigate/events'` | Frigate-subscriber подключился |
|
||||
| `[cfc/temp] update: 'XX.X°C'` | (старый код, deprecated — теперь mqtt-overlay) |
|
||||
|
||||
### 4.3 Когда что-то сломалось
|
||||
|
||||
| Симптом | Где искать |
|
||||
|---|---|
|
||||
| `src active=0 stale=0 dead=4` | cuframes-pub-* контейнеры; проверь `docker ps` и сетевой доступ к камерам |
|
||||
| `overlay 0 draw failed` | `cfc_overlay_text_rebuild_atlas` — обычно невалидный шрифт или текст |
|
||||
| RTSP стрим не отдаёт | `cfc-grid-ffmpeg` логи; смотри §6.1 |
|
||||
| TV/ONVIF не находит | `cctv-onvif` логи; проверь multicast WS-Discovery в LAN |
|
||||
|
||||
## 5. Monitoring
|
||||
|
||||
### 5.1 MQTT health
|
||||
|
||||
`cfc-grid` публикует health в `cuda_grid/health/composer/cfc-grid`
|
||||
каждые ~10 секунд:
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime_s": 3600,
|
||||
"frames_encoded": 90000,
|
||||
"fps_actual": 25.0,
|
||||
"bitrate_kbps": 6000,
|
||||
"src_active": 4,
|
||||
"src_stale": 0,
|
||||
"src_dead": 0,
|
||||
"idr_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
PW=$(grep '^COMPOSER_MQTT_PASSWORD=' \
|
||||
/home/claude/projects/localhost-infra/hosts/R9-88.23/docker/cctv/.env | cut -d= -f2)
|
||||
mosquitto_sub -h 192.168.88.23 -u composer -P "$PW" \
|
||||
-t 'cuda_grid/health/composer/cfc-grid' -v
|
||||
```
|
||||
|
||||
### 5.2 Watchdog
|
||||
|
||||
`cfc-grid-watchdog` — отдельный сервис, мониторит mediamtx
|
||||
`inboundBytes` для пути `cfc-grid`. Если **30 секунд молчания** —
|
||||
`docker restart cfc-grid`.
|
||||
|
||||
Логи watchdog'а:
|
||||
```bash
|
||||
docker logs --tail 30 cfc-grid-watchdog
|
||||
```
|
||||
|
||||
При срабатывании — публикует в `cuda_grid/health/watchdog/cfc-grid`.
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
### 6.1 RTSP не отдаёт / `cfc-grid-ffmpeg` в "Broken pipe"
|
||||
|
||||
**Симптом:** `docker logs cfc-grid-ffmpeg` показывает
|
||||
`[out#0/rtsp] Task finished with error code: -32 (Broken pipe)`.
|
||||
|
||||
**Причина:** `--intra-refresh` в composer'е (без IDR-burst'ов), mediamtx
|
||||
рвёт RTSP-publisher если не может отдать новому клиенту start-frame.
|
||||
|
||||
**Лечение:**
|
||||
- Полный restart pipeline:
|
||||
```bash
|
||||
docker compose ... restart cfc-grid-ffmpeg cfc-grid cuda-grid-mediamtx
|
||||
```
|
||||
- Если повторяется — отключить `--intra-refresh` в compose override
|
||||
(стоимость: IDR-bursts в bitrate, но стабильнее для downstream
|
||||
клиентов с frequent disconnect/reconnect)
|
||||
|
||||
### 6.2 ffmpeg не получает кадры от RTSP
|
||||
|
||||
**Симптом:** при `ffmpeg -i rtsp://192.168.88.23:554/cfc-grid -frames:v 1 out.jpg`
|
||||
зависает на 30+ секунд.
|
||||
|
||||
**Причина:** Composer пишет H.264 без regular IDR (intra-refresh). Новый
|
||||
RTSP-клиент ждёт keyframe для старта декодинга. ffmpeg в default
|
||||
конфигурации не ждёт достаточно долго.
|
||||
|
||||
**Workaround:**
|
||||
```bash
|
||||
ffmpeg -rtsp_transport tcp \
|
||||
-analyzeduration 10000000 -probesize 10000000 \
|
||||
-i rtsp://192.168.88.23:554/cfc-grid \
|
||||
-frames:v 1 -y out.jpg
|
||||
```
|
||||
|
||||
Или используй HLS:
|
||||
```bash
|
||||
ffmpeg -i http://192.168.88.23:8888/cfc-grid/index.m3u8 \
|
||||
-frames:v 1 -y out.jpg
|
||||
```
|
||||
|
||||
### 6.3 MQTT-overlay не обновляется
|
||||
|
||||
**Чек-лист:**
|
||||
|
||||
1. Бридж к HA broker (192.168.88.4) работает?
|
||||
```bash
|
||||
docker logs cctv-mosquitto 2>&1 | grep -i 'bridge'
|
||||
```
|
||||
Ищи `Connecting bridge ha-bridge` и подтверждение connect.
|
||||
|
||||
2. Нужный topic в bridge config?
|
||||
```bash
|
||||
docker exec cctv-mosquitto grep 'topic.*in 0' /mosquitto/config/mosquitto.conf
|
||||
```
|
||||
Если новый префикс — добавь `topic XXX/# in 0` и restart mosquitto.
|
||||
|
||||
3. Subscriber подключился?
|
||||
```bash
|
||||
docker logs cfc-grid 2>&1 | grep 'mqtt-overlay/<id>.*connected'
|
||||
```
|
||||
|
||||
4. Тестовый publish:
|
||||
```bash
|
||||
mosquitto_pub -h 192.168.88.4 -t '<твой topic>' -m 'test' -r
|
||||
```
|
||||
В логах composer'а должно появиться `[cfc/mqtt-overlay/<id>] 'test'`.
|
||||
|
||||
### 6.4 Motion-mode не переключает layout
|
||||
|
||||
**Чек-лист:**
|
||||
|
||||
1. Frigate шлёт events?
|
||||
```bash
|
||||
mosquitto_sub -h 192.168.88.23 -u composer -P "$PW" \
|
||||
-t 'frigate/events' -C 3
|
||||
```
|
||||
|
||||
2. Composer получает events?
|
||||
```bash
|
||||
docker logs cfc-grid 2>&1 | grep 'frigate.*started\|grow\|shrink'
|
||||
```
|
||||
|
||||
3. Camera-name матчится?
|
||||
`frigate=<имя>` в `--source` должно совпадать с `event.after.camera`.
|
||||
|
||||
4. Zone-filter не отсекает?
|
||||
Если `zones=A:B:C` в `--source` — посмотри в Frigate event
|
||||
`current_zones`. Если пусто или не пересекается — pulse отбрасывается.
|
||||
|
||||
5. TTL не истёк?
|
||||
Logs `motion_ttl=45000` (45 сек) — если события приходят реже —
|
||||
камера выпадает из active.
|
||||
|
||||
### 6.5 ONVIF PTZ presets пусты в TV
|
||||
|
||||
**Причина:** TV закешировал старый ответ `GetPresets` (Phase 9 имена).
|
||||
|
||||
**Лечение:** удалить и заново добавить камеру в TV-клиенте.
|
||||
|
||||
### 6.6 Templates загрузились но motion-mode не использует новый
|
||||
|
||||
Composer читает global registry `cfc::current_templates()` на каждом
|
||||
кадре — изменение через `cfc_layout_load_file` (ZMQ или CLI) должно
|
||||
быть подхвачено сразу. Если нет — проверь:
|
||||
|
||||
```bash
|
||||
echo '{"cmd":"list_layouts"}' | python3 -c "
|
||||
import zmq,json,sys
|
||||
s = zmq.Context().socket(zmq.REQ)
|
||||
s.connect('tcp://192.168.88.23:5599')
|
||||
s.send_json({'cmd':'list_layouts'})
|
||||
print(json.dumps(s.recv_json(), indent=2, ensure_ascii=False))"
|
||||
```
|
||||
|
||||
Поле `source` показывает текущий загруженный path. Если built-in (только
|
||||
`tpl_1` + `tpl_4`) — JSON не подгрузился (syntax error, кривой path).
|
||||
|
||||
## 7. Конфиги в репо
|
||||
|
||||
| Что | Где |
|
||||
|---|---|
|
||||
| templates.json | `cuframes-composer/docker/templates.json` |
|
||||
| mqtt_overlays.json | `cuframes-composer/docker/mqtt_overlays.json` |
|
||||
| compose override | `localhost-infra/hosts/R9-88.23/docker/cctv/cuframes-composer/docker-compose.override.yml` |
|
||||
| ONVIF config | `localhost-infra/.../onvif/onvif.yaml` |
|
||||
| ONVIF server | `localhost-infra/.../onvif/server.py` |
|
||||
| Mosquitto config | `localhost-infra/.../cctv/mosquitto/config/mosquitto.conf` |
|
||||
| .env (passwords) | `localhost-infra/.../cctv/.env` (gitignored) |
|
||||
|
||||
После изменения compose override — `docker compose ... up -d cfc-grid`
|
||||
автоматически recreate'нет.
|
||||
|
||||
## 8. Известные ограничения / TODO
|
||||
|
||||
- **`--intra-refresh` ↔ RTSP-clients**: trade-off bitrate vs latency
|
||||
(см. §6.1)
|
||||
- **Watchdog только cfc-grid**: cfc-grid-ffmpeg в зомби-state не
|
||||
детектится напрямую; помогает только полный restart
|
||||
- **Hot-reload mqtt_overlays.json**: нет ZMQ verb'а
|
||||
- **MQTT-overlay per-broker config**: всё через один broker; для
|
||||
внешнего broker'а нужно расширить `MqttBrokerCfg` per-item
|
||||
|
||||
## 9. См. также
|
||||
|
||||
- [user.md](user.md) — настройка композитора
|
||||
- [developer.md](developer.md) — внутренности, добавление модулей
|
||||
- `memory/host-and-project.md` — общая инфра R9-88.23
|
||||
- `memory/project_cfc-grid-deployed.md` — deploy 1-го прода
|
||||
- `memory/project_cfc-grid-cpp-refactor.md` — Phase 11b refactor
|
||||
- `memory/incremental-ffmpeg-rebuild.md` — incremental docker recipe
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
# cfc-grid — руководство пользователя
|
||||
|
||||
> Аудитория: администратор инсталляции. Кто настраивает камеры, layout'ы,
|
||||
> overlay'и, смотрит RTSP-поток на TV или в браузере. Не правит C++ код.
|
||||
>
|
||||
> Если ты разработчик и хочешь добавить новый тип ячейки/декорации/виджета —
|
||||
> см. [developer.md](developer.md).
|
||||
|
||||
## 1. Что это
|
||||
|
||||
**cfc-grid** — CUDA-композитор, собирающий N камер в один RTSP-поток
|
||||
`rtsp://192.168.88.23:554/cfc-grid` (1920×1080, H.264, NVENC). Раскладка
|
||||
выбирается автоматически по движению (от Frigate) или вручную через
|
||||
ONVIF-presets с TV.
|
||||
|
||||
Параллельно с видеокадром поверх рисуются:
|
||||
|
||||
- **Borders** — серые рамки 2 px вокруг каждой ячейки
|
||||
- **Labels** — подпись `имя prio=N` в углу каждой камеры
|
||||
- **Detection boxes** — рамки объектов от Frigate, повторяющие позицию
|
||||
камеры при смене layout
|
||||
- **MQTT-overlays** — текстовые поля привязанные к топикам MQTT
|
||||
(температура, статусы, чаты)
|
||||
|
||||
## 2. Архитектура одной фразой
|
||||
|
||||
```
|
||||
cuframes-pub-* (на камеру)
|
||||
↓ shared VMM
|
||||
cfc-grid (composer) ── ZMQ control ──┐
|
||||
↓ pipe (H.264) │
|
||||
cfc-grid-ffmpeg (relay) ─→ mediamtx ─┴─→ TV / VLC / Frigate / ...
|
||||
↑
|
||||
ONVIF discovery от cctv-onvif
|
||||
```
|
||||
|
||||
Кадры через cuframes идут zero-copy (один VMM-буфер, разделяемый между
|
||||
publisher'ом и composer'ом). Композитор берёт NV12-поверхность,
|
||||
ресайзит/блитит в свой output, добавляет декорации, отдаёт NVENC,
|
||||
NVENC пишет H.264 в pipe, `cfc-grid-ffmpeg` транскодирует pipe → RTSP
|
||||
к mediamtx.
|
||||
|
||||
## 3. Motion-mode — основной режим работы
|
||||
|
||||
### 3.1 Что происходит
|
||||
|
||||
На каждом кадре композитор:
|
||||
|
||||
1. Берёт `last_motion_ms` для каждой камеры (обновляется из Frigate MQTT
|
||||
`frigate/events`)
|
||||
2. Считает «активными» те у кого `(now - last_motion_ms) < motion_ttl_ms`
|
||||
(по умолчанию **45 секунд**)
|
||||
3. Сортирует активных по `priority` (число; больше = главнее)
|
||||
4. Выбирает template из `templates.json` по правилу **best-fit**:
|
||||
минимальный template с `nb_camera_cells >= количество_активных`
|
||||
5. Если в template'е больше camera-cells, чем активных — лишние
|
||||
заполняются остальными drawable камерами из pool (по приоритету)
|
||||
6. Применяет **asymmetric hysteresis**: рост числа активных переключает
|
||||
layout мгновенно, уменьшение — ждёт 3 секунды (чтобы не мелькало)
|
||||
|
||||
### 3.2 Что значит «drawable»
|
||||
|
||||
Камера **исключается** из pool если её `cfc_source_state_t` =
|
||||
`CONNECTING`, `DISCONNECTED` или `DEAD` (cuframes publisher молчит
|
||||
дольше `dead_threshold_ms`, по умолчанию 5 секунд).
|
||||
|
||||
`STALE` (кадры приходят редко) — считается, рисуется последний доступный
|
||||
кадр.
|
||||
|
||||
### 3.3 Manual override через PTZ
|
||||
|
||||
В TV в ONVIF PTZ-presets отображаются имена templates (`tpl_1, tpl_3,
|
||||
tpl_4, ..., tpl_16`). Нажатие `GotoPreset` или movement-кнопок:
|
||||
|
||||
- Применяет выбранный layout мгновенно
|
||||
- **Замораживает** motion-mode на 60 секунд
|
||||
- По истечении — возвращается в auto-режим
|
||||
|
||||
ContinuousMove (стрелки): pan/tilt циклируют по списку presets, zoom-in
|
||||
=`tpl_1` (full screen), zoom-out=`tpl_16` (4×4 grid).
|
||||
|
||||
## 4. templates.json — раскладка экрана
|
||||
|
||||
### 4.1 Схема
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"grid_cols": 8,
|
||||
"grid_rows": 8,
|
||||
"templates": [
|
||||
{
|
||||
"name": "tpl_N",
|
||||
"_desc": "Описание",
|
||||
"priority": 0,
|
||||
"cells": [
|
||||
{
|
||||
"col": 0, "row": 0,
|
||||
"cs": 4, "rs": 4,
|
||||
"role": "camera",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"col": 6, "row": 0,
|
||||
"cs": 2, "rs": 6,
|
||||
"role": "widget",
|
||||
"widget": "temp_chart"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Грид: 8×8 микроячеек** (240×135 px каждая = 16:9 на output 1920×1080).
|
||||
Любой квадрат N×N микроячеек тоже 16:9.
|
||||
|
||||
| Поле | Значение |
|
||||
|---|---|
|
||||
| `col`, `row` | Top-left угол cell в микроячейках (0..7) |
|
||||
| `cs`, `rs` | Размер в микроячейках |
|
||||
| `role` | `camera` либо `widget` |
|
||||
| `order` | Для `camera`: порядок placement'а активных (0 = главная, обычно крупнейшая cell) |
|
||||
| `widget` | Для `widget`: имя placeholder'а (текст подписи) |
|
||||
|
||||
### 4.2 Best-fit selection
|
||||
|
||||
Композитор выбирает template для текущего числа активных:
|
||||
|
||||
```
|
||||
candidates = [t for t in templates if t.nb_camera_cells >= n_active]
|
||||
pick = min(candidates, key=lambda t: t.nb_camera_cells - n_active)
|
||||
# При равенстве: больший priority побеждает
|
||||
```
|
||||
|
||||
Если активных больше чем cell'ов в самом большом template — берётся
|
||||
самый большой, лишние камеры обрезаются (низший priority вылетает).
|
||||
|
||||
### 4.3 Встроенные templates
|
||||
|
||||
По умолчанию 9 templates в `/opt/templates.json`:
|
||||
|
||||
| Имя | Cells | Описание |
|
||||
|---|---|---|
|
||||
| `tpl_1` | 1 cam | Одна камера во весь экран |
|
||||
| `tpl_3` | 3 cam + 2 widgets | Главная 1440×810 слева + 2 превью справа + 2 widget |
|
||||
| `tpl_4` | 4 cam | Quad 2×2, 960×540 каждая |
|
||||
| `tpl_5` | 5 cam + 1 widget | Главная + 4 превью справа стопкой + widget снизу |
|
||||
| `tpl_6` | 6 cam + 1 widget | Главная + 3 правые + 2 нижние + widget |
|
||||
| `tpl_7` | 7 cam + 1 widget | Главная + 3 правые + 3 нижние + widget |
|
||||
| `tpl_8` | 8 cam (1+3+4) | Главная + 3 правые + 4 в нижней строке |
|
||||
| `tpl_9` | 9 cam + 2 widgets | 3×3 главных + widget справа + widget снизу |
|
||||
| `tpl_16` | 16 cam | 4×4 grid, 480×270 каждая |
|
||||
|
||||
Подробности — см. `docker/templates.json` в репо.
|
||||
|
||||
### 4.4 Как добавить свой template
|
||||
|
||||
1. Открыть `docker/templates.json` (или подмонтированный override)
|
||||
2. Добавить блок в `"templates": [...]` по схеме выше
|
||||
3. Перезапустить cfc-grid (либо `docker exec cfc-grid sh -c 'kill -HUP 1'`
|
||||
когда добавим hot-reload в Phase 12), либо вызвать ZMQ:
|
||||
|
||||
```bash
|
||||
mosquitto_pub -h 192.168.88.23 -p 5599 ... # пока нет CLI, см. operations.md
|
||||
```
|
||||
|
||||
### 4.5 Координатная математика
|
||||
|
||||
Каждая микроячейка = `1920/8 = 240 px` ширина, `1080/8 = 135 px` высота
|
||||
(aspect 16:9). Cell `{col=2, row=4, cs=4, rs=2}`:
|
||||
|
||||
- pixel x = `2 * 240 = 480`
|
||||
- pixel y = `4 * 135 = 540`
|
||||
- pixel w = `4 * 240 = 960`
|
||||
- pixel h = `2 * 135 = 270`
|
||||
|
||||
Aspect cell = `cs/rs * 16/9`. Для 16:9 cell'ов **cs == rs**.
|
||||
|
||||
## 5. mqtt_overlays.json — text overlay'и из MQTT
|
||||
|
||||
### 5.1 Схема
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"overlays": [
|
||||
{
|
||||
"id": "temp_outside",
|
||||
"topic": "zigbee2mqtt/Температура на улице",
|
||||
"json_field": "temperature",
|
||||
"format": "%+.1f°C",
|
||||
"anchor": "right-bottom",
|
||||
"margin_x": 32, "margin_y": 24,
|
||||
"pixel_size": 32,
|
||||
"color": [255, 255, 255],
|
||||
"alpha": 230,
|
||||
"bg_alpha": 160,
|
||||
"bg_y": 16, "bg_u": 128, "bg_v": 128,
|
||||
"bg_pad": 10,
|
||||
"placeholder": "—",
|
||||
"font_path": "/fonts/DejaVuSans-Bold.ttf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| `id` | Уникальный идентификатор overlay'я (используется ZMQ для lookup'а) |
|
||||
| `topic` | MQTT topic для subscribe (через `cctv-mosquitto`) |
|
||||
| `json_field` | Если payload JSON — имя поля для извлечения значения; **пусто = raw payload как строка** |
|
||||
| `format` | `printf`-стиль для отформатированного значения (например `"%+.1f°C"`, `"%s"`) |
|
||||
| `anchor` | Якорь позиционирования: `right-bottom`, `right-top`, `left-bottom`, `left-top`, `center` |
|
||||
| `margin_x`, `margin_y` | Отступ от ближайшего края экрана (px) |
|
||||
| `pixel_size` | Размер шрифта в пикселях |
|
||||
| `color` | RGB цвет текста |
|
||||
| `alpha` | Общая непрозрачность текста (0..255) |
|
||||
| `bg_alpha` | Непрозрачность подложки (0..255); **0 = без фона** |
|
||||
| `bg_y`, `bg_u`, `bg_v` | BT.709 limited-range цвет подложки (Y=16..235, UV=16..240); default чёрный |
|
||||
| `bg_pad` | Отступ подложки вокруг текста (px) |
|
||||
| `placeholder` | Что показывать пока не пришло MQTT-сообщение; пусто = "—" |
|
||||
| `font_path` | Путь к шрифту (.ttf/.otf) в контейнере |
|
||||
|
||||
### 5.2 Поддерживаемые символы
|
||||
|
||||
Шрифт `DejaVuSans-Bold.ttf` — стандартный из пакета `fonts-dejavu`.
|
||||
**Покрывает Basic Multilingual Plane** (latin, cyrillic, базовые
|
||||
символы), включая:
|
||||
|
||||
- `❯` (U+276F), `✎` (U+270E), `➤` (U+27A4), `→` (U+2192)
|
||||
- `★` (U+2605), `▶` (U+25B6), `✉` (U+2709)
|
||||
|
||||
**Emoji из Supplementary Multilingual Plane (>U+10000)** — например
|
||||
`🗣` (U+1F5E3), `🤖` (U+1F916), `💬` (U+1F4AC) — **не рендерятся**:
|
||||
шрифт не содержит таких глифов. Рисуется placeholder-квадрат.
|
||||
|
||||
Чтобы добавить color emoji — нужно подключить Noto Color Emoji и
|
||||
расширить рендерер для COLR/CPAL/SBIX (см. developer.md).
|
||||
|
||||
### 5.3 Примеры
|
||||
|
||||
**Только число**: payload = `"23.5"` (raw), `json_field: ""`, `format: "%s°"`
|
||||
|
||||
**JSON с полем**: payload = `{"temperature": 23.5, "humidity": 45}`,
|
||||
`json_field: "temperature"`, `format: "%+.1f°C"`
|
||||
|
||||
**Несколько overlay'ев** в правом-верхнем углу столбиком: первый с
|
||||
`margin_y: 24`, второй с `margin_y: 72`, третий с `margin_y: 120`.
|
||||
|
||||
### 5.4 Как добавить
|
||||
|
||||
1. Открыть `docker/mqtt_overlays.json` (либо подмонтированный override)
|
||||
2. Добавить блок в массив `"overlays": [...]`
|
||||
3. Перезапустить cfc-grid
|
||||
|
||||
Hot-reload через ZMQ — Phase 12 (`reload_overlays` verb).
|
||||
|
||||
## 6. CLI флаги composer'а
|
||||
|
||||
В compose override `docker/cctv/cuframes-composer/docker-compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- "--out=/out/grid.h264" # named pipe для ffmpeg-relay
|
||||
- "--fps=25"
|
||||
- "--bitrate=6000" # kbps
|
||||
- "--width=1920"
|
||||
- "--height=1080"
|
||||
- "--intra-refresh" # вместо IDR-burst'ов (low-latency)
|
||||
- "--control=tcp://0.0.0.0:5599" # ZMQ control plane
|
||||
- "--mqtt=cctv-mosquitto:1883" # MQTT для health-публикации
|
||||
- "--mqtt-instance=cfc-grid"
|
||||
- "--mqtt-user=composer"
|
||||
- "--mqtt-pass=${COMPOSER_MQTT_PASSWORD}"
|
||||
|
||||
# Источники (камеры) — повторяемое
|
||||
- "--source=cam-parking,frigate=parking_overview,priority=100,zones=parking_zone:canopy:private_area"
|
||||
- "--source=cam-back_yard,frigate=back_yard,priority=70"
|
||||
# ...
|
||||
|
||||
- "--motion-mode" # включить auto-layout
|
||||
- "--motion-ttl=45000" # ms
|
||||
|
||||
- "--templates=/opt/templates.json"
|
||||
- "--mqtt-overlays=/opt/mqtt_overlays.json"
|
||||
|
||||
# Frigate motion-driver и detection boxes
|
||||
- "--frigate-mqtt=cctv-mosquitto:1883"
|
||||
- "--frigate-topic=frigate/events"
|
||||
- "--detection-cell=parking,parking_overview,0,0,960,540,640,480,parking_zone:canopy:private_area"
|
||||
```
|
||||
|
||||
### 6.1 `--source` синтаксис
|
||||
|
||||
```
|
||||
--source=<cuframes_key>,frigate=<camera>,priority=<N>[,zones=<z1>:<z2>:...]
|
||||
```
|
||||
|
||||
- `cuframes_key` — имя cuframes-publisher'а (например `cam-parking`)
|
||||
- `frigate=NAME` — имя камеры в Frigate (для матчинга motion-событий)
|
||||
- `priority=N` — целое, больше = главнее
|
||||
- `zones=...` — опциональный whitelist Frigate zones; motion засчитывается
|
||||
только если `event.current_zones` пересекается со списком (отсев
|
||||
street-флуда)
|
||||
|
||||
### 6.2 `--detection-cell` синтаксис
|
||||
|
||||
```
|
||||
--detection-cell=<key>,<frigate_camera>,<x>,<y>,<w>,<h>,<detect_w>,<detect_h>[,zones]
|
||||
```
|
||||
|
||||
- `key` — произвольный идентификатор overlay'я для логов
|
||||
- `frigate_camera` — имя в Frigate (для матчинга event.camera)
|
||||
- `x,y,w,h` — initial geometry (composer пересчитает динамически)
|
||||
- `detect_w,detect_h` — разрешение детектора Frigate (например 640×480)
|
||||
- `zones` — whitelist для bbox
|
||||
|
||||
## 7. ZMQ control plane
|
||||
|
||||
Default endpoint: `tcp://192.168.88.23:5599`. Все verb'ы — JSON request/reply.
|
||||
|
||||
### 7.1 Список verbs
|
||||
|
||||
| Команда | Параметры | Что делает |
|
||||
|---|---|---|
|
||||
| `ping` | — | Health-check |
|
||||
| `health` | — | `{total, active, stale, dead}` по pool'у |
|
||||
| `set_text` | `id, text, r, g, b, x, y, visible` | Обновить текстовый overlay (для CLI `--text=...`) |
|
||||
| `set_visible` | `id, visible` | Скрыть/показать overlay |
|
||||
| `list_overlays` | — | Список overlay'ев |
|
||||
| `set_layout` | `name` | Применить named template (manual override на 60s в motion-mode) |
|
||||
| `list_layouts` | — | Список доступных templates с cells |
|
||||
| `get_layout` | — | Имя текущего template'а |
|
||||
| `set_motion_mode` | `on, ttl_ms` | Включить/выключить motion-режим |
|
||||
| `get_motion_mode` | — | Состояние motion-mode |
|
||||
| `get_template` | `name` | Полный JSON template'а |
|
||||
| `reload_templates` | `path?` | Перезагрузить templates из файла (default — последний путь) |
|
||||
|
||||
### 7.2 Пример
|
||||
|
||||
```bash
|
||||
# Python
|
||||
python3 <<EOF
|
||||
import zmq, json
|
||||
s = zmq.Context().socket(zmq.REQ)
|
||||
s.connect("tcp://192.168.88.23:5599")
|
||||
s.send_json({"cmd": "list_layouts"})
|
||||
print(json.dumps(s.recv_json(), indent=2, ensure_ascii=False))
|
||||
EOF
|
||||
```
|
||||
|
||||
```bash
|
||||
# Force layout
|
||||
echo '{"cmd":"set_layout","name":"tpl_4"}' | \
|
||||
nc -q1 192.168.88.23 5599 # ! REP socket требует ZMQ framing, не голый TCP
|
||||
```
|
||||
|
||||
Голый `nc` **не работает** — REP socket ожидает ZMQ wire-protocol. Используй
|
||||
`zmq` Python/Go/JS либо `mosquitto_pub` через RPC-bridge (Phase 12).
|
||||
|
||||
## 8. ONVIF и PTZ
|
||||
|
||||
Сервис `cctv-onvif` биндится на host network, отвечает на WS-Discovery
|
||||
multicast (239.255.255.250:3702) и SOAP-запросы по HTTP `:8085`.
|
||||
|
||||
### 8.1 Добавление в TV
|
||||
|
||||
В клиенте (TV / IP-CamViewer / Synology / Frigate):
|
||||
|
||||
- ONVIF host: `192.168.88.23`
|
||||
- Port: `8085`
|
||||
- User / Password: пусто (auth не настроен)
|
||||
|
||||
WS-Discovery в LAN 192.168.88.0/24 найдёт устройство `cfc-grid (Goldix)`.
|
||||
RTSP-URL автоматически — `rtsp://192.168.88.23:554/cfc-grid`.
|
||||
|
||||
### 8.2 PTZ presets
|
||||
|
||||
Список (`GetPresets`): `tpl_1, tpl_3, tpl_4, tpl_5, tpl_6, tpl_7, tpl_8,
|
||||
tpl_9, tpl_16`.
|
||||
|
||||
GotoPreset(name) → composer применяет template + замораживает motion-mode
|
||||
на 60 секунд → auto-возврат.
|
||||
|
||||
### 8.3 PTZ movement (ContinuousMove)
|
||||
|
||||
| Команда | Действие |
|
||||
|---|---|
|
||||
| Pan right / Tilt down | Следующий template в списке |
|
||||
| Pan left / Tilt up | Предыдущий |
|
||||
| Zoom in (+) | `tpl_1` (full screen) |
|
||||
| Zoom out (−) | `tpl_16` (4×4 grid) |
|
||||
|
||||
## 9. Где смотреть RTSP
|
||||
|
||||
| Способ | URL |
|
||||
|---|---|
|
||||
| VLC / mpv / ffplay | `rtsp://192.168.88.23:554/cfc-grid` |
|
||||
| Браузер (HLS) | `http://192.168.88.23:8888/cfc-grid` |
|
||||
| WebRTC | `http://192.168.88.23:8889/cfc-grid` |
|
||||
| OBS / FFmpeg input | `rtsp://192.168.88.23:554/cfc-grid` |
|
||||
| ONVIF клиенты | через WS-Discovery (см. §8) |
|
||||
|
||||
## 10. Известные ограничения
|
||||
|
||||
- **Color emoji не рендерятся** (нужен Noto Color Emoji + COLR/CPAL
|
||||
поддержка в text renderer — Phase 12)
|
||||
- **Hot-reload `mqtt_overlays.json`** — нет ZMQ verb'а, нужен restart
|
||||
cfc-grid
|
||||
- **Per-overlay broker** — все MQTT-overlay'и используют общий
|
||||
broker (тот что задан как `--mqtt`); подписку на сторонний broker
|
||||
отдельно — нет
|
||||
- **Widget rendering** — placeholder (тёмный rect + label), реальные
|
||||
виджеты (graph, chat) — Phase 12+
|
||||
- **HA Assist в MQTT** — архитектурное ограничение HA (см. operations.md
|
||||
§Troubleshooting)
|
||||
|
||||
## 11. FAQ
|
||||
|
||||
**В: На TV вижу старые имена layouts (`quad`, `dual_horizontal`).
|
||||
Что делать?**
|
||||
|
||||
О: TV закешировал ONVIF-presets. В клиенте удали камеру и добавь
|
||||
заново — он перечитает `GetPresets` с актуальными именами.
|
||||
|
||||
**В: Камера парковки в DEAD, но в logs показывает active=3.
|
||||
Почему?**
|
||||
|
||||
О: `cfc_composer_get_health` показывает **pool-wide** state, а
|
||||
motion-active считает по `last_motion_ms` независимо от source state.
|
||||
DEAD исключается на этапе `is_camera_drawable()` в `compose_motion_relayout`.
|
||||
|
||||
**В: PTZ нажал на TV, рамка переключилась, через минуту вернулась
|
||||
обратно.**
|
||||
|
||||
О: Это by design — `set_layout` в motion-mode замораживает auto на
|
||||
60 секунд (`manual_override_duration_ms`). Чтобы зафиксировать template
|
||||
надолго — выключи motion-mode целиком через ZMQ:
|
||||
|
||||
```json
|
||||
{"cmd": "set_motion_mode", "on": 0}
|
||||
```
|
||||
|
||||
**В: Хочу подложку у text overlay'я разного цвета.**
|
||||
|
||||
О: Поля `bg_y/bg_u/bg_v` в JSON принимают BT.709 limited-range. Чтобы
|
||||
получить красный — Y≈80, U≈90, V≈240. Для голубого — Y≈170, U≈170, V≈100.
|
||||
Калькулятор: https://www.rapidtables.com/convert/color/rgb-to-yuv.html
|
||||
(использовать BT.709 limited).
|
||||
|
||||
**В: При motion на 5 камерах layout не появляется, остаётся quad.**
|
||||
|
||||
О: Проверь `docker logs cfc-grid | grep "loaded N templates"` — там
|
||||
должно быть ≥5 (есть `tpl_5..tpl_8`, `tpl_9`, `tpl_16`). Если нет —
|
||||
templates.json не подгрузился (проверь syntax через `jq` либо
|
||||
`python3 -m json.tool`).
|
||||
|
||||
**В: Frigate-bbox рисуется не на той камере.**
|
||||
|
||||
О: Проверь `--detection-cell` — там должен быть `frigate_camera`
|
||||
который совпадает с `event.after.camera`. Composer связывает
|
||||
detbox-overlay с pool-entry по `frigate_camera` (см.
|
||||
`cfc_composer::pool::by_frigate_camera`).
|
||||
|
||||
## 12. Куда дальше
|
||||
|
||||
- [developer.md](developer.md) — внутреннее устройство, расширение
|
||||
- [operations.md](operations.md) — build, deploy, troubleshooting
|
||||
- README репо: краткий overview
|
||||
Reference in New Issue
Block a user