[Bugfix][Frontend] Return 400 for invalid PyNvVideoCodec video input (#51120)

Signed-off-by: Dat Mai <dmai@nvidia.com>
This commit is contained in:
dmai-afk
2026-08-12 10:58:46 +00:00
committed by GitHub
parent e60f3c4c13
commit 86e2ab50aa
3 changed files with 110 additions and 5 deletions
+28
View File
@@ -1,7 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import io
import sys
from pathlib import Path
from types import SimpleNamespace
import numpy as np
import numpy.typing as npt
@@ -19,6 +21,7 @@ from vllm.multimodal.media import ImageMediaIO, VideoMediaIO
from vllm.multimodal.video import (
PYNVVIDEOCODEC_VIDEO_BACKEND,
VIDEO_LOADER_REGISTRY,
PyNvVideoCodecVideoBackend,
VideoLoader,
_pynvvc_frames_to_nhwc,
)
@@ -364,6 +367,31 @@ def test_load_base64_jpeg_raises_on_zero_num_frames():
videoio.load_base64("video/jpeg", data)
def test_pynvvideocodec_unrelated_error_propagates(
monkeypatch: pytest.MonkeyPatch,
):
class FakePyNvVCException(Exception):
pass
fake_nvc = SimpleNamespace(PyNvVCException=FakePyNvVCException)
monkeypatch.setitem(sys.modules, "PyNvVideoCodec", fake_nvc)
original_error = RuntimeError("GPU decoder unavailable")
def raise_unrelated_error(cls, file_path, nvc):
raise original_error
monkeypatch.setattr(
PyNvVideoCodecVideoBackend,
"_read_source_metadata",
classmethod(raise_unrelated_error),
)
with pytest.raises(RuntimeError) as exc_info:
PyNvVideoCodecVideoBackend.decode_frames_pynvvideocodec(b"video", None)
assert exc_info.value is original_error
# ---------------------------------------------------------------------------
# GPU video backend policy tests
# ---------------------------------------------------------------------------
+54
View File
@@ -200,6 +200,60 @@ def test_pynvvideocodec_codec_uses_dynamic_sampling_strategy(
assert metadata["frames_indices"] == [0, 9]
@pytest.mark.skipif(not current_platform.is_cuda(), reason="Requires CUDA")
def test_pynvvideocodec_corrupted_videos_raise_value_error():
valid_video = create_long_gop_video(num_frames=2, width=64, height=64)
corrupted_video = (ASSETS_DIR / "corrupted.mp4").read_bytes()
malformed_video = corrupted_video[:128]
old_slots = PyNvVideoCodecVideoBackend._decoder_slots
old_active_slots = PyNvVideoCodecVideoBackend._active_decoder_slots
old_cond = PyNvVideoCodecVideoBackend._decoder_slot_cond
old_max_slots = PyNvVideoCodecVideoBackend._max_decoder_slots
try:
PyNvVideoCodecVideoBackend._decoder_slots = []
PyNvVideoCodecVideoBackend._active_decoder_slots = 0
PyNvVideoCodecVideoBackend._decoder_slot_cond = threading.Condition()
PyNvVideoCodecVideoBackend._max_decoder_slots = None
loader = VIDEO_LOADER_REGISTRY.load(PYNVVIDEOCODEC_VIDEO_BACKEND)
with pytest.raises(
ValueError,
match=r"^Invalid or unsupported video file\.$",
) as malformed_exc:
loader.load_bytes(
malformed_video,
num_frames=1,
hw_decoders=1,
)
assert malformed_exc.value.__cause__ is not None
with pytest.raises(
ValueError,
match=r"^Invalid or unsupported video file\.$",
) as exc_info:
loader.load_bytes(
corrupted_video,
num_frames=-1,
hw_decoders=1,
)
assert exc_info.value.__cause__ is not None
frames, _ = loader.load_bytes(
valid_video,
num_frames=1,
hw_decoders=1,
)
assert frames.shape[0] == 1
finally:
PyNvVideoCodecVideoBackend._decoder_slots = old_slots
PyNvVideoCodecVideoBackend._active_decoder_slots = old_active_slots
PyNvVideoCodecVideoBackend._decoder_slot_cond = old_cond
PyNvVideoCodecVideoBackend._max_decoder_slots = old_max_slots
@pytest.mark.parametrize("hw_decoders", [1, 3])
def test_pynvvideocodec_decoder_slots_are_bounded(
monkeypatch: pytest.MonkeyPatch,
+28 -5
View File
@@ -234,6 +234,16 @@ def validate_pynvvideocodec_hw_decoders(hw_decoders: object) -> int:
return hw_decoders
def _pynvvideocodec_exception_types(nvc) -> tuple[type[Exception], ...]:
return tuple(
exception_type
for name in dir(nvc)
if name.startswith("PyNvVCException")
and isinstance((exception_type := getattr(nvc, name)), type)
and issubclass(exception_type, Exception)
)
class PyNvVideoCodecDecoderSlot:
"""A retained PyNv decoder slot and its CUDA stream.
@@ -806,10 +816,18 @@ class PyNvVideoCodecVideoBackendMixin:
with cls._borrow_decoder_slot() as decoder_slot:
stream = decoder_slot.stream
with cls._torch_stream_context(stream):
decoder = decoder_slot.get_decoder(
file_path, nvc, device_index=cls._DEVICE_INDEX
)
decoded_frames = decoder.get_batch_frames_by_index(frame_idx)
try:
decoder = decoder_slot.get_decoder(
file_path, nvc, device_index=cls._DEVICE_INDEX
)
decoded_frames = decoder.get_batch_frames_by_index(frame_idx)
except Exception as exc:
if not isinstance(
exc,
_pynvvideocodec_exception_types(nvc) + (IndexError,),
):
raise
raise ValueError("Invalid or unsupported video file.") from exc
if len(decoded_frames) < len(frame_idx):
logger.warning(
"pynvvideocodec video loading: expected %d frames but got %d.",
@@ -854,7 +872,12 @@ class PyNvVideoCodecVideoBackendMixin:
with os.fdopen(temp_fd, "wb") as temp_file:
temp_file.write(data)
gpu_source = cls._read_source_metadata(temp_path, nvc)
try:
gpu_source = cls._read_source_metadata(temp_path, nvc)
except Exception as exc:
if not isinstance(exc, _pynvvideocodec_exception_types(nvc)):
raise
raise ValueError("Invalid or unsupported video file.") from exc
_check_frame_pixel_limit(gpu_source.width, gpu_source.height)
source = cls._prepare_source(gpu_source.source)
frame_idx = cls.compute_frames_index_to_sample(