# 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; using StateCallback = std::function; 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` — 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://") 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 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 source; if (camera.source_type == "cuframes") { source = std::make_unique(); } else { source = std::make_unique(); // 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:`. 3. Быть в **same** PID namespace (для CUDA driver IPC validation) — `pid: container:` (если 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 файл