12708618d4
- 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>
310 lines
10 KiB
Markdown
310 lines
10 KiB
Markdown
# 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 файл
|