GPU-resident video decode via NVIDIA DeepStream — single-wheel install
bundles the DS shared libraries in a flat _libs/ directory and
configures GStreamer + libv4l plugin discovery on Python import.
apt update
apt install gstreamer1.0-tools gstreamer1.0-plugins-{base,good,bad,ugly} \
gstreamer1.0-libav python3-gi python3-gst-1.0 libv4l-0 \
cuda-libraries-13-0
pip install deepstream_decode-*.whlThat's it. Python consumers (vLLM, custom training stacks) get a working
decode pipeline on import deepstream_decode: GST_PLUGIN_PATH and
LIBV4L2_PLUGIN_DIR are set in-process to the bundled _libs/
directory.
Once installed, confirm the pipeline works:
# Built-in selftest — verifies lib resolution, plugin discovery, CUDA context.
deepstream-decode-selftestSample decode app (lives in examples/ in this repo, not shipped in the
wheel):
# Default: decodes the bundled sample 10s 1080p H.264 clip on the host
python3 examples/decode_example.py
# Decode any file you have
python3 examples/decode_example.py /path/to/video.mp4
# File + live RTSP source in one run
python3 examples/decode_example.py /path/to/video.mp4 \
--rtsp rtsp://10.24.217.130:8554/
# Tune worker count and sampled-frames-per-call
python3 examples/decode_example.py video.mp4 --workers 4 --frames 16Successful output ends with frames shape : (N, H, W, 3) on
torch.uint8, cuda:0 — the GPU tensor is ready for downstream
consumers (model inference, training pipeline, etc.) with no D2H copy.
from deepstream_decode import (
DecodePool, # pool of N file-decode pipelines on N threads
StreamHandle, # one persistent pipeline for an RTSP/URI stream
DecodeFrames, # @dataclass: frames, n_kept, n_total, fps, error
probe_metadata, # GStreamer-only metadata probe (no decode, no PyAV)
lib_dir, # path to the bundled _libs/ directory
)Read container metadata from raw bytes using GStreamer only — an
appsrc → parsebin → fakesink pipeline prerolled to PAUSED, so no frames
are decoded and no external library (PyAV / libmediainfo) is needed.
frame_count is derived as round(duration × fps); unknown numeric
fields are 0 and codec is "".
Decode raw container bytes on a pool worker and keep the frames whose
decode-order index is in target_indices (returns exactly those frames,
1:1 with the indices). The bytes are pushed into an in-process appsrc
pipeline — no file path — so HTTP, base64, and local-file sources
decode identically. The whole stream is decoded and the requested indices
are kept.
from deepstream_decode import DecodePool, probe_metadata
pool = DecodePool(num_workers=8)
data = open("/path/to/video.mp4", "rb").read()
# Probe metadata (GStreamer, no decode) to know the frame count, then pick
# a uniform sample. fc/codec come straight from the wheel — no PyAV.
fc, fps, dur, w, h, codec = probe_metadata(data)
import numpy as np
indices = np.linspace(0, fc - 1, 8, dtype=int).tolist()
out = pool.decode(data, target_indices=indices, codec=codec,
max_frames=len(indices))
# out.frames is a CUDA tensor (out.n_kept, H, W, 3) uint8 — no D2H copy.codec (e.g. "h264"/"hevc") is an optional hint: a worker keeps its
NVDEC session warm across same-codec streams and rebuilds only on a codec
change. Omit it to disable that check.
Frame selection is by index here, which is exact (1:1). A GOP-drop fast path (decode only the GOPs that contain a sampled frame) needs PTS-based selection and is intentionally not enabled, since PTS targets aren't 1:1 with indices when a stream's PTS don't align with
idx / fps.
deepstream_decode/
├── __init__.py
├── _ds_dec.py # DecodePool / StreamHandle API
├── _runtime.py # _libs/ path resolver
├── _selftest.py # `deepstream-decode-selftest` CLI
├── _version.py
└── _libs/ # main libs + GStreamer plugins (flat)
├── libnvbufsurface.so
├── libnvbufsurftransform.so (~26 MB)
├── libnvbuf_fdmap.so
├── libnvds_meta.so
├── libnvdsbufferpool.so
├── libnvdsgst_helper.so
├── libnvdsgst_meta.so
├── libgstnvdsseimeta.so
├── libgstnvcustomhelper.so
├── libnvv4l2.so
├── libcuvidv4l2.so
├── libv4l2.so.0 (symlink → libnvv4l2.so)
├── libgstnvvideo4linux2.so (GStreamer plugin)
├── libgstnvvideoconvert.so (GStreamer plugin)
└── v4l_plugins/ # isolated to silence libv4l2's plugin scanner
└── libcuvidv4l2_plugin.so
RPATHs: $ORIGIN on the flat libs (siblings); $ORIGIN/.. on
v4l_plugins/*.so (their deps live one level up). The libv4l plugin
gets its own sub-dir because libnvv4l2.so's scanner dlsyms every
.so in LIBV4L2_PLUGIN_DIR looking for the libv4l2_plugin symbol —
putting non-plugin libs alongside it triggers noisy dlsym failed
warnings.
- Get the DeepStream tbz2 from NVIDIA. Two paths:
- Auto-download from NGC (recommended): set
NGC_API_KEY(https://ngc.nvidia.com → Setup → API Key) and the script fetches it on first run. URL pattern:https://api.ngc.nvidia.com/v2/recipes/nvidia/deepstream/versions/<MAJOR.MINOR>/files/deepstream_sdk_v<VERSION>_x86_64.tbz2 - Manual download: developer.nvidia.com → DeepStream → Linux x86_64
→ Tar Package. Save next to
build_wheel.sh(or passDS_TBZ2=/path/to/file.tbz2). License-gated — not redistributable.
- Auto-download from NGC (recommended): set
- The patched
libnvv4l2.sois committed to this folder (it has theLIBV4L2_PLUGIN_DIRenv-var support that the SDK's stock libnvv4l2.so lacks). Override withPATCHED_LIBNVV4L2=/path/to/itif needed. This is a temporary substitution until the patched build ships in the DS tbz2 itself — at which point delete the file and setUSE_PATCHED_LIBNVV4L2=0-equivalent in the script (or just remove the override block). - Run:
./build_wheel.sh # uses DS_VERSION=9.0.0 by default
./build_wheel.sh --ds-version=9.1.0
DS_TBZ2=/path/file.tbz2 ./build_wheel.sh
./build_wheel.sh --ds-src=/path/to/already_extracted_treeOutput: dist/deepstream_decode-<VERSION>+cuda<MAJOR>-py3-none-manylinux_2_28_x86_64.whl
Build prerequisites:
sudo apt update
sudo apt install patchelf binutils tar python3 python3-pip
pip install uv buildGStreamer caches a plugin registry at ~/.cache/gstreamer-1.0/registry.<arch>.bin.
If the cache was built before cuda-libraries-13-0 was installed,
it permanently records "this plugin failed to load" — GStreamer trusts
the cache and never retries.
Common trigger: installing the wheel and running it once before
running the apt-install line, then installing cuda-libraries-13-0
afterwards. The first run cached the failure; subsequent runs don't
rescan even though the missing lib is now present.
Fix:
rm -rf ~/.cache/gstreamer-1.0/
python3 -c "import deepstream_decode" # forces a rescan; rebuilds the cache
gst-inspect-1.0 nvv4l2decoder # should now print Factory DetailsCUDA's NPP image-processing runtime is missing. The wheel bundles DeepStream's own libs but not the CUDA runtime — that comes from apt:
apt update && apt install -y --no-install-recommends cuda-libraries-13-0Other CUDA-side libs in the same package (libcublas, libcudart,
libcuda) are required too — they're all in cuda-libraries-13-0.
After installing, clear the GStreamer cache (see above) before re-running.
The wheel's _libs/ directory is empty or missing. Causes:
- The wheel was installed without
--force-reinstallafter a build that wiped the staging dir mid-build. - Some packaging tools (older pip, certain CI runners) drop
.sofiles from wheels by default.
Fix: pip install --force-reinstall --no-deps deepstream_decode-*.whl.
Verify with python3 -c "import deepstream_decode, os; print(os.listdir(deepstream_decode.lib_dir()))" — should list ~15 .so files.
Informational message from nvv4l2decoder itself — not an error.
NVDEC negotiated a blocking I/O mode for the V4L2 capture buffer
(default for file decode). Silence with:
GST_DEBUG=2 python3 your_script.py # below WARNING levelOlder builds of this wheel placed libcuvidv4l2_plugin.so alongside
the main libs, triggering libnvv4l2.so's plugin scanner to dlsym
every .so in LIBV4L2_PLUGIN_DIR. Current builds isolate it in
_libs/v4l_plugins/, so this warning shouldn't appear. If you see it,
you're on an old wheel — rebuild and reinstall.