Files
cuframes/docs/integrations/cctv-cpp.md
T
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

10 KiB
Raw Blame History

C++ project integration (cctv-processor pattern)

Reference guide на основе реального production deployment (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).

// 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 — реализация

// 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)

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)

// В 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() — отдельный цикл:

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/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()
# 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:

{
  "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:

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

См. также