Files
gx 12708618d4 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>
2026-05-18 21:37:35 +01:00

310 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 файл