diff --git a/docs/integration.md b/docs/integration.md index dc76aa3..03b2667 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -1,11 +1,20 @@ # Integration guide -Этот guide описывает, как использовать cuframes для устранения дублирующего -GPU-декодирования между несколькими consumer'ами одного RTSP-потока. +Хочешь подключить cuframes к своему проекту? Выбери guide по типу integration'а: + +## Готовые reference guides + +| Тип integration'а | Guide | Reference deployment | +|---|---|---| +| **Frigate NVR** (через FFmpeg `cuframes://` demuxer) | [integrations/frigate.md](integrations/frigate.md) | Production: Frigate 0.17.1 + RTX 5090 + Dahua HEVC | +| **C++ project** (через `CuframesSource` pattern) | [integrations/cctv-cpp.md](integrations/cctv-cpp.md) | Production: [gx/cctv](https://git.goldix.org/gx/cctv) C++17 processor | +| **Python AI/ML pipeline** (через ctypes wrapper) | [examples/python-consumer/](../examples/python-consumer/) | Skeleton ready; v0.3 даст native bindings | +| **FFmpeg-based custom tool** (своя сборка ffmpeg) | [filter/README.md](../filter/README.md) | Out-of-tree patch + build instructions | ## Целевой сценарий (motivation) -В типичной CCTV-системе один и тот же RTSP-stream декодируется несколько раз: +В типичной CCTV / video-analytics системе один и тот же RTSP-поток +декодируется **несколько раз**: ``` Камера ──► RTSP ──► Frigate (decode #1: detection + recording) @@ -13,13 +22,14 @@ GPU-декодирования между несколькими consumer'ами ─► AI-скрипт (decode #3: классификация / OCR) ``` -На 16 камер × 25 fps × 3 consumer'а = 1200 NVDEC-операций/сек. RTX 5090 имеет -~3 NVDEC-движка, но шина PCIe и memory bandwidth становятся узким местом. +На 16 камер × 25 fps × 3 consumer'а = **1200 NVDEC operations/sec**. RTX 5090 +имеет ~3 NVDEC-движка с capacity ~50 FHD25 streams → загрузка близка к лимиту, +плюс tax на PCIe bandwidth и memory. С cuframes: ``` - Камера ──► cuframes-rtsp-source ──► CUDA frame в /dev/shm + cudaIpcEvent + Камера ──► cuframes-rtsp-source ──► CUDA frame в VRAM + IPC handles │ ├──► Frigate (zero-copy) ├──► mosaic-сервер (zero-copy) @@ -27,242 +37,36 @@ GPU-декодирования между несколькими consumer'ами ``` Decode выполняется **один раз** на источник, потребители получают тот же CUDA -device pointer без копий. +device pointer без копий. **3× меньше NVDEC operations** на том же setup'е. -## Текущие limitations v0.1 +## Текущие ограничения (v0.1) -- **Frigate** (по состоянию на 0.17) **не имеет** plugin-точки для приёма - готовых CUDA-frames. Чтобы убрать Frigate decode полностью, нужен: - - либо FFmpeg-filter `vf_cuda_ipc_input` (planned для cuframes v0.2 — требует - patch FFmpeg upstream и пересборку Frigate's bundled ffmpeg), - - либо Frigate-plugin (требует upstream работы с командой Frigate). -- В v0.1 практическое улучшение: **исключить decode для всех custom consumer'ов - кроме Frigate** (то есть cctv-processor, AI-скрипты — на cuframes; Frigate - остаётся как есть, со своим decode). +- **Decoded frame sharing only** (не encoded). Для `record` path в Frigate + (mux без decode) consumer всё ещё открывает свой RTSP — это решит **v0.2 + encoded packet sharing** (см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2)). -Это уже даёт значительную экономию: было 1×Frigate + N×consumer decode'ов, -стало 1×Frigate + 1×cuframes-rtsp-source (один на все consumer'ы). +- **NV12 frame format only**. Other formats (YUV420P, RGB) — v0.2. -## Сценарий 1: cuframes-rtsp-source + cctv-processor (FRIGATE остаётся) +- **GPU → CPU copy** в FFmpeg demuxer'е (`cudaMemcpy2DAsync`). Zero-copy через + `AVHWFramesContext` — v0.2. -### docker-compose.yml +- **Cross-container CUDA IPC** требует shared `ipc + pid` namespace. Если + consumer использует s6-overlay (как Frigate) — pid не shareable, нужен + workaround (см. [integrations/frigate.md](integrations/frigate.md) + troubleshooting). -```yaml -services: - # Один источник на камеру — публикует декодированный поток через cuframes IPC - cuframes-cam-parking: - image: gx/cuframes-rtsp-source:0.1 - restart: unless-stopped - runtime: nvidia - environment: - NVIDIA_VISIBLE_DEVICES: all - NVIDIA_DRIVER_CAPABILITIES: compute,video,utility - # CRITICAL: --ipc=shareable для cross-container CUDA IPC - ipc: shareable - shm_size: 1g - volumes: - - cuframes_sock:/run/cuframes - command: - - --rtsp=rtsp://admin:${CAM_PASS}@192.168.88.98:554/cam/realmonitor?channel=1&subtype=0 - - --key=cam-parking - - --ring=6 - - --realtime # не нужен для RTSP (real-time источник), оставлен для file:// +- **Только Linux + NVIDIA GPU** compute capability ≥ 7.5 (Turing+). - # Frigate (как и был — со своим decode на main+sub streams) - frigate: - image: ghcr.io/blakeblackshear/frigate:stable-tensorrt - # ... как обычно +## Production reference deployments - # cctv-processor — подписывается на cuframes (без отдельного RTSP decode) - cctv-backend: - image: gx/cctv-processor:cuda - restart: unless-stopped - runtime: nvidia - # CRITICAL: shared IPC + PID namespace с publisher'ом (см. ниже) - ipc: container:cuframes-cam-parking - pid: container:cuframes-cam-parking - volumes: - - cuframes_sock:/run/cuframes:ro - environment: - # cuframes-keys для backend'а: - CCTV_SOURCES: cuframes:cam-parking,cuframes:cam-front-gate,... +| Setup | Версия | Где смотреть | +|---|---|---| +| 1 publisher (1× NVDEC) → Frigate (detect) + cctv-backend (motion+grid→RTSP→TV) | v0.1.0 | [BENCHMARKS.md](../BENCHMARKS.md), [integrations/frigate.md](integrations/frigate.md) | -volumes: - cuframes_sock: -``` +## Roadmap для v0.2+ -**Важно — оба флага обязательны** для cross-container CUDA IPC: +Полный roadmap — [ROADMAP.md](../ROADMAP.md). Highlights: -| Флаг | Зачем | -|---|---| -| `ipc: container:` | shared `/dev/shm` (нужен для `shm_open` под header/sockets) | -| `pid: container:` | CUDA driver валидирует IPC peer через `/proc//...`; без этого `cudaIpcOpenEventHandle` падает с `invalid device context` | - -Альтернативы: -- Запускать consumer внутри того же container'а через `docker exec` (наследует все namespaces) — удобно для отладки. -- `--ipc=host --pid=host` — убирает namespacing вообще, но ослабляет изоляцию (не рекомендуется в production). - -### Изменения в cctv-processor - -Нужно добавить новый Source-тип (рядом с RtspSource) — `CuframesSource`: - -```cpp -// cpp/apps/cctv-processor/src/sources/cuframes_source.hpp -#include - -class CuframesSource : public IVideoSource { -public: - CuframesSource(const std::string &key) : key_(key) { - cuframes::SubscriberOptions opt; - opt.key = key; - opt.consumer_name = "cctv-processor"; - opt.mode = CUFRAMES_MODE_NEWEST_ONLY; - sub_ = std::make_unique(opt); - cudaStreamCreate(&stream_); - } - - // Вызывается processing-loop'ом - std::optional nextFrame() override { - auto f = sub_->next(stream_, 100); // 100ms timeout - if (!f) return std::nullopt; - // cudaStreamWaitEvent уже сделан внутри next() — frame готов на stream_ - return GpuFrame{ - .cuda_ptr = f->cuda_ptr(), - .width = f->width(), - .height = f->height(), - .pitch_y = f->pitch_y(), - .pitch_uv = f->pitch_uv(), - .seq = f->seq(), - .pts_ns = f->pts_ns(), - .stream = stream_, - ._release = std::move(f), // RAII release при destroy - }; - } - -private: - std::string key_; - std::unique_ptr sub_; - cudaStream_t stream_; -}; -``` - -Конфиг `cameras.json` — добавить альтернативный source-тип: - -```jsonc -{ - "cameras": [ - { - "id": "parking", - "source_type": "cuframes", // вместо "rtsp" - "cuframes_key": "cam-parking", - // rtsp_url больше не нужен — он используется cuframes-rtsp-source'ом - } - ] -} -``` - -## Сценарий 2: AI-скрипт на Python (subscriber) - -Python-bindings — в Phase 3 cuframes. Сейчас простой workaround через -ctypes: - -```python -import ctypes -lib = ctypes.CDLL("libcuframes.so") -# ... wrap нужные функции — см. include/cuframes/cuframes.h -``` - -Или: writer simple C-обёртку, которая принимает callback и публикует -данные через ZMQ / shared memory в python-process. - -## Сценарий 3: Замена Frigate decode (v0.2+) - -Целевой сценарий — Frigate тоже подписан на cuframes. Реализуется через -один из двух путей: - -### Путь A: FFmpeg filter - -Добавить out-of-tree filter `vf_cuda_ipc_input` который читает кадр из -cuframes ring и эмитит AVFrame в pipeline. Frigate использует ffmpeg для -RTSP/decode — заменяем "RTSP→decode→detect" на -"cuframes_ipc_input→detect" (без decode'а вообще). - -Требования: -- Patch ffmpeg sources (libavfilter/vf_cuda_ipc_input.c + Makefile) -- Сборка кастомного Frigate-образа с patched ffmpeg -- Тестирование на совместимость с Frigate's pipeline assumptions - -### Путь B: Frigate plugin - -Engage с upstream Frigate чтобы добавить custom Source-type ("cuframes://"). -Это требует Python-API изменений в Frigate's source layer. - -## Verification checklist - -После настройки убедитесь: - -```bash -# 1. Publisher запущен и socket существует -ls -la /run/cuframes/cam-parking.sock -ls -la /dev/shm/cuframes-cam-parking - -# 2. Контейнеры в одном IPC и PID namespace -docker inspect cuframes-cam-parking cctv-backend \ - -f '{{.Name}} ipc={{.HostConfig.IpcMode}} pid={{.HostConfig.PidMode}}' -# Publisher: ipc=shareable pid=(default) -# Consumer: ipc=container:cuframes-cam-parking pid=container:cuframes-cam-parking - -# 3. Subscriber connect успешен -docker exec cctv-backend /usr/local/bin/sub_count --key cam-parking --max-frames 10 -# Ожидаем: -# [sub_count] connected to 'cuframes-cam-parking' -# [sub_count] received=10 gaps=0 elapsed=0.4s avg_fps=25 - -# 4. NVDEC utilization — должно быть N decodes, а не N*M -nvidia-smi dmon -s u -# Колонка %dec должна показать decode-нагрузку одного instance на камеру -``` - -## Troubleshooting - -### `Subscriber::create: timeout` -Subscriber не нашёл publisher. Причины: -- Publisher не запущен или crashed — проверь `docker logs cuframes-cam-parking` -- Socket-файл не volumes'нут в consumer-контейнер — добавь `volumes: - - cuframes_sock:/run/cuframes:ro` в consumer'е -- IPC namespace не совпадает — см. checklist пункт 2 - -### `cudaIpcOpenEventHandle: invalid device context` -Проявляется в **отдельном** consumer-container'е после успешного handshake (socket -открыт, header валиден, но open event handle не проходит). - -Причина: CUDA driver валидирует sender'а IPC peer'а через `/proc`. Если PID -namespace не совпадает, sender невидим — context считается невалидным. - -Fix: добавить `pid: container:` в consumer's compose service (рядом -с `ipc: container:`). Проверено на CUDA 13.0 + driver 555+. - -### `cudaIpcOpenMemHandle returned 'invalid device pointer'` -- Контейнеры в РАЗНЫХ ipc namespace — должны быть в одном (через - `ipc: container:` или общий `ipc: shareable`) -- Subscriber работает на другом CUDA device — `--cuda-device` должен совпадать - у publisher и subscriber (одно и то же физическое GPU) - -### Высокая latency (>50ms tail) -- Subscriber slow — frames копятся в ring, по политике DROP_OLDEST они - пропускаются. Используй `CUFRAMES_MODE_NEWEST_ONLY` (default) — это нормально - для real-time системы. -- При STRICT_ORDER + STRICT_WAIT — slow consumer блокирует publisher. Не - рекомендуется для CCTV. - -### Frigate показывает чёрный экран после интеграции -- Frigate не подключён к cuframes (v0.1 — это not yet supported). В v0.1 - Frigate должен оставаться на своём RTSP decode (см. Сценарий 1). - -## Roadmap - -- **v0.1** (текущая): standalone publisher/subscriber, C/C++ API, examples. -- **v0.2**: FFmpeg filter `vf_cuda_ipc_input` (out-of-tree), Python bindings. -- **v0.3**: NVENC-bridge для re-encode подписчиков, Frigate plugin - proof-of-concept. -- **v1.0**: stable ABI, multi-GPU, documented Frigate integration. +- **v0.2**: encoded packet sharing (Frigate record без второго RTSP), FFmpeg upstream PR, publisher-side resize для устранения scale_cuda dependency +- **v0.3**: pybind11 Python bindings, Jetson/arm64 support +- **v1.0**: stable ABI, multi-GPU, env-based credentials diff --git a/docs/integrations/cctv-cpp.md b/docs/integrations/cctv-cpp.md new file mode 100644 index 0000000..68657a0 --- /dev/null +++ b/docs/integrations/cctv-cpp.md @@ -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; + 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 файл diff --git a/docs/integrations/frigate.md b/docs/integrations/frigate.md new file mode 100644 index 0000000..3ddd03c --- /dev/null +++ b/docs/integrations/frigate.md @@ -0,0 +1,305 @@ +# Frigate integration + +Полный production-tested guide для интеграции cuframes с +[Frigate NVR](https://github.com/blakeblackshear/frigate). На основе реального +deployment (Frigate 0.17.1-tensorrt + RTX 5090 + Dahua HEVC камеры). + +## Что вы получаете + +- **Один NVDEC decode на камеру** вместо одного у Frigate + одного у каждого + другого consumer'а (cctv-processor, AI-скрипт, mosaic-сервер). +- Frigate видит decoded frames через **обычный FFmpeg URL** — никакого fork'а + Frigate-кода. Frigate сам не подозревает что под капотом cuframes. + +## Что вы НЕ получаете в v0.1 + +- **Record path** (`-c:v copy` для архива) — этот path в Frigate всё ещё через + свой отдельный RTSP. v0.2 cuframes решит это через encoded packet sharing + (см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2)). +- Hwaccel CUDA filters для detect resize (`scale_cuda`) — наш minimal FFmpeg + собран без `--enable-cuda-llvm` (не работает на glibc < 2.38 что у Debian 12, + на котором Frigate base). Workaround: `hwaccel_args: []` в config → CPU + scale (cost ~5-10% CPU на FHD25). + +## Архитектура + +``` +Camera RTSP ──► cuframes-rtsp-source ──► [NVDEC ─► NV12 in CUDA IPC] + │ + ├──► Frigate (ffmpeg -f cuframes) → detect + ├──► cctv-processor (CuframesSource) → motion+mosaic + └──► AI-script (Python ctypes) → inference +``` + +## Требования + +| | Минимум | Note | +|---|---|---| +| NVIDIA driver | 555+ | для CUDA 12 runtime | +| CUDA Toolkit (для build patched FFmpeg) | 12.4+ | host или builder container | +| GPU compute capability | ≥ 7.5 | требование CUDA IPC | +| OS на target (Frigate runtime) | Debian 12 bookworm | glibc 2.36 — это база Frigate `stable-tensorrt` | +| OS на builder | Ubuntu 22.04 (glibc 2.35) | forward-compat с Debian 12 | +| docker buildx | latest | для multi-stage build | + +## Шаг 1 — Build patched Frigate image + +Cuframes integration требует patched FFmpeg внутри Frigate с `cuframes://` +demuxer. Самый простой путь — собрать overlay image поверх existing Frigate. + +### 1.1. Минимальный Dockerfile (Debian 12 builder + custom FFmpeg) + +```dockerfile +# Build patched FFmpeg на Debian 12 (glibc-совместимо с Frigate runtime) +FROM debian:bookworm AS builder +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake git nasm pkg-config ca-certificates wget patch ninja-build \ + libssl-dev libx264-dev libx265-dev libnuma-dev zlib1g-dev \ + libfreetype-dev libfribidi-dev libharfbuzz-dev libfontconfig-dev \ + libvpx-dev libopus-dev libmp3lame-dev libvorbis-dev libtheora-dev libwebp-dev \ + libaom-dev libdav1d-dev libsvtav1enc-dev \ + libssh-dev librist-dev libsrt-openssl-dev \ + libdrm-dev libva-dev libxcb1-dev \ + && rm -rf /var/lib/apt/lists/* + +# CUDA toolkit 12.x +RUN wget -q https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb \ + && dpkg -i cuda-keyring_1.1-1_all.deb && rm cuda-keyring_1.1-1_all.deb \ + && apt-get update && apt-get install -y --no-install-recommends cuda-toolkit-12-6 \ + && rm -rf /var/lib/apt/lists/* +ENV PATH=/usr/local/cuda/bin:$PATH + +# nv-codec-headers (для FFmpeg ffnvcodec/nvenc/nvdec) +RUN git clone --depth 1 --branch n12.2.72.0 https://github.com/FFmpeg/nv-codec-headers.git /tmp/nvc \ + && make -C /tmp/nvc install && rm -rf /tmp/nvc + +# Build libcuframes (static install в /opt/cuframes) +RUN git clone --depth 1 https://git.goldix.org/gx/cuframes.git /src/cuframes \ + && cmake -B /src/cuframes/build -S /src/cuframes -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \ + -DBUILD_EXAMPLES=OFF -DBUILD_TOOLS=OFF \ + && cmake --build /src/cuframes/build -j"$(nproc)" \ + && cmake --install /src/cuframes/build --prefix /opt/cuframes + +# Clone patched FFmpeg fork (либо upstream + apply patch — см. filter/README.md) +RUN git clone --depth 1 --branch n7.1-cuframes \ + https://git.goldix.org/gx/ffmpeg-patched.git /src/ffmpeg + +# Configure (minimal-but-functional для Frigate) +RUN cd /src/ffmpeg && ./configure \ + --prefix=/opt/ffmpeg \ + --enable-gpl --enable-version3 --enable-nonfree \ + --enable-libcuframes \ + --enable-libx264 --enable-libx265 \ + --enable-libvpx --enable-libopus --enable-libmp3lame \ + --enable-libvorbis --enable-libtheora --enable-libwebp \ + --enable-libaom --enable-libdav1d --enable-libsvtav1 \ + --enable-libfreetype --enable-libfribidi --enable-libharfbuzz \ + --enable-libssh --enable-librist --enable-libsrt \ + --enable-openssl \ + --enable-ffnvcodec --enable-cuvid --enable-nvenc --enable-nvdec \ + --extra-cflags="-I/opt/cuframes/include -I/usr/local/cuda/include" \ + --extra-ldflags="-L/opt/cuframes/lib -L/usr/local/cuda/lib64" \ + --extra-libs="-lcudart -lpthread -lrt -lm" \ + --disable-doc --disable-htmlpages --disable-manpages +RUN cd /src/ffmpeg && make -j"$(nproc)" && make install + +# ─── Runtime: Frigate + наши binaries поверх ────────────────────────── +FROM ghcr.io/blakeblackshear/frigate:stable-tensorrt + +# Missing dynamic .so которые требует наш patched ffmpeg (Frigate image их не имеет — +# bundled статически собран без них в DT_NEEDED) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libharfbuzz0b libfribidi0 librist4 libsrt1.5-openssl libssh-4 \ + libvpx7 libwebpmux3 libwebp7 libdav1d6 libaom3 libmp3lame0 \ + libsvtav1enc1 libtheora0 libvorbis0a libvorbisenc2 \ + libx264-164 libx265-199 libopus0 \ + && rm -rf /var/lib/apt/lists/* + +# Replace bundled ffmpeg (оригинал backup'нем под .orig) +RUN cp /usr/lib/ffmpeg/7.0/bin/ffmpeg /usr/lib/ffmpeg/7.0/bin/ffmpeg.orig \ + && cp /usr/lib/ffmpeg/7.0/bin/ffprobe /usr/lib/ffmpeg/7.0/bin/ffprobe.orig +COPY --from=builder /opt/ffmpeg/bin/ffmpeg /usr/lib/ffmpeg/7.0/bin/ffmpeg +COPY --from=builder /opt/ffmpeg/bin/ffprobe /usr/lib/ffmpeg/7.0/bin/ffprobe +COPY --from=builder /opt/cuframes/lib/libcuframes.so.0.1.0 /usr/local/lib/ +RUN cd /usr/local/lib && ln -sf libcuframes.so.0.1.0 libcuframes.so.0 \ + && ln -sf libcuframes.so.0 libcuframes.so && ldconfig + +# Build-time smoke: ldd resolved + cuframes demuxer registered +RUN ldd /usr/lib/ffmpeg/7.0/bin/ffmpeg | grep -q "not found" && exit 1 || true +RUN /usr/lib/ffmpeg/7.0/bin/ffmpeg -hide_banner -formats | grep -q cuframes \ + && echo "OK: cuframes demuxer registered in Frigate image" +``` + +Build: +```bash +docker build -t local/frigate-cuframes:latest -f Dockerfile.frigate . +``` + +Размер ~10 GB (наследует Frigate `stable-tensorrt` ~9 GB). + +## Шаг 2 — docker-compose: publisher + Frigate + +```yaml +services: + # Один publisher на камеру — единственный source RTSP, делает 1× NVDEC. + cuframes-pub-parking: + image: git.goldix.org/gx/cuframes:0.1 # либо local build из filter/Dockerfile.runtime + container_name: cuframes-pub-parking + restart: unless-stopped + runtime: nvidia + # CRITICAL: ipc=shareable — Frigate и другие consumers подсоединяются через + # ipc: container:cuframes-pub-parking + ipc: shareable + shm_size: 256m + environment: + NVIDIA_VISIBLE_DEVICES: all + NVIDIA_DRIVER_CAPABILITIES: compute,video,utility + volumes: + - cuframes_sock:/run/cuframes + command: + - /usr/local/bin/cuframes-rtsp-source + - --rtsp + - "rtsp://admin:${CAM_PASS}@cam-parking-ip:554/cam/realmonitor?channel=1&subtype=1" + - --key + - cam-parking + - --ring + - "6" + - --verbose + + frigate: + image: local/frigate-cuframes:latest + container_name: frigate + restart: unless-stopped + depends_on: + cuframes-pub-parking: + condition: service_started + runtime: nvidia + privileged: true + shm_size: 512m + # CUDA IPC c publisher'ом: shared /dev/shm + # WARN: pid намерено НЕ share'ится — Frigate использует s6-overlay, + # которое требует PID 1 в своём namespace. + ipc: "container:cuframes-pub-parking" + environment: + FRIGATE_RTSP_PASSWORD: "${FRIGATE_RTSP_PASSWORD}" + NVIDIA_VISIBLE_DEVICES: all + NVIDIA_DRIVER_CAPABILITIES: compute,video,utility + ports: + - "5000:5000" + - "8971:8971" + volumes: + - cuframes_sock:/run/cuframes:ro + - ./config/config.yml:/config/config.yml:ro + - /home/user/frigate-media:/media/frigate + # ... остальные volumes как обычно + +volumes: + cuframes_sock: +``` + +## Шаг 3 — Frigate config.yml + +Ключевые отличия от стандартного config: + +```yaml +ffmpeg: + # ВАЖНО: hwaccel cuda отключаем (наш ffmpeg без cuda-llvm → нет scale_cuda). + # Detect-path использует CPU scale, но decode уже done у publisher'а. + hwaccel_args: [] + output_args: + record: preset-record-generic-audio-aac + +cameras: + parking_overview: + enabled: true + ffmpeg: + inputs: + # main (full-res) — только запись в архив через прямой RTSP + # (decode у Frigate НЕ происходит — это `-c:v copy` мux) + - path: rtsp://admin:${FRIGATE_RTSP_PASSWORD}@cam-parking-ip:554/cam/realmonitor?channel=1&subtype=0 + roles: [record] + + # sub-stream → через cuframes (decoded у publisher'а, без второго NVDEC у Frigate) + - path: cuframes://cam-parking + input_args: -f cuframes + roles: [detect] + detect: + width: 640 + height: 480 + fps: 5 +``` + +После v0.2 cuframes (encoded packet sharing) record-path тоже мoжет +переключиться на `cuframes_packets://cam-parking` — тогда **никакого RTSP в +Frigate config'е вообще**. + +## Шаг 4 — Run + verify + +```bash +docker compose up -d +docker logs -f frigate +``` + +Что искать в logs: +- `[INFO] Camera processor started for parking_overview` — normal startup +- НЕТ `[ERROR] Ffmpeg process crashed` — если есть, посмотри + [Troubleshooting](#troubleshooting) +- В `nvidia-smi dmon -s u` колонка `%dec` должна показывать ~1-2% на одну + камеру (это publisher), Frigate сам не decode'ит cuframes input + +```bash +# Проверить что Frigate реально читает cuframes: +docker exec frigate ps -ef | grep ffmpeg | grep cuframes +# Должна быть линия вида: +# ffmpeg ... -f cuframes -i cuframes://cam-parking -r 5 -vf fps=5,scale=640:480 ... +``` + +## Troubleshooting + +### `s6-overlay-suexec: fatal: can only run as pid 1` + +Появляется если попытались добавить `pid: container:cuframes-pub-parking` в +Frigate service. Frigate's s6-overlay strict требует PID 1. + +**Fix**: убрать `pid:` из compose. Если только `ipc:` shared — большинство +случаев работают (Frigate подсоединяется первым и его CUDA context служит +для последующих). + +**Альтернатива**: запустить Frigate с собственным namespace но дублировать +publisher socket через bind-mount. Frigate сам управляется first CUDA context. + +### `[AVFilterGraph] No such filter: 'scale_cuda'` + +Frigate config имеет `hwaccel_args: preset-nvidia` (default). Наш patched +ffmpeg собран без `--enable-cuda-llvm` (не работает на glibc < 2.38). Эта +опция компилирует CUDA filters, включая `scale_cuda`. + +**Fix**: `hwaccel_args: []` в config.yml. CPU scale (5-10% CPU per FHD25 камера). + +**Real fix** (planned): cuframes v0.2 — publisher сам делает resize до detect-size +и публикует pre-scaled frames. Тогда Frigate не нуждается в scale_cuda. + +### `cudaIpcOpenEventHandle: invalid device context` + +Consumer container не имеет shared pid namespace с publisher'ом → CUDA driver +не валидирует IPC peer. + +**Fix для cross-container CUDA IPC**: `pid: container:` + `ipc: +container:`. Для Frigate этот fix недоступен (см. предыдущий пункт). +Workaround — поднять Frigate первым после publisher (race window) или использовать +encoded packet path (v0.2). + +### `Nonmatching transport in server reply` от RTSP-output Frigate + +Не относится к cuframes — это нормальное поведение Frigate's go2rtc для +TCP transport. TV/VLC обычно использует UDP — оно работает. + +## См. также + +- [filter/README.md](../../filter/README.md) — детали FFmpeg demuxer + patch +- [docs/integration.md](../integration.md) — общий integration guide +- [BENCHMARKS.md](../../BENCHMARKS.md) — production-measured результаты +- [ROADMAP.md](../../ROADMAP.md) — v0.2 что улучшит для Frigate diff --git a/examples/frigate-compose/.env.example b/examples/frigate-compose/.env.example new file mode 100644 index 0000000..05eb498 --- /dev/null +++ b/examples/frigate-compose/.env.example @@ -0,0 +1,8 @@ +# Скопировать в .env (не commit'ить!) +# .env должен быть в .gitignore + +# Камеры: пароли admin user'а на Dahua/Hikvision/etc +CAM_PARKING_PASS=changeme + +# Frigate API/UI auth password +FRIGATE_RTSP_PASSWORD=changeme diff --git a/examples/frigate-compose/README.md b/examples/frigate-compose/README.md new file mode 100644 index 0000000..4ce4f4f --- /dev/null +++ b/examples/frigate-compose/README.md @@ -0,0 +1,61 @@ +# examples/frigate-compose + +Reference docker-compose для Frigate + cuframes integration. **НЕ** копировать +в production бездумно — это шаблон, адаптируй под свою инфру (IP-адреса камер, +пароли, mount paths, network). + +## Quickstart + +1. Build patched Frigate image (single-time setup, ~15 мин): + ```bash + # См. docs/integrations/frigate.md, Шаг 1 — там полный Dockerfile. + docker build -t local/frigate-cuframes:latest -f Dockerfile.frigate . + ``` + +2. Pull cuframes publisher image: + ```bash + docker pull git.goldix.org/gx/cuframes:0.1 + # либо собрать local: docker build -t local/cuframes:0.1 -f docker/Dockerfile.runtime ../.. + ``` + +3. Скопировать .env: + ```bash + cp .env.example .env + $EDITOR .env # подставь свои camera passwords + ``` + +4. Адаптировать `docker-compose.yml`: + - `parking-cam-ip` → реальный IP камеры + - `--key cam-parking` → имя по вкусу (должно matche'ить config.yml `cuframes://`) + - `cam-parking` в Frigate config → так же matched + +5. Адаптировать `config/config.yml`: + - детектор (cpu / onnx / tensorrt) + - пути к media + - дополнительные камеры если нужно + +6. Run: + ```bash + docker compose up -d + docker logs -f frigate + # UI: http://localhost:5000 (internal) либо https://localhost:8971 (auth) + ``` + +## Что демонстрирует + +- Один publisher (`cuframes-pub-parking`) делает 1× NVDEC на parking-камеру +- Frigate подключается к publisher через `ipc:container:` + `cuframes://` URL +- Frigate **не** делает свой NVDEC для detect-path — берёт готовые NV12 frames + +## Что НЕ демонстрирует + +- Record path — Frigate всё ещё открывает второй RTSP к камере (для архива + `-c:v copy` mux). v0.2 cuframes решит через encoded packet sharing + (см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2)) +- Multi-camera setup — добавь больше publisher'ов и camera-blocks в config.yml +- HA/MQTT интеграция — добавь свой mqtt block + +## См. также + +- [docs/integrations/frigate.md](../../docs/integrations/frigate.md) — полный walkthrough +- [docs/integration.md](../../docs/integration.md) — общая интеграция diff --git a/examples/frigate-compose/config/config.yml b/examples/frigate-compose/config/config.yml new file mode 100644 index 0000000..8eff703 --- /dev/null +++ b/examples/frigate-compose/config/config.yml @@ -0,0 +1,49 @@ +# Minimal Frigate config с cuframes integration. +# Полный guide: docs/integrations/frigate.md + +mqtt: + enabled: false + +detectors: + # Замени на свой detector (tensorrt / onnx / cpu). Здесь — placeholder. + cpu: + type: cpu + +# CRITICAL: hwaccel cuda отключён — наш patched ffmpeg без --enable-cuda-llvm +# (не работает на glibc < 2.38 что у Debian 12, на котором Frigate runtime). +# Без cuda-llvm нет scale_cuda filter. Detect-path использует CPU scale, но +# decode уже сделан у publisher'а — net выигрыш всё равно. +ffmpeg: + hwaccel_args: [] + output_args: + record: preset-record-generic-audio-aac + +cameras: + parking_overview: + enabled: true + friendly_name: Парковка + ffmpeg: + inputs: + # main (full-res) — только запись в архив через прямой RTSP (`-c:v copy`, no decode у Frigate) + # После cuframes v0.2 этот path тоже может через cuframes_packets:// (encoded share) + - path: rtsp://admin:${FRIGATE_RTSP_PASSWORD}@parking-cam-ip:554/cam/realmonitor?channel=1&subtype=0 + roles: [record] + + # sub-stream → через cuframes (decoded у publisher'а, без второго NVDEC) + - path: cuframes://cam-parking + input_args: -f cuframes + roles: [detect] + detect: + width: 640 + height: 480 + fps: 5 + +record: + enabled: true + retain: + days: 7 + +snapshots: + enabled: true + retain: + default: 7 diff --git a/examples/frigate-compose/docker-compose.yml b/examples/frigate-compose/docker-compose.yml new file mode 100644 index 0000000..9a0bae0 --- /dev/null +++ b/examples/frigate-compose/docker-compose.yml @@ -0,0 +1,73 @@ +# Reference docker-compose для Frigate + cuframes integration. +# Полный guide: docs/integrations/frigate.md +# +# Что нужно подготовить заранее: +# 1. Build local image local/frigate-cuframes:latest по Dockerfile.frigate +# (см. docs/integrations/frigate.md, Шаг 1) +# 2. Pull cuframes runtime image: +# docker pull git.goldix.org/gx/cuframes:0.1 # либо собрать local +# 3. Скопировать config/config.yml (placeholder в config/ рядом) +# 4. .env с CAM_PARKING_PASS=... и FRIGATE_RTSP_PASSWORD=... +# +# Запуск: +# docker compose up -d +# # UI: http://host:5000 (internal, без auth) либо https://host:8971 (with auth) + +services: + # 1× publisher на камеру — single source of RTSP + NVDEC + cuframes-pub-parking: + image: git.goldix.org/gx/cuframes:0.1 + container_name: cuframes-pub-parking + restart: unless-stopped + runtime: nvidia + ipc: shareable + shm_size: 256m + environment: + NVIDIA_VISIBLE_DEVICES: all + NVIDIA_DRIVER_CAPABILITIES: compute,video,utility + volumes: + - cuframes_sock:/run/cuframes + command: + - /usr/local/bin/cuframes-rtsp-source + - --rtsp + # Используем sub-stream для detect-path (lighter resolution, тот же camera load) + - "rtsp://admin:${CAM_PARKING_PASS}@parking-cam-ip:554/cam/realmonitor?channel=1&subtype=1" + - --key + - cam-parking + - --ring + - "6" + - --verbose + + frigate: + image: local/frigate-cuframes:latest # см. docs/integrations/frigate.md Шаг 1 + container_name: frigate + restart: unless-stopped + depends_on: + cuframes-pub-parking: + condition: service_started + runtime: nvidia + privileged: true + shm_size: 512m + # WARN: только ipc share — pid НЕ shared (Frigate's s6-overlay требует PID 1). + # Frigate подсоединяется к first CUDA context publisher'а в shared /dev/shm. + ipc: "container:cuframes-pub-parking" + environment: + FRIGATE_RTSP_PASSWORD: "${FRIGATE_RTSP_PASSWORD}" + NVIDIA_VISIBLE_DEVICES: all + NVIDIA_DRIVER_CAPABILITIES: compute,video,utility + ports: + - "5000:5000" # UI без auth (internal, не expose external!) + - "8971:8971" # UI с HTTPS + auth + - "8554:8554" # RTSP restream (go2rtc) + - "8555:8555/tcp" + - "8555:8555/udp" + volumes: + - cuframes_sock:/run/cuframes:ro + - ./config/config.yml:/config/config.yml:ro + - ./media:/media/frigate + - type: tmpfs + target: /tmp/cache + tmpfs: { size: 1000000000 } + +volumes: + cuframes_sock: diff --git a/examples/python-consumer/README.md b/examples/python-consumer/README.md new file mode 100644 index 0000000..b7c56b0 --- /dev/null +++ b/examples/python-consumer/README.md @@ -0,0 +1,78 @@ +# examples/python-consumer + +Reference Python consumer для cuframes через `ctypes` wrapper. + +## Use case + +AI/ML pipeline (PyTorch / ONNX / TensorRT) которому нужны декодированные кадры +с камер. Без cuframes — каждый Python скрипт открывает RTSP + decode сам. +С cuframes — подписывается на готовые NV12 frames от publisher'а. + +## Запуск + +```bash +# Publisher должен быть запущен (см. tools/cuframes-rtsp-source или Docker image) +cuframes-rtsp-source --rtsp rtsp://admin:pw@cam-ip:554/... --key cam-parking & + +# Consumer (same host, либо same docker namespace — см. требования ниже) +python3 cuframes_consumer.py --key cam-parking --max-frames 100 +``` + +Ожидаемый output: +``` +[consumer] connected to 'cam-parking' +[consumer] first frame: 640x480 NV12, pitch_y=640, pitch_uv=640, cuda_ptr=0x... +[consumer] received=25 seq=42 pts_ms=... +... +=== RESULT === +received: 100 / 100 +elapsed: 3.96s +avg_fps: 25.03 +``` + +## Что этот пример НЕ делает + +- **НЕ копирует** GPU NV12 frame на host — `cuda_ptr` это raw CUDA device pointer. + Для реальной работы нужно: + - `pycuda` / `cupy` / `cuda-python` библиотека для CUDA memcpy + - либо передать `cuda_ptr` напрямую в GPU-aware ML framework (PyTorch's + `torch.cuda.IntTensor.from_dlpack` etc.) + +- **НЕ конвертирует** NV12 → RGB. Используй `cv2.cvtColor(nv12, cv2.COLOR_YUV2RGB_NV12)` + на host или GPU-side conversion. + +- **НЕ обрабатывает** inference — это skeleton, в твоём pipeline replace + comment-block `### ВАШ ML PIPELINE ЗДЕСЬ ###` с актуальным кодом. + +## Требования + +| | Значение | +|---|---| +| Python | 3.8+ | +| `libcuframes.so.0` | в `LD_LIBRARY_PATH` (либо `/usr/local/lib`) | +| Publisher running | да, с matching `--key` | +| Same IPC namespace | да (host либо `ipc:container:` в docker) | +| Same PID namespace | да (host либо `pid:container:` в docker) | +| NVIDIA GPU + driver | для access `cuda_ptr` (read-only frame от publisher'а) | + +## Docker-style + +```yaml +# В compose рядом с publisher service +ai-pipeline: + image: your-ai-image:cuda + runtime: nvidia + ipc: "container:cuframes-pub-parking" + pid: "container:cuframes-pub-parking" + volumes: + - cuframes_sock:/run/cuframes:ro + environment: + LD_LIBRARY_PATH: /usr/local/lib + command: python3 /app/cuframes_consumer.py --key cam-parking --max-frames 1000000 +``` + +## v0.3 → first-class pybind11 bindings + +Текущий ctypes pattern будет заменён на native pybind11 bindings в v0.3 cuframes +([ROADMAP.md](../../ROADMAP.md)). Тогда API будет более pythonic + zero-copy через +`__cuda_array_interface__` / `dlpack`. diff --git a/examples/python-consumer/cuframes_consumer.py b/examples/python-consumer/cuframes_consumer.py new file mode 100644 index 0000000..dafeae7 --- /dev/null +++ b/examples/python-consumer/cuframes_consumer.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Reference Python consumer для cuframes (через ctypes wrapper). + +До v0.3 (когда появятся первоклассные pybind11 bindings) — это minimal +working pattern для AI/ML скриптов которые хотят подписаться на cuframes IPC. + +Pattern: + 1. subscribe to cuframes (open libcuframes.so via ctypes) + 2. в цикле: получить next() frame + 3. cudaMemcpy → host (через pycuda либо отдельной CUDA-Python библиотекой) + 4. передать в свой ML pipeline (ONNX/TensorRT/PyTorch) + 5. release frame обратно publisher'у + +Limitations: + - Этот skeleton НЕ делает actual CUDA copy (нужна pycuda / cupy / cuda-python) + - Только sync API + - Только NV12 (v0.1) + +Запуск: + python3 cuframes_consumer.py --key cam-parking --max-frames 100 + +Требования (на target host): + - libcuframes.so в LD_LIBRARY_PATH (либо apt install / docker) + - publisher запущен (cuframes-rtsp-source --key cam-parking ...) + - same IPC + PID namespace что publisher (если в docker — ipc:container: + pid:container:) +""" + +import argparse +import ctypes +import sys +import time +from ctypes import c_int, c_int32, c_int64, c_uint64, c_uint32, c_char_p, c_void_p, c_size_t, POINTER, Structure + + +# ─── C API bindings ───────────────────────────────────────────────────── + +# Error codes +CUFRAMES_OK = 0 +CUFRAMES_ERR_TIMEOUT = -7 +CUFRAMES_ERR_WOULD_BLOCK = -11 +CUFRAMES_ERR_DISCONNECTED = -9 + +# Modes +CUFRAMES_MODE_NEWEST_ONLY = 0 +CUFRAMES_MODE_STRICT_ORDER = 1 + +# Pixel format +CUFRAMES_FORMAT_NV12 = 0 + + +class SubscriberConfig(Structure): + """Соответствует C struct cuframes_subscriber_config.""" + _fields_ = [ + ("key", c_char_p), + ("consumer_name", c_char_p), + ("mode", c_int), + ("cuda_device", c_int32), + ("connect_timeout_ms", c_int32), + ("_reserved", c_uint64 * 4), + ] + + +def _load_libcuframes(): + """Загрузить libcuframes.so + bind ctypes signatures.""" + try: + lib = ctypes.CDLL("libcuframes.so.0") + except OSError as e: + sys.stderr.write(f"Cannot load libcuframes.so.0: {e}\n") + sys.stderr.write("Установи libcuframes (см. cuframes README) и убедись что .so в LD_LIBRARY_PATH.\n") + sys.exit(1) + + # cuframes_strerror + lib.cuframes_strerror.argtypes = [c_int] + lib.cuframes_strerror.restype = c_char_p + + # cuframes_subscriber_create + lib.cuframes_subscriber_create.argtypes = [POINTER(SubscriberConfig), POINTER(c_void_p)] + lib.cuframes_subscriber_create.restype = c_int + + # cuframes_subscriber_next (consumer_stream=NULL — sync API, default stream) + lib.cuframes_subscriber_next.argtypes = [c_void_p, c_void_p, POINTER(c_void_p), c_int32] + lib.cuframes_subscriber_next.restype = c_int + + # cuframes_subscriber_release + lib.cuframes_subscriber_release.argtypes = [c_void_p, c_void_p] + lib.cuframes_subscriber_release.restype = c_int + + # cuframes_subscriber_destroy + lib.cuframes_subscriber_destroy.argtypes = [c_void_p] + lib.cuframes_subscriber_destroy.restype = c_int + + # cuframes_frame_* accessors + lib.cuframes_frame_cuda_ptr.argtypes = [c_void_p] + lib.cuframes_frame_cuda_ptr.restype = c_void_p + + lib.cuframes_frame_size.argtypes = [c_void_p, POINTER(c_int32), POINTER(c_int32)] + lib.cuframes_frame_size.restype = None + + lib.cuframes_frame_pitch_y.argtypes = [c_void_p] + lib.cuframes_frame_pitch_y.restype = c_int32 + + lib.cuframes_frame_pitch_uv.argtypes = [c_void_p] + lib.cuframes_frame_pitch_uv.restype = c_int32 + + lib.cuframes_frame_seq.argtypes = [c_void_p] + lib.cuframes_frame_seq.restype = c_uint64 + + lib.cuframes_frame_pts_ns.argtypes = [c_void_p] + lib.cuframes_frame_pts_ns.restype = c_int64 + + return lib + + +# ─── Main consumer loop ──────────────────────────────────────────────── + +def main(): + ap = argparse.ArgumentParser(description="Reference cuframes Python consumer") + ap.add_argument("--key", required=True, help="publisher key (e.g. cam-parking)") + ap.add_argument("--max-frames", type=int, default=100, help="N frames to receive (default 100)") + ap.add_argument("--cuda-device", type=int, default=0) + ap.add_argument("--timeout-ms", type=int, default=1000, help="per-frame timeout") + args = ap.parse_args() + + lib = _load_libcuframes() + + # Configure subscriber + cfg = SubscriberConfig() + cfg.key = args.key.encode("utf-8") + cfg.consumer_name = None # auto-generated + cfg.mode = CUFRAMES_MODE_NEWEST_ONLY + cfg.cuda_device = args.cuda_device + cfg.connect_timeout_ms = 5000 + + sub_handle = c_void_p() + rc = lib.cuframes_subscriber_create(ctypes.byref(cfg), ctypes.byref(sub_handle)) + if rc != CUFRAMES_OK: + sys.stderr.write(f"subscribe failed: {lib.cuframes_strerror(rc).decode()}\n") + sys.exit(1) + + print(f"[consumer] connected to '{args.key}'") + + received = 0 + first_pts = None + start_wall = None + + try: + while received < args.max_frames: + frame_handle = c_void_p() + rc = lib.cuframes_subscriber_next(sub_handle, None, ctypes.byref(frame_handle), + args.timeout_ms) + + if rc == CUFRAMES_ERR_TIMEOUT or rc == CUFRAMES_ERR_WOULD_BLOCK: + continue + if rc == CUFRAMES_ERR_DISCONNECTED: + print(f"[consumer] publisher disconnected — exit") + break + if rc != CUFRAMES_OK or not frame_handle.value: + sys.stderr.write(f"next failed: {lib.cuframes_strerror(rc).decode()}\n") + break + + # Frame metadata + w, h = c_int32(0), c_int32(0) + lib.cuframes_frame_size(frame_handle, ctypes.byref(w), ctypes.byref(h)) + pitch_y = lib.cuframes_frame_pitch_y(frame_handle) + pitch_uv = lib.cuframes_frame_pitch_uv(frame_handle) + cuda_ptr = lib.cuframes_frame_cuda_ptr(frame_handle) + seq = lib.cuframes_frame_seq(frame_handle) + pts_ns = lib.cuframes_frame_pts_ns(frame_handle) + + if first_pts is None: + first_pts = pts_ns + start_wall = time.monotonic() + print(f"[consumer] first frame: {w.value}x{h.value} NV12, " + f"pitch_y={pitch_y}, pitch_uv={pitch_uv}, cuda_ptr=0x{cuda_ptr:x}") + + # ─── ВАШ ML PIPELINE ЗДЕСЬ ──────────────────────────── + # 1. cudaMemcpy NV12 frame → host (или используй pycuda / cupy для in-GPU pipeline) + # 2. NV12 → RGB conversion (CPU либо GPU) + # 3. inference: model(frame) → results + # 4. publish results (mqtt / API / etc) + # + # В этом skeleton — просто counter. + received += 1 + if received % 25 == 0: + print(f"[consumer] received={received} seq={seq} pts_ms={pts_ns // 1_000_000}") + + # CRITICAL: release frame ОБЯЗАТЕЛЬНО — иначе publisher застрянет + # (или drop new frames при ring overflow в STRICT_ORDER mode). + lib.cuframes_subscriber_release(sub_handle, frame_handle) + + finally: + lib.cuframes_subscriber_destroy(sub_handle) + + if received > 1 and start_wall: + elapsed = time.monotonic() - start_wall + fps = (received - 1) / elapsed if elapsed > 0 else 0 + print(f"\n=== RESULT ===") + print(f"received: {received} / {args.max_frames}") + print(f"elapsed: {elapsed:.2f}s") + print(f"avg_fps: {fps:.2f}") + sys.exit(0 if received >= args.max_frames else 1) + + +if __name__ == "__main__": + main()