[Bugfix] Align OpenCV video metadata timeline (#47099)
Signed-off-by: VectorPeak <73048950+VectorPeak@users.noreply.github.com>
VectorPeak committed
Jul 1, 2026 at 04:43 UTC
68294739d1b5caaa1d537adcd79762a1ee9283fc
2 files changed
+27
-4
tests/multimodal/media/test_video.py
+19
-1
@@ -10,7 +10,11 @@ import pytest
10
from PIL import Image
11
12
from vllm.assets.base import get_vllm_public_assets
13
-from vllm.assets.video import video_to_ndarrays, video_to_pil_images_list
13
+from vllm.assets.video import (
14
+ video_get_metadata,
15
+ video_to_ndarrays,
16
+ video_to_pil_images_list,
17
+)
18
from vllm.multimodal.media import ImageMediaIO, VideoMediaIO
19
from vllm.multimodal.video import VIDEO_LOADER_REGISTRY, VideoLoader
20
@@ -112,6 +116,20 @@ def test_opencv_video_io_colorspace(tmp_path, is_color: bool, fourcc: str, ext:
116
assert np.nanmean(sim) > 0.99
117
118
119
+def test_opencv_video_metadata_matches_sampled_frame_timeline(tmp_path):
120
+ image_path = f"{tmp_path}/test_metadata_image.png"
121
+ Image.new("RGB", (8, 8), color=(255, 0, 0)).save(image_path)
122
+ video_path = f"{tmp_path}/test_metadata_video.mp4"
123
+ create_video_from_image(image_path, video_path, num_frames=10, fps=5.0)
124
+
125
+ metadata = video_get_metadata(video_path, num_frames=4)
126
+
127
+ assert metadata["fps"] == pytest.approx(5.0)
128
+ assert metadata["duration"] == pytest.approx(2.0)
129
+ assert metadata["frames_indices"] == [0, 3, 6, 9]
130
+ assert metadata["total_num_frames"] == 4
131
+
132
+
133
NUM_FRAMES = 10
134
FAKE_OUTPUT_1 = np.random.rand(NUM_FRAMES, 1280, 720, 3)
135
FAKE_OUTPUT_2 = np.random.rand(NUM_FRAMES, 1280, 720, 3)
vllm/assets/video.py
+8
-3
@@ -15,6 +15,10 @@ from vllm.transformers_utils.repo_utils import hf_api
15
from .base import get_cache_dir
16
17
18
+def _sample_frame_indices(total_frames: int, num_frames: int) -> npt.NDArray:
19
+ return np.linspace(0, total_frames - 1, num_frames, dtype=int)
20
+
21
+
22
@lru_cache
23
def download_video_asset(filename: str) -> str:
24
"""
@@ -47,7 +51,7 @@ def video_to_ndarrays(path: str, num_frames: int = -1) -> npt.NDArray:
51
frames = []
52
53
num_frames = num_frames if num_frames > 0 else total_frames
50
- frame_indices = np.linspace(0, total_frames - 1, num_frames, dtype=int)
54
+ frame_indices = _sample_frame_indices(total_frames, num_frames)
55
for idx in range(total_frames):
56
ok = cap.grab() # next img
57
if not ok:
@@ -86,13 +90,14 @@ def video_get_metadata(path: str, num_frames: int = -1) -> dict[str, Any]:
90
91
if num_frames == -1 or num_frames > total_frames:
92
num_frames = total_frames
93
+ frame_indices = _sample_frame_indices(total_frames, num_frames)
94
95
metadata = {
96
"total_num_frames": num_frames,
92
- "fps": duration / num_frames,
97
+ "fps": fps,
98
"duration": duration,
99
"video_backend": "opencv",
95
- "frames_indices": list(range(num_frames)),
100
+ "frames_indices": frame_indices.tolist(),
101
# extra field used to control hf processor's video
102
# sampling behavior
103
"do_sample_frames": num_frames == total_frames,