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>
This commit is contained in:
+38
-234
@@ -1,11 +1,20 @@
|
|||||||
# Integration guide
|
# Integration guide
|
||||||
|
|
||||||
Этот guide описывает, как использовать cuframes для устранения дублирующего
|
Хочешь подключить cuframes к своему проекту? Выбери guide по типу integration'а:
|
||||||
GPU-декодирования между несколькими consumer'ами одного RTSP-потока.
|
|
||||||
|
## Готовые 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)
|
## Целевой сценарий (motivation)
|
||||||
|
|
||||||
В типичной CCTV-системе один и тот же RTSP-stream декодируется несколько раз:
|
В типичной CCTV / video-analytics системе один и тот же RTSP-поток
|
||||||
|
декодируется **несколько раз**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Камера ──► RTSP ──► Frigate (decode #1: detection + recording)
|
Камера ──► RTSP ──► Frigate (decode #1: detection + recording)
|
||||||
@@ -13,13 +22,14 @@ GPU-декодирования между несколькими consumer'ами
|
|||||||
─► AI-скрипт (decode #3: классификация / OCR)
|
─► AI-скрипт (decode #3: классификация / OCR)
|
||||||
```
|
```
|
||||||
|
|
||||||
На 16 камер × 25 fps × 3 consumer'а = 1200 NVDEC-операций/сек. RTX 5090 имеет
|
На 16 камер × 25 fps × 3 consumer'а = **1200 NVDEC operations/sec**. RTX 5090
|
||||||
~3 NVDEC-движка, но шина PCIe и memory bandwidth становятся узким местом.
|
имеет ~3 NVDEC-движка с capacity ~50 FHD25 streams → загрузка близка к лимиту,
|
||||||
|
плюс tax на PCIe bandwidth и memory.
|
||||||
|
|
||||||
С cuframes:
|
С cuframes:
|
||||||
|
|
||||||
```
|
```
|
||||||
Камера ──► cuframes-rtsp-source ──► CUDA frame в /dev/shm + cudaIpcEvent
|
Камера ──► cuframes-rtsp-source ──► CUDA frame в VRAM + IPC handles
|
||||||
│
|
│
|
||||||
├──► Frigate (zero-copy)
|
├──► Frigate (zero-copy)
|
||||||
├──► mosaic-сервер (zero-copy)
|
├──► mosaic-сервер (zero-copy)
|
||||||
@@ -27,242 +37,36 @@ GPU-декодирования между несколькими consumer'ами
|
|||||||
```
|
```
|
||||||
|
|
||||||
Decode выполняется **один раз** на источник, потребители получают тот же CUDA
|
Decode выполняется **один раз** на источник, потребители получают тот же CUDA
|
||||||
device pointer без копий.
|
device pointer без копий. **3× меньше NVDEC operations** на том же setup'е.
|
||||||
|
|
||||||
## Текущие limitations v0.1
|
## Текущие ограничения (v0.1)
|
||||||
|
|
||||||
- **Frigate** (по состоянию на 0.17) **не имеет** plugin-точки для приёма
|
- **Decoded frame sharing only** (не encoded). Для `record` path в Frigate
|
||||||
готовых CUDA-frames. Чтобы убрать Frigate decode полностью, нужен:
|
(mux без decode) consumer всё ещё открывает свой RTSP — это решит **v0.2
|
||||||
- либо FFmpeg-filter `vf_cuda_ipc_input` (planned для cuframes v0.2 — требует
|
encoded packet sharing** (см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2)).
|
||||||
patch FFmpeg upstream и пересборку Frigate's bundled ffmpeg),
|
|
||||||
- либо Frigate-plugin (требует upstream работы с командой Frigate).
|
|
||||||
- В v0.1 практическое улучшение: **исключить decode для всех custom consumer'ов
|
|
||||||
кроме Frigate** (то есть cctv-processor, AI-скрипты — на cuframes; Frigate
|
|
||||||
остаётся как есть, со своим decode).
|
|
||||||
|
|
||||||
Это уже даёт значительную экономию: было 1×Frigate + N×consumer decode'ов,
|
- **NV12 frame format only**. Other formats (YUV420P, RGB) — v0.2.
|
||||||
стало 1×Frigate + 1×cuframes-rtsp-source (один на все consumer'ы).
|
|
||||||
|
|
||||||
## Сценарий 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
|
- **Только Linux + NVIDIA GPU** compute capability ≥ 7.5 (Turing+).
|
||||||
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://
|
|
||||||
|
|
||||||
# Frigate (как и был — со своим decode на main+sub streams)
|
## Production reference deployments
|
||||||
frigate:
|
|
||||||
image: ghcr.io/blakeblackshear/frigate:stable-tensorrt
|
|
||||||
# ... как обычно
|
|
||||||
|
|
||||||
# cctv-processor — подписывается на cuframes (без отдельного RTSP decode)
|
| Setup | Версия | Где смотреть |
|
||||||
cctv-backend:
|
|---|---|---|
|
||||||
image: gx/cctv-processor:cuda
|
| 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) |
|
||||||
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,...
|
|
||||||
|
|
||||||
volumes:
|
## Roadmap для v0.2+
|
||||||
cuframes_sock:
|
|
||||||
```
|
|
||||||
|
|
||||||
**Важно — оба флага обязательны** для cross-container CUDA IPC:
|
Полный roadmap — [ROADMAP.md](../ROADMAP.md). Highlights:
|
||||||
|
|
||||||
| Флаг | Зачем |
|
- **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
|
||||||
| `ipc: container:<publisher>` | shared `/dev/shm` (нужен для `shm_open` под header/sockets) |
|
- **v1.0**: stable ABI, multi-GPU, env-based credentials
|
||||||
| `pid: container:<publisher>` | CUDA driver валидирует IPC peer через `/proc/<pid>/...`; без этого `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 <cuframes/cuframes.hpp>
|
|
||||||
|
|
||||||
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<cuframes::Subscriber>(opt);
|
|
||||||
cudaStreamCreate(&stream_);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вызывается processing-loop'ом
|
|
||||||
std::optional<GpuFrame> 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<cuframes::Subscriber> 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:<publisher>` в consumer's compose service (рядом
|
|
||||||
с `ipc: container:<publisher>`). Проверено на CUDA 13.0 + driver 555+.
|
|
||||||
|
|
||||||
### `cudaIpcOpenMemHandle returned 'invalid device pointer'`
|
|
||||||
- Контейнеры в РАЗНЫХ ipc namespace — должны быть в одном (через
|
|
||||||
`ipc: container:<publisher>` или общий `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.
|
|
||||||
|
|||||||
@@ -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<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 файл
|
||||||
@@ -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:<publisher>` + `ipc:
|
||||||
|
container:<publisher>`. Для 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
|
||||||
@@ -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
|
||||||
@@ -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://<key>`)
|
||||||
|
- `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) — общая интеграция
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -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:<publisher>` в docker) |
|
||||||
|
| Same PID namespace | да (host либо `pid:container:<publisher>` в 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`.
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user