docs: reference integrations + examples

- docs/integrations/frigate.md — полный production-tested guide:
  Dockerfile, docker-compose, config.yml, troubleshooting (s6+pid, scale_cuda,
  hwaccel issues), build steps
- docs/integrations/cctv-cpp.md — C++ pattern: IFrameSource interface +
  CuframesSource skeleton + CMake setup + runtime requirements
- examples/frigate-compose/ — reference compose stack (cuframes-pub + Frigate)
  с config.yml stub, .env.example, README
- examples/python-consumer/ — ctypes-based skeleton для AI/ML pipeline'ов
  (до v0.3 native pybind11 bindings)
- docs/integration.md — превратился в index-страницу, ссылается на specific guides

Reorganization упрощает onboarding: пользователь выбирает guide по типу
integration'а (Frigate/C++/Python/FFmpeg) и сразу видит реальный code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 21:37:35 +01:00
parent a3ba3a95b2
commit 12708618d4
9 changed files with 1127 additions and 234 deletions
+309
View File
@@ -0,0 +1,309 @@
# C++ project integration (cctv-processor pattern)
Reference guide на основе реального production deployment
([gx/cctv](https://git.goldix.org/gx/cctv) — C++17 video processor).
## Use case
Custom video pipeline (motion detection, mosaic compose, encode-out, snapshots),
получает кадры с N камер и выполняет per-frame processing. Без cuframes:
один RTSP+NVDEC на каждую камеру **внутри** processor + дублирующий decode
если Frigate/AI script тоже подключены к той же камере.
С cuframes: processor подписывается на published frames, **никакого RTSP / NVDEC**
у него — все консьюмеры используют один decode от publisher'а.
## Архитектурный паттерн
Выделить **interface** `IFrameSource` чтобы pipeline не зависел от конкретного
источника (RTSP vs cuframes vs тестовый file).
```cpp
// include/sources/IFrameSource.h
namespace cctv::sources {
enum class ConnectionState {
DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING, ERROR
};
struct StreamInfo {
int width = 0;
int height = 0;
double fps = 0.0;
std::string codec_name;
int64_t bitrate = 0;
};
class IFrameSource {
public:
using FrameCallback = std::function<void(const cv::Mat& frame, int64_t ts_ms)>;
using StateCallback = std::function<void(ConnectionState, const std::string&)>;
virtual ~IFrameSource() = default;
virtual bool connect(const std::string& url) = 0;
virtual void disconnect() = 0;
virtual bool isConnected() const = 0;
virtual void setFrameCallback(FrameCallback) = 0;
virtual void setStateCallback(StateCallback) = 0;
virtual void setReconnectEnabled(bool) = 0;
virtual StreamInfo getStreamInfo() const = 0;
virtual ConnectionState getState() const = 0;
virtual std::string getLastError() const = 0;
virtual uint64_t getFramesReceived() const = 0;
virtual uint64_t getFramesDropped() const = 0;
virtual double getCurrentFPS() const = 0;
};
} // namespace cctv::sources
```
`RTSPClient` (legacy) и `CuframesSource` оба implement `IFrameSource`. Pipeline
работает с `unique_ptr<IFrameSource>` — code не знает, RTSP это или cuframes.
## CuframesSource — реализация
```cpp
// include/sources/CuframesSource.h
#include "sources/IFrameSource.h"
// Forward-declare — не утекают в header
struct cuframes_subscriber;
typedef struct cuframes_subscriber cuframes_subscriber_t;
namespace cctv::sources {
class CuframesSource : public IFrameSource {
public:
CuframesSource();
~CuframesSource() override;
// IFrameSource: URL для cuframes — это просто `key` (либо "cuframes://<key>")
bool connect(const std::string& url) override;
void disconnect() override;
// ... остальные методы (см. полный файл в gx/cctv repo)
void setCudaDevice(int device);
void setReconnectInterval(int seconds);
private:
void workerThread();
bool openSubscriber();
void closeSubscriber();
std::string m_key;
int m_cudaDevice = 0;
cuframes_subscriber_t* m_sub = nullptr;
void* m_cudaStream = nullptr; // cudaStream_t, opaque
void* m_hostBuffer = nullptr; // pinned host buffer для NV12
size_t m_hostBufferSize = 0;
std::thread m_thread;
std::atomic<bool> m_shouldStop{false};
// ... callbacks, state, stats
};
} // namespace cctv::sources
```
### Worker thread (core)
```cpp
void CuframesSource::workerThread() {
while (!m_shouldStop.load()) {
if (!m_sub) {
if (!openSubscriber()) {
changeState(ConnectionState::RECONNECTING, m_lastError);
if (!m_reconnectEnabled) return;
sleep_for(seconds(m_reconnectInterval));
continue;
}
changeState(ConnectionState::CONNECTED, "");
}
cuframes_frame_t* frame = nullptr;
int rc = cuframes_subscriber_next(m_sub, m_cudaStream, &frame, 200);
if (rc == CUFRAMES_ERR_TIMEOUT) continue;
if (rc == CUFRAMES_ERR_DISCONNECTED) {
closeSubscriber();
changeState(ConnectionState::RECONNECTING, "publisher disconnected");
continue;
}
if (rc != CUFRAMES_OK || !frame) {
LOG_ERROR("cuframes next: " + std::string(cuframes_strerror(rc)));
closeSubscriber();
continue;
}
// Frame metadata
int32_t w, h;
cuframes_frame_size(frame, &w, &h);
const int32_t pitch_y = cuframes_frame_pitch_y(frame);
const int32_t pitch_uv = cuframes_frame_pitch_uv(frame);
const int64_t pts_ns = cuframes_frame_pts_ns(frame);
// Ensure host buffer big enough
const size_t need = (size_t)w * h * 3 / 2; // NV12 packed
if (need > m_hostBufferSize) {
cudaFreeHost(m_hostBuffer);
cudaMallocHost(&m_hostBuffer, need);
m_hostBufferSize = need;
}
// Copy GPU NV12 → host NV12 (Y plane + UV plane)
uint8_t* cu = (uint8_t*)cuframes_frame_cuda_ptr(frame);
cudaMemcpy2DAsync(m_hostBuffer, w, cu, pitch_y,
w, h, cudaMemcpyDeviceToHost, m_cudaStream);
cudaMemcpy2DAsync((uint8_t*)m_hostBuffer + (size_t)w*h, w,
cu + (size_t)pitch_y*h, pitch_uv,
w, h/2, cudaMemcpyDeviceToHost, m_cudaStream);
cudaStreamSynchronize(m_cudaStream);
// Release frame BEFORE downstream processing — publisher может переиспользовать slot
cuframes_subscriber_release(m_sub, frame);
// NV12 → BGR (CPU) — downstream pipeline ожидает cv::Mat BGR
cv::Mat nv12(h * 3 / 2, w, CV_8UC1, m_hostBuffer);
cv::Mat bgr;
cv::cvtColor(nv12, bgr, cv::COLOR_YUV2BGR_NV12);
// Доставка через callback
if (m_frameCallback) m_frameCallback(bgr, pts_ns / 1000000);
}
closeSubscriber();
}
```
`cudaMemcpy → CPU → cv::cvtColor` это v0.1 path. **Zero-copy** через
`AVHWFramesContext` / OpenCV cv::cuda::GpuMat — planned v0.2.
## Factory pattern (per-camera)
```cpp
// В StreamProcessor::initializeComponents()
for (const auto& camera : cameras) {
if (!camera.enabled) continue;
std::unique_ptr<sources::IFrameSource> source;
if (camera.source_type == "cuframes") {
source = std::make_unique<sources::CuframesSource>();
} else {
source = std::make_unique<rtsp::RTSPClient>(); // legacy RTSP
}
source->setFrameCallback([this, id = camera.id](const cv::Mat& frame, int64_t ts) {
m_videoProcessor->processFrame(id, frame);
});
source->setStateCallback([this, id = camera.id](auto state, const std::string& msg) {
// logging, alerting, watchdog
});
source->setReconnectEnabled(true);
m_frameSources[camera.id] = std::move(source);
}
```
В `start()` — отдельный цикл:
```cpp
for (const auto& camera : cameras) {
if (!camera.enabled) continue;
auto& src = m_frameSources[camera.id];
const std::string url = (camera.source_type == "cuframes")
? camera.cuframes_key
: camera.rtsp_url;
src->connect(url);
}
```
## CMake integration
```cmake
# cmake/Dependencies.cmake
if(ENABLE_CUDA AND CUDA_AVAILABLE)
find_path(CUFRAMES_INCLUDE_DIR cuframes/cuframes.h
HINTS ${CUFRAMES_ROOT}/include /usr/local/include /usr/include
)
find_library(CUFRAMES_LIBRARY cuframes
HINTS ${CUFRAMES_ROOT}/lib ${CUFRAMES_ROOT}/lib64
/usr/local/lib /usr/lib
)
if(CUFRAMES_INCLUDE_DIR AND CUFRAMES_LIBRARY)
set(CUFRAMES_FOUND TRUE)
find_package(CUDAToolkit REQUIRED)
message(STATUS "cuframes: FOUND (${CUFRAMES_LIBRARY})")
else()
message(STATUS "cuframes: NOT FOUND (camera source_type=cuframes недоступен)")
endif()
endif()
```
```cmake
# apps/your-processor/CMakeLists.txt
if(CUFRAMES_FOUND)
target_include_directories(your-processor PRIVATE ${CUFRAMES_INCLUDE_DIR})
target_link_libraries(your-processor PRIVATE ${CUFRAMES_LIBRARY} CUDA::cudart)
target_compile_definitions(your-processor PRIVATE CCTV_HAVE_CUFRAMES=1)
endif()
```
`CuframesSource.cpp` оборачивается в `#ifdef CCTV_HAVE_CUFRAMES` — без cuframes
в системе фабрика возвращает error при `source_type == "cuframes"`, остальное
компилируется как обычно.
## Config
`cameras.json` extension:
```json
{
"cameras": [
{
"id": 1,
"name": "Парковка через cuframes",
"source_type": "cuframes",
"cuframes_key": "cam-parking",
"rtsp_url": "",
"enabled": true,
"motion_detection": { "enabled": false, ... }
},
{
"id": 2,
"name": "Камера на RTSP",
"rtsp_url": "rtsp://admin:pw@cam-ip:554/stream",
"enabled": true
}
]
}
```
## Runtime requirements
Consumer container/process должен:
1. Иметь доступ к `/run/cuframes` (volume mount от publisher'а).
2. Быть в **same** IPC namespace (для `/dev/shm` shared) — `ipc: container:<publisher>`.
3. Быть в **same** PID namespace (для CUDA driver IPC validation) — `pid: container:<publisher>` (если consumer не имеет PID-1-strict init типа s6-overlay).
4. Иметь NVIDIA runtime — `runtime: nvidia` в compose.
5. Запускаться с правом доступа к socket (по умолчанию root) — `user: root` в compose.
Пример compose service:
```yaml
your-cctv-backend:
image: your-image:cuda
runtime: nvidia
user: root # socket в publisher container root-owned
ipc: "container:cuframes-pub-parking"
pid: "container:cuframes-pub-parking" # если ваш image не использует s6
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
volumes:
- cuframes_sock:/run/cuframes:ro
```
## См. также
- [filter/README.md](../../filter/README.md) — FFmpeg demuxer (если ваш processor построен на FFmpeg)
- [docs/integrations/frigate.md](frigate.md) — Frigate-specific guide
- [docs/architecture.md](../architecture.md) — внутренности CUDA IPC
- [Полный код CuframesSource](https://git.goldix.org/gx/cctv/src/branch/enterprise/develop/cpp/apps/cctv-processor/src/sources/CuframesSource.cpp) — реальный production-tested файл