@@ -826,21 +826,130 @@ def decode_frames_pynvvideocodec(
826826 return frames , source , frame_idx , valid_frame_indices
827827
828828
829+ class DeepStreamVideoBackendMixin :
830+ """NVIDIA DeepStream (NVDEC) GPU-decode codec utilities.
831+
832+ Decoding runs on a shared pool of daemon threads inside one CUDA
833+ context (see the ``nvidia-deepstream-videodecode-cu13`` package). The
834+ container bytes are pushed into an ``appsrc`` GStreamer pipeline, so no
835+ local file path is required — HTTP and base64 sources decode identically
836+ to local files.
837+
838+ Like the OpenCV/PyAV mixins, this provides only the codec layer.
839+ Frame *selection* lives in the loader's
840+ ``compute_frames_index_to_sample`` and arrives here as an explicit
841+ list of frame indices.
842+ """
843+
844+ # Process-wide lazy decode pool, shared across all DeepStream backends.
845+ _pool : ClassVar [Any ] = None
846+ _pool_lock : ClassVar [Any ] = None
847+
848+ @classmethod
849+ def _get_pool (cls , pool_size : int | None = None ):
850+ """Lazy-initialize the shared decode pool on first use.
851+
852+ ``pool_size`` (number of decode worker threads) comes from
853+ ``--media-io-kwargs`` (``{"video": {"pool_size": N}}``); when unset it
854+ defaults to the existing ``VLLM_MEDIA_LOADING_THREAD_COUNT`` so no
855+ DeepStream-specific env var is needed. The pool is a process-wide
856+ singleton, so the first decode's value wins.
857+ """
858+ if cls ._pool is not None :
859+ return cls ._pool
860+ if cls ._pool_lock is None :
861+ cls ._pool_lock = threading .Lock ()
862+ with cls ._pool_lock :
863+ if cls ._pool is not None :
864+ return cls ._pool
865+ import os
866+
867+ from nvidia .deepstream_videodecode import DecodePool
868+
869+ if pool_size is None :
870+ pool_size = int (os .environ .get ("VLLM_MEDIA_LOADING_THREAD_COUNT" , 8 ))
871+ pool_size = max (1 , min (int (pool_size ), 16 ))
872+ logger .info (
873+ "[DeepStream] initializing decode pool with %d workers" ,
874+ pool_size ,
875+ )
876+ cls ._pool = DecodePool (num_workers = pool_size )
877+ return cls ._pool
878+
879+ @classmethod
880+ def decode_indices (
881+ cls ,
882+ data : bytes ,
883+ frame_indices : list [int ],
884+ source : VideoSourceMetadata ,
885+ codec : str = "" ,
886+ pool_size : int | None = None ,
887+ timeout_sec : float = 120.0 ,
888+ ) -> tuple [npt .NDArray , list [int ]]:
889+ """Decode the requested frame indices from raw container bytes.
890+
891+ The whole stream is decoded; the pool keeps exactly the frames whose
892+ decode-order index is in ``frame_indices`` (1:1, frame-exact) and
893+ sends EOS once the last one is matched.
894+
895+ ``codec`` (e.g. ``"h264"``/``"hevc"``) lets the pool keep its NVDEC
896+ session warm across same-codec streams and rebuild only on a codec
897+ change. Frames are returned as a CPU NHWC uint8 array so the
898+ upstream multimodal parser sees the same shape as the other
899+ backends.
900+ """
901+ if not frame_indices :
902+ raise ValueError ("DeepStream backend received no frame indices" )
903+
904+ result = cls ._get_pool (pool_size ).decode (
905+ data ,
906+ target_indices = frame_indices ,
907+ codec = codec ,
908+ max_frames = len (frame_indices ),
909+ timeout_sec = timeout_sec ,
910+ )
911+ if result .error :
912+ raise ValueError (f"DeepStream decode failed: { result .error } " )
913+ if result .frames is None or result .n_kept == 0 :
914+ raise ValueError ("DeepStream decode produced no frames" )
915+
916+ valid = frame_indices [: result .n_kept ]
917+ # GPU -> CPU NHWC uint8 at the codec boundary (one PCIe copy); keeps
918+ # the array shape identical to the OpenCV/PyAV backends. Copy into
919+ # PINNED host memory (reused across calls by PyTorch's pinned caching
920+ # allocator) so the D2H runs at full PCIe bandwidth (~13 GB/s) rather
921+ # than the ~1 GB/s pageable path that plain ``.cpu()`` takes — ~12x
922+ # faster for a 1080p x8 frame batch (~46ms -> ~4ms). ``numpy()`` keeps
923+ # the pinned tensor alive via the array's base.
924+ import torch
925+
926+ gpu = result .frames
927+ if gpu .is_cuda :
928+ host = torch .empty (gpu .shape , dtype = gpu .dtype , pin_memory = True )
929+ host .copy_ (gpu , non_blocking = True )
930+ torch .cuda .current_stream ().synchronize ()
931+ arr = host .numpy ()
932+ else :
933+ arr = gpu .numpy ()
934+ return arr , valid
935+
936+
829937@VIDEO_LOADER_REGISTRY .register ("opencv" )
830938class VideoBackend (
831939 VideoLoader ,
832940 OpenCVVideoBackendMixin ,
833941 PyAVVideoBackendMixin ,
834942 TorchCodecVideoBackendMixin ,
835943 PyNvVideoCodecVideoBackendMixin ,
944+ DeepStreamVideoBackendMixin ,
836945):
837946 """Uniform-sampling video backend.
838947
839948 Samples ``num_frames`` uniformly across the video (or one frame every
840949 ``1/fps`` seconds, whichever produces fewer frames). The decoding codec
841950 is selected via the ``backend`` kwarg (``"opencv"``, ``"pyav"``,
842- ``"torchcodec"`` or ``"pynvvideocodec"``), which can be passed through
843- ``--media-io-kwargs``. Defaults to ``"opencv"``.
951+ ``"torchcodec"``, ``"pynvvideocodec"``, or ``"deepstream"``),
952+ which can be passed through ``--media-io-kwargs``. Defaults to ``"opencv"``.
844953 """
845954
846955 _sampling_suffix : ClassVar [str ] = ""
@@ -885,7 +994,9 @@ def load_bytes(
885994 max_duration : int = 300 ,
886995 frame_recovery : bool = False ,
887996 * ,
888- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
997+ backend : Literal [
998+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
999+ ] = "opencv" ,
8891000 num_ffmpeg_threads : int = 0 ,
8901001 seek_mode : Literal ["exact" , "approximate" ] = "exact" ,
8911002 ** kwargs ,
@@ -901,7 +1012,7 @@ def load_bytes(
9011012 frame_recovery: Enable forward-scan recovery for failed frames.
9021013 Only honored by the OpenCV codec.
9031014 backend: Decoding codec — ``"opencv"``, ``"pyav"``,
904- ``"torchcodec"`` or ``"pynvvideocodec "``.
1015+ ``"torchcodec"``, ``"pynvvideocodec"`` or ``"deepstream "``.
9051016 num_ffmpeg_threads: Number of FFmpeg decoding threads, only used by
9061017 TorchCodec: ``0`` (default) relies on the FFmpeg default value
9071018 which is ``min(cpu_count + 1, 16)``.
@@ -982,11 +1093,37 @@ def load_bytes(
9821093 target ,
9831094 ** kwargs ,
9841095 )
1096+ elif backend == "deepstream" :
1097+ assert not frame_recovery , (
1098+ "frame_recovery is only available for `opencv` backend"
1099+ )
1100+ # Decode-pool size comes from media-io-kwargs (no env var); the
1101+ # pool is a process-wide singleton so the first decode's value
1102+ # wins. Pop it so it isn't forwarded to the frame sampler.
1103+ pool_size = kwargs .pop ("pool_size" , None )
1104+ # Probe container metadata from the bytes via GStreamer (in
1105+ # the deepstream video-decode wheel) — no PyAV/pymediainfo, no path.
1106+ from nvidia .deepstream_videodecode import probe_metadata
1107+
1108+ total_frames , original_fps , duration , _w , _h , codec = probe_metadata (data )
1109+ source = cls ._prepare_source (
1110+ VideoSourceMetadata (
1111+ total_frames_num = total_frames ,
1112+ original_fps = original_fps ,
1113+ duration = duration ,
1114+ )
1115+ )
1116+ frame_idx = cls .compute_frames_index_to_sample (
1117+ source = source , target = target , ** kwargs
1118+ )
1119+ frames , valid = cls .decode_indices (
1120+ data , frame_idx , source , codec = codec , pool_size = pool_size
1121+ )
9851122 else :
9861123 raise ValueError (
9871124 f"Unknown video codec backend { backend !r} ; "
9881125 "valid options: 'opencv', 'pyav', 'torchcodec', "
989- "'pynvvideocodec'."
1126+ "'pynvvideocodec' and 'deepstream' ."
9901127 )
9911128
9921129 if len (valid ) < len (frame_idx ):
@@ -1073,7 +1210,9 @@ def load_bytes(
10731210 max_duration : int = 300 ,
10741211 frame_recovery : bool = False ,
10751212 * ,
1076- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
1213+ backend : Literal [
1214+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
1215+ ] = "opencv" ,
10771216 ** kwargs ,
10781217 ) -> tuple [npt .NDArray , dict [str , Any ]]:
10791218 return super ().load_bytes (
@@ -1152,7 +1291,9 @@ def load_bytes(
11521291 max_duration : int = 300 ,
11531292 frame_recovery : bool = False ,
11541293 * ,
1155- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
1294+ backend : Literal [
1295+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
1296+ ] = "opencv" ,
11561297 ** kwargs ,
11571298 ) -> tuple [npt .NDArray , dict [str , Any ]]:
11581299 return super ().load_bytes (
@@ -1244,7 +1385,9 @@ def load_bytes(
12441385 max_duration : int = 300 ,
12451386 frame_recovery : bool = False ,
12461387 * ,
1247- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
1388+ backend : Literal [
1389+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
1390+ ] = "opencv" ,
12481391 ** kwargs ,
12491392 ) -> tuple [npt .NDArray , dict [str , Any ]]:
12501393 return super ().load_bytes (
@@ -1369,7 +1512,9 @@ def load_bytes(
13691512 max_duration : int = 300 ,
13701513 frame_recovery : bool = False ,
13711514 * ,
1372- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
1515+ backend : Literal [
1516+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
1517+ ] = "opencv" ,
13731518 ** kwargs ,
13741519 ) -> tuple [npt .NDArray , dict [str , Any ]]:
13751520 return super ().load_bytes (
@@ -1467,7 +1612,9 @@ def load_bytes(
14671612 max_duration : int = 300 ,
14681613 frame_recovery : bool = False ,
14691614 * ,
1470- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
1615+ backend : Literal [
1616+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
1617+ ] = "opencv" ,
14711618 ** kwargs ,
14721619 ) -> tuple [npt .NDArray , dict [str , Any ]]:
14731620 frames , metadata = super ().load_bytes (
@@ -1790,7 +1937,9 @@ def load_bytes(
17901937 max_duration : int = 300 ,
17911938 frame_recovery : bool = False ,
17921939 * ,
1793- backend : Literal ["opencv" , "pyav" , "torchcodec" , "pynvvideocodec" ] = "opencv" ,
1940+ backend : Literal [
1941+ "opencv" , "pyav" , "torchcodec" , "pynvvideocodec" , "deepstream"
1942+ ] = "opencv" ,
17941943 ** kwargs ,
17951944 ) -> tuple [npt .NDArray , dict [str , Any ]]:
17961945 frames , metadata = super ().load_bytes (
0 commit comments