v0.2: encoded packet sharing (для Frigate record path без второго RTSP) #2

Closed
opened 2026-05-18 21:12:33 +01:00 by gx · 2 comments
Owner

Motivation

Текущий cuframes v0.1 раздает decoded NV12 frames через CUDA IPC. Это идеально для consumer'ов которые делают detect/AI/processing.

Но Frigate (как и любой recorder) для record roles делает -c:v copy — берёт encoded H.264/H.265 stream и mux'ит в mp4 без decode. Сейчас Frigate должен открывать второй RTSP connection к камере для record stream (decode skipped, но второй RTSP-link на камеру).

Proposal

Расширить cuframes публиковать encoded packets (H.264/H.265 NAL units) параллельно decoded frames — через отдельный shared ring.

                                ┌─► [encoded packets] ─► cuframes_packets IPC ─► record consumers (mux only, no decode)
Camera RTSP ─► publisher demuxer┤
                                └─► NVDEC ─► [NV12 frames] ─► cuframes IPC ─► detect/AI consumers

Benefits

Без v0.2 С v0.2
RTSP connections на камеру 2+ (Frigate detect + Frigate record + cctv + ...) 1 (только publisher)
Camera load N parallel streams (часто limit 4-5 у Dahua) 1 stream
CPU on RTSP demuxers N (по одному на consumer) 1
NVDEC Уже 1 (cuframes v0.1) 1 (unchanged)

API Sketch

Producer

/* Publisher API extension. EXTERNAL ownership уже есть в v0.1 — теперь для packets.
 * Packets variable-size, поэтому ring — packet-indexed (offsets + size),
 * backed by shared memory (NE CUDA-память — encoded data на host). */

int cuframes_publisher_publish_packet(
    cuframes_publisher_t *pub,
    const void *data,       /* указатель на NAL unit'ы */
    size_t size,
    int64_t pts_ns,
    int64_t dts_ns,
    uint32_t flags          /* AV_PKT_FLAG_KEY etc. */
);

Consumer (sync API)

typedef struct cuframes_packet cuframes_packet_t;

int cuframes_subscriber_next_packet(
    cuframes_subscriber_t *sub,
    cuframes_packet_t **pkt_out,
    int32_t timeout_ms
);

const void *cuframes_packet_data(const cuframes_packet_t *p);
size_t      cuframes_packet_size(const cuframes_packet_t *p);
int64_t     cuframes_packet_pts(const cuframes_packet_t *p);
int64_t     cuframes_packet_dts(const cuframes_packet_t *p);
uint32_t    cuframes_packet_flags(const cuframes_packet_t *p);

int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub, cuframes_packet_t *p);

Same key namespace что v0.1 — но отдельный ring в одной публикации. Subscriber выбирает frames-ring и/или packets-ring при cuframes_subscriber_create() (новая опция).

FFmpeg demuxer extension

cuframes_packets:// — комплементарный к существующему cuframes://. AVPacket'ы выдаются 1-в-1 как они пришли с камеры, без decode. Codec info (SPS/PPS, profile, level) extracted из первого SETUP/RTSP DESCRIBE и шарится через protocol header.

# Frigate example config
cameras:
  cam_parking:
    ffmpeg:
      inputs:
        - path: cuframes://cam-parking          # decoded NV12 для detect
          input_args: -f cuframes
          roles: [detect]
        - path: cuframes_packets://cam-parking  # encoded H.265 для record (no decode!)
          input_args: -f cuframes_packets
          roles: [record]

Implementation plan

  1. Protocol extension (docs/protocol.md): добавить packet ring header (entries: pts/dts/size/offset/flags, backed by посsibly mmap'ed file).
  2. Variable-length ring в libcuframes: simple wrap-around byte buffer с metadata-indexed entries. Slow consumer = drop oldest packet (как ring frames).
  3. Publisher modifications: в cuframes-rtsp-source после av_read_frame() дублировать AVPacket в encoded ring до передачи в decoder.
  4. FFmpeg demuxer: libavformat/cuframes_packetsdec.ccuframes_packets_read_packet копирует bytes из shared ring в AVPacket. Включить SPS/PPS handshake.
  5. Backward compat: publisher без BUILD_ENCODED_RING=ON шлёт только NV12 (как v0.1). С опцией — оба rings active.

Edge cases

  • Variable packet size: H.265 IDR может быть >1 MB, P-frames ~30 KB. Ring buffer должен handle. Compromise: max_packet_size = 2 MB default, configurable.
  • B-frames / DTS vs PTS: encoded ring должен сохранять DTS (decoder order). PTS metadata уже есть в v0.1.
  • Late subscriber: subscriber подключается через 10 минут после публикации — он должен получить только GOP-aligned start (с keyframe), иначе decoder fails. Publisher отмечает keyframes в flags, subscriber wait'ит keyframe before delivery.
  • Codec change mid-stream: камера переключилась с H.264 на H.265 (редко) — invalidate codec params, subscribers reconnect.

Связь с Frigate

После v0.2 — Frigate config'ы parking-камер мoгут быть полностью через cuframes:

  • detect via cuframes://
  • record via cuframes_packets://
  • Frigate не открывает RTSP сам.

Это сильное value-add для community. Сейчас Frigate users регулярно жалуются на NVDEC saturation (Frigate#17033, #20191).

Effort estimate

~1-2 недели focused работы. См. ROADMAP.md.

Related

  • ROADMAP.md — v0.2 section
  • docs/architecture.md — обновится с описанием packet ring
  • FFmpeg upstream PR — отдельный, после стабильного API
## Motivation Текущий cuframes v0.1 раздает **decoded NV12 frames** через CUDA IPC. Это идеально для consumer'ов которые делают **detect/AI/processing**. Но **Frigate** (как и любой recorder) для **record** roles делает **`-c:v copy`** — берёт encoded H.264/H.265 stream и mux'ит в mp4 **без** decode. Сейчас Frigate должен открывать **второй** RTSP connection к камере для record stream (decode skipped, но второй RTSP-link на камеру). ## Proposal Расширить cuframes публиковать **encoded packets** (H.264/H.265 NAL units) **параллельно** decoded frames — через **отдельный** shared ring. ``` ┌─► [encoded packets] ─► cuframes_packets IPC ─► record consumers (mux only, no decode) Camera RTSP ─► publisher demuxer┤ └─► NVDEC ─► [NV12 frames] ─► cuframes IPC ─► detect/AI consumers ``` ## Benefits | | Без v0.2 | С v0.2 | |---|---|---| | RTSP connections на камеру | 2+ (Frigate detect + Frigate record + cctv + ...) | **1** (только publisher) | | Camera load | N parallel streams (часто limit 4-5 у Dahua) | 1 stream | | CPU on RTSP demuxers | N (по одному на consumer) | 1 | | NVDEC | Уже 1 (cuframes v0.1) | 1 (unchanged) | ## API Sketch ### Producer ```c /* Publisher API extension. EXTERNAL ownership уже есть в v0.1 — теперь для packets. * Packets variable-size, поэтому ring — packet-indexed (offsets + size), * backed by shared memory (NE CUDA-память — encoded data на host). */ int cuframes_publisher_publish_packet( cuframes_publisher_t *pub, const void *data, /* указатель на NAL unit'ы */ size_t size, int64_t pts_ns, int64_t dts_ns, uint32_t flags /* AV_PKT_FLAG_KEY etc. */ ); ``` ### Consumer (sync API) ```c typedef struct cuframes_packet cuframes_packet_t; int cuframes_subscriber_next_packet( cuframes_subscriber_t *sub, cuframes_packet_t **pkt_out, int32_t timeout_ms ); const void *cuframes_packet_data(const cuframes_packet_t *p); size_t cuframes_packet_size(const cuframes_packet_t *p); int64_t cuframes_packet_pts(const cuframes_packet_t *p); int64_t cuframes_packet_dts(const cuframes_packet_t *p); uint32_t cuframes_packet_flags(const cuframes_packet_t *p); int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub, cuframes_packet_t *p); ``` Same `key` namespace что v0.1 — но **отдельный ring** в одной публикации. Subscriber выбирает frames-ring и/или packets-ring при `cuframes_subscriber_create()` (новая опция). ### FFmpeg demuxer extension `cuframes_packets://` — комплементарный к существующему `cuframes://`. AVPacket'ы выдаются 1-в-1 как они пришли с камеры, **без decode**. Codec info (SPS/PPS, profile, level) extracted из первого SETUP/RTSP DESCRIBE и шарится через protocol header. ```yaml # Frigate example config cameras: cam_parking: ffmpeg: inputs: - path: cuframes://cam-parking # decoded NV12 для detect input_args: -f cuframes roles: [detect] - path: cuframes_packets://cam-parking # encoded H.265 для record (no decode!) input_args: -f cuframes_packets roles: [record] ``` ## Implementation plan 1. **Protocol extension** (docs/protocol.md): добавить packet ring header (entries: pts/dts/size/offset/flags, backed by посsibly mmap'ed file). 2. **Variable-length ring** в libcuframes: simple wrap-around byte buffer с metadata-indexed entries. Slow consumer = drop oldest packet (как ring frames). 3. **Publisher modifications**: в `cuframes-rtsp-source` после `av_read_frame()` дублировать AVPacket в encoded ring **до** передачи в decoder. 4. **FFmpeg demuxer**: `libavformat/cuframes_packetsdec.c` — `cuframes_packets_read_packet` копирует bytes из shared ring в AVPacket. Включить SPS/PPS handshake. 5. **Backward compat**: publisher без `BUILD_ENCODED_RING=ON` шлёт только NV12 (как v0.1). С опцией — оба rings active. ## Edge cases - **Variable packet size**: H.265 IDR может быть >1 MB, P-frames ~30 KB. Ring buffer должен handle. Compromise: max_packet_size = 2 MB default, configurable. - **B-frames / DTS vs PTS**: encoded ring должен сохранять DTS (decoder order). PTS metadata уже есть в v0.1. - **Late subscriber**: subscriber подключается через 10 минут после публикации — он должен получить **только GOP-aligned start** (с keyframe), иначе decoder fails. Publisher отмечает keyframes в flags, subscriber wait'ит keyframe before delivery. - **Codec change mid-stream**: камера переключилась с H.264 на H.265 (редко) — invalidate codec params, subscribers reconnect. ## Связь с Frigate После v0.2 — Frigate config'ы parking-камер мoгут быть **полностью через cuframes**: - detect via `cuframes://` - record via `cuframes_packets://` - Frigate **не** открывает RTSP сам. Это **сильное** value-add для community. Сейчас Frigate users регулярно жалуются на NVDEC saturation ([Frigate#17033](https://github.com/blakeblackshear/frigate/discussions/17033), [#20191](https://github.com/blakeblackshear/frigate/discussions/20191)). ## Effort estimate ~1-2 недели focused работы. См. [ROADMAP.md](../ROADMAP.md). ## Related - ROADMAP.md — v0.2 section - docs/architecture.md — обновится с описанием packet ring - FFmpeg upstream PR — отдельный, после стабильного API
Author
Owner

Sub-task: publisher-side resize

Дополнение к v0.2: publisher должен поддерживать pre-scaling кадров до target size перед публикацией:

cuframes-rtsp-source --rtsp ... --key cam-X --scale 640x480

Motivation

Patched FFmpeg для downstream consumer'ов (Frigate / FFmpeg-based) собирается без --enable-cuda-llvm на runtime платформах с glibc < 2.38 (Debian 12, CentOS 8). Без cuda-llvm — нет scale_cuda фильтра → consumer вынужден CPU-resize либо отключить hwaccel совсем.

Publisher-side resize устраняет эту зависимость: consumer получает frames уже нужного размера, scale-фильтр не нужен.

Дополнительные выгоды

  • Resize делается один раз publisher'ом (vs N раз каждым consumer'ом) — cheaper compute
  • Меньше bandwidth в IPC ring (640×480 NV12 ~460 KB vs 1920×1080 ~3 MB на frame)
  • Cleaner architecture: detect path и record path могут иметь разные resolution через разные publisher keys

Implementation sketch

В cuframes-rtsp-source:

  1. После avcodec_receive_frame() — apply CUDA-side resize (через CUDA kernel либо libavfilter scale_cuda если builder image его имеет)
  2. Result frame копируется в EXTERNAL pool с уже scaled размерами
  3. cuframes_publisher_config_t::width/height отражают post-scale размеры

В libcuframes — никаких изменений, scale делается на стороне publisher'а в memory before publish.

Multi-key setup для разных resolutions

Один RTSP → multiple cuframes publishers с разными keys/scales:

# Один декод, два scale outputs:
cuframes-rtsp-source --rtsp ... --key cam-full     --scale 1920x1080  # для record reference
cuframes-rtsp-source --rtsp ... --key cam-detect   --scale 640x480    # для Frigate detect
cuframes-rtsp-source --rtsp ... --key cam-preview  --scale 320x240    # для UI thumbnail

(Или extend single instance чтобы publish'ить под multiple keys одновременно — TBD.)

## Sub-task: publisher-side resize Дополнение к v0.2: publisher должен поддерживать pre-scaling кадров до **target size** перед публикацией: ```bash cuframes-rtsp-source --rtsp ... --key cam-X --scale 640x480 ``` ### Motivation Patched FFmpeg для downstream consumer'ов (Frigate / FFmpeg-based) собирается **без `--enable-cuda-llvm`** на runtime платформах с glibc < 2.38 (Debian 12, CentOS 8). Без cuda-llvm — нет `scale_cuda` фильтра → consumer вынужден CPU-resize либо отключить hwaccel совсем. Publisher-side resize устраняет эту зависимость: consumer получает frames уже нужного размера, scale-фильтр не нужен. ### Дополнительные выгоды - Resize делается **один раз** publisher'ом (vs N раз каждым consumer'ом) — cheaper compute - Меньше bandwidth в IPC ring (640×480 NV12 ~460 KB vs 1920×1080 ~3 MB на frame) - Cleaner architecture: detect path и record path могут иметь разные resolution через разные publisher keys ### Implementation sketch В `cuframes-rtsp-source`: 1. После `avcodec_receive_frame()` — apply CUDA-side resize (через CUDA kernel либо libavfilter `scale_cuda` если builder image его имеет) 2. Result frame копируется в EXTERNAL pool с уже **scaled** размерами 3. `cuframes_publisher_config_t::width/height` отражают **post-scale** размеры В libcuframes — никаких изменений, scale делается на стороне publisher'а в memory before publish. ### Multi-key setup для разных resolutions Один RTSP → multiple cuframes publishers с разными keys/scales: ```bash # Один декод, два scale outputs: cuframes-rtsp-source --rtsp ... --key cam-full --scale 1920x1080 # для record reference cuframes-rtsp-source --rtsp ... --key cam-detect --scale 640x480 # для Frigate detect cuframes-rtsp-source --rtsp ... --key cam-preview --scale 320x240 # для UI thumbnail ``` (Или extend single instance чтобы publish'ить под multiple keys одновременно — TBD.)
Author
Owner

Delivered в v0.2.0:

  • PR #4 (libcuframes + rtsp-source + docs) — merged
  • gx/ffmpeg-patched PR #1 (cuframes_packets:// demuxer) — merged

Все 6 шагов из issue plan завершены, CI зелёный, stress test в libcuframes/tests/test_packet_ring.c.

Frigate dual-input config готов: docs/integrations/frigate.md секция "v0.2: dual-input".

Delivered в v0.2.0: - PR #4 (libcuframes + rtsp-source + docs) — merged - gx/ffmpeg-patched PR #1 (`cuframes_packets://` demuxer) — merged Все 6 шагов из issue plan завершены, CI зелёный, stress test в `libcuframes/tests/test_packet_ring.c`. Frigate dual-input config готов: `docs/integrations/frigate.md` секция "v0.2: dual-input".
gx closed this issue 2026-05-19 17:48:22 +01:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gx/cuframes#2