PyNvVideoCodec 2.0.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyNvVideoCodec/PyNvVideoCodec_121.cp313-win_amd64.pyd +0 -0
- PyNvVideoCodec/PyNvVideoCodec_130.cp313-win_amd64.pyd +0 -0
- PyNvVideoCodec/VersionCheck.cp313-win_amd64.pyd +0 -0
- PyNvVideoCodec/__init__.py +237 -0
- PyNvVideoCodec/avcodec-58.dll +0 -0
- PyNvVideoCodec/avcodec-61.dll +0 -0
- PyNvVideoCodec/avcodec.dll +0 -0
- PyNvVideoCodec/avdevice-58.dll +0 -0
- PyNvVideoCodec/avdevice-61.dll +0 -0
- PyNvVideoCodec/avdevice.dll +0 -0
- PyNvVideoCodec/avfilter-10.dll +0 -0
- PyNvVideoCodec/avfilter-7.dll +0 -0
- PyNvVideoCodec/avfilter.dll +0 -0
- PyNvVideoCodec/avformat-58.dll +0 -0
- PyNvVideoCodec/avformat-61.dll +0 -0
- PyNvVideoCodec/avformat.dll +0 -0
- PyNvVideoCodec/avutil-56.dll +0 -0
- PyNvVideoCodec/avutil-59.dll +0 -0
- PyNvVideoCodec/avutil.dll +0 -0
- PyNvVideoCodec/decoders/SimpleDecoder.py +225 -0
- PyNvVideoCodec/decoders/ThreadedDecoder.py +119 -0
- PyNvVideoCodec/decoders/__init__.py +22 -0
- PyNvVideoCodec/swresample-3.dll +0 -0
- PyNvVideoCodec/swresample-5.dll +0 -0
- PyNvVideoCodec/swresample.dll +0 -0
- PyNvVideoCodec/swscale-5.dll +0 -0
- PyNvVideoCodec/swscale-8.dll +0 -0
- PyNvVideoCodec/swscale.dll +0 -0
- PyNvVideoCodec/transcoder/Transcoder.py +55 -0
- PyNvVideoCodec/transcoder/__init__.py +22 -0
- PyNvVideoCodec-2.0.0.data/data/external/ffmpeg/readme.txt +10 -0
- PyNvVideoCodec-2.0.0.data/data/external/ffmpeg/src/ffmpeg-7.0.2.tar.xz +0 -0
- PyNvVideoCodec-2.0.0.dist-info/METADATA +77 -0
- PyNvVideoCodec-2.0.0.dist-info/RECORD +64 -0
- PyNvVideoCodec-2.0.0.dist-info/WHEEL +5 -0
- PyNvVideoCodec-2.0.0.dist-info/licenses/LICENSE.txt +22 -0
- PyNvVideoCodec-2.0.0.dist-info/licenses/NOTICES.txt +167 -0
- PyNvVideoCodec-2.0.0.dist-info/top_level.txt +3 -0
- benchmarks/cached_decoder_benchmark.py +635 -0
- benchmarks/frame_sampling_benchmark.py +610 -0
- benchmarks/segmented_transcode_benchmark.py +1072 -0
- samples/Decode.py +194 -0
- samples/DecodeMultiprocessing.py +162 -0
- samples/DecodePerf.py +215 -0
- samples/DecodeSEIMsgExtraction.py +338 -0
- samples/DemuxFromByteArray.py +185 -0
- samples/Encode.py +189 -0
- samples/EncodeFromCPUBuffer.py +220 -0
- samples/EncodeMultiprocessing.py +268 -0
- samples/EncodePerf.py +380 -0
- samples/EncodeReconfigure.py +201 -0
- samples/EncodeSEIMsgInsertion.py +189 -0
- samples/ObjectDetection.py +218 -0
- samples/SimpleDecoder/EnsembleApp.py +416 -0
- samples/SimpleDecoder/SubsamplingAndReconfigure.py +341 -0
- samples/SimpleDecoder/fileLists.txt +4 -0
- samples/SimpleDecoder/segments.txt +3 -0
- samples/SimpleDecoder/timeListInSeconds.txt +5 -0
- samples/SimpleDecoder/transcode_config.json +6 -0
- samples/Utils.py +290 -0
- samples/coco_classes.pickle +0 -0
- samples/encode_config.json +16 -0
- samples/encode_config_lowlatency.json +11 -0
- samples/encode_config_perf.json +9 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# This copyright notice applies to this file only
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
8
|
+
# to deal in the Software without restriction, including without limitation
|
|
9
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
10
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
11
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
|
14
|
+
# all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
19
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
22
|
+
# DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import platform
|
|
26
|
+
import json
|
|
27
|
+
import time
|
|
28
|
+
import torch
|
|
29
|
+
import numpy as np
|
|
30
|
+
from concurrent.futures import ThreadPoolExecutor, wait
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
import PyNvVideoCodec as nvc
|
|
33
|
+
import pycuda.driver as cuda
|
|
34
|
+
import pycuda.autoinit
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
37
|
+
from abc import ABC, abstractmethod
|
|
38
|
+
from tabulate import tabulate
|
|
39
|
+
import threading
|
|
40
|
+
import subprocess
|
|
41
|
+
import argparse
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
PyNvVideoCodec Benchmarking Script
|
|
45
|
+
|
|
46
|
+
This script performs benchmarking tests on the PyNvVideoCodec decoder to evaluate its performance
|
|
47
|
+
under different conditions. It measures:
|
|
48
|
+
1. Decoding performance with different thread counts
|
|
49
|
+
2. Efficiency of direct frame sampling vs sequential decoding
|
|
50
|
+
3. Performance across different video configurations (resolution, GOP size)
|
|
51
|
+
|
|
52
|
+
The script creates test videos with various configurations and then runs benchmarks with:
|
|
53
|
+
- Different thread counts (1, n where n is the number of NVDecs)
|
|
54
|
+
- Different decoding patterns:
|
|
55
|
+
* Sequential decoding of first N frames
|
|
56
|
+
* Uniform sampling of frames
|
|
57
|
+
* Random sampling of frames
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
python pynvc_benchmarking_script.py --nvdecs <number_of_nvdecs> [options]
|
|
61
|
+
|
|
62
|
+
Required Arguments:
|
|
63
|
+
--nvdecs <number> Number of NVDecs available on the system
|
|
64
|
+
|
|
65
|
+
Optional Arguments:
|
|
66
|
+
--verbose, -v Show detailed thread-level performance information (default: False)
|
|
67
|
+
--resolution, -res Video resolution (default: 1920x1080)
|
|
68
|
+
--gop, -g GOP sizes to test, space-separated list (default: 30 250)
|
|
69
|
+
--duration, -d Video duration in seconds (default: 30)
|
|
70
|
+
--fps, -f Video frames per second (default: 30)
|
|
71
|
+
--num-seq-frames, -seq Number of frames to decode sequentially (default: 100)
|
|
72
|
+
--num-samp-frames, -samp Number of frames to sample for uniform/random patterns (default: 30)
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
python pynvc_benchmarking_script.py --nvdecs 3 --resolution 1280x720 --gop 30 60 --verbose
|
|
76
|
+
This will run benchmarks with thread counts [1, 3], 720p resolution, GOP sizes 30 and 60,
|
|
77
|
+
and show detailed thread information.
|
|
78
|
+
|
|
79
|
+
Output:
|
|
80
|
+
The script generates two types of tables:
|
|
81
|
+
1. Main Results Table: Shows FPS and efficiency for each configuration
|
|
82
|
+
2. Thread Details Table (with --verbose): Shows per-thread FPS performance
|
|
83
|
+
|
|
84
|
+
Results are also saved to a JSON file for further analysis.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class DecoderPerformance:
|
|
90
|
+
decoder_name: str
|
|
91
|
+
video_file: str
|
|
92
|
+
frames_decoded: int
|
|
93
|
+
thread_execution_times: List[float]
|
|
94
|
+
overall_fps: float
|
|
95
|
+
per_thread_fps: List[float]
|
|
96
|
+
thread_count: int
|
|
97
|
+
test_description: str
|
|
98
|
+
sampling_method: str # "sequential", "uniform", or "random"
|
|
99
|
+
sequential_decode_time: Optional[float] = None
|
|
100
|
+
sampling_efficiency: Optional[float] = None # sequential_time / actual_time
|
|
101
|
+
|
|
102
|
+
class VideoFrameDecoder(ABC):
|
|
103
|
+
def __init__(self, worker_count: int = 4):
|
|
104
|
+
self.worker_count = worker_count
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def get_frames_at_timestamps(self, video_path: str, timestamps: List[float]) -> Tuple[List[np.ndarray], List[float]]:
|
|
108
|
+
"""Retrieve video frames at specified timestamps"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def get_sequential_frames(self, video_path: str, frame_count: int, target_timestamp: float = None) -> Tuple[List[np.ndarray], List[float]]:
|
|
113
|
+
"""Retrieve frames sequentially from the start of the video"""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def get_benchmark_description(self, frame_count: int, pattern_type: str) -> str:
|
|
117
|
+
"""Generate description for the benchmark test case"""
|
|
118
|
+
if pattern_type == "sequential":
|
|
119
|
+
return f"Sequential retrieval of first {frame_count} frames"
|
|
120
|
+
return f"Sampling {frame_count} frames with {pattern_type} pattern"
|
|
121
|
+
|
|
122
|
+
class PyNvVideoCodecDecoder(VideoFrameDecoder):
|
|
123
|
+
def __init__(self, worker_count: int = 4):
|
|
124
|
+
super().__init__(worker_count)
|
|
125
|
+
self.decoders = []
|
|
126
|
+
self.video_file = None
|
|
127
|
+
self.lock = threading.Lock() # For thread-safe decoder initialization
|
|
128
|
+
|
|
129
|
+
def get_frames_at_timestamps(self, video_path: str, timestamps: List[float]) -> Tuple[List[np.ndarray], List[float]]:
|
|
130
|
+
"""Retrieve video frames at specified timestamps"""
|
|
131
|
+
with self.lock:
|
|
132
|
+
if not self.decoders:
|
|
133
|
+
self.decoders = [nvc.SimpleDecoder(video_path, bWaitForSessionWarmUp=True) for _ in range(self.worker_count)]
|
|
134
|
+
else:
|
|
135
|
+
for decoder in self.decoders:
|
|
136
|
+
decoder.reconfigure_decoder(video_path)
|
|
137
|
+
self.video_file = video_path
|
|
138
|
+
|
|
139
|
+
nvc.PyNvDecoder.SetSessionCount(self.worker_count)
|
|
140
|
+
|
|
141
|
+
frames = []
|
|
142
|
+
thread_times = []
|
|
143
|
+
|
|
144
|
+
def decode_worker(decoder_idx: int, timestamps: List[float]) -> Tuple[List[np.ndarray], float]:
|
|
145
|
+
start_time = time.time()
|
|
146
|
+
batch_frames = []
|
|
147
|
+
decoder = self.decoders[decoder_idx]
|
|
148
|
+
target_indices = []
|
|
149
|
+
|
|
150
|
+
for timestamp in timestamps:
|
|
151
|
+
index = decoder.get_index_from_time_in_seconds(timestamp)
|
|
152
|
+
target_indices.append(index)
|
|
153
|
+
|
|
154
|
+
batch_frames = decoder.get_batch_frames_by_index(target_indices)
|
|
155
|
+
|
|
156
|
+
end_time = time.time()
|
|
157
|
+
elapsed_time = end_time - start_time
|
|
158
|
+
session_init_time = decoder.get_session_init_time()/1000
|
|
159
|
+
elapsed_time -= session_init_time
|
|
160
|
+
return batch_frames, elapsed_time
|
|
161
|
+
|
|
162
|
+
with ThreadPoolExecutor(max_workers=self.worker_count) as executor:
|
|
163
|
+
futures = []
|
|
164
|
+
for i in range(self.worker_count):
|
|
165
|
+
futures.append(executor.submit(decode_worker, i, timestamps))
|
|
166
|
+
|
|
167
|
+
for future in futures:
|
|
168
|
+
batch_frames, thread_time = future.result()
|
|
169
|
+
frames.extend(batch_frames)
|
|
170
|
+
thread_times.append(thread_time)
|
|
171
|
+
|
|
172
|
+
return frames, thread_times
|
|
173
|
+
|
|
174
|
+
def get_sequential_frames(self, video_path: str, frame_count: int = None, target_timestamp: float = None) -> Tuple[List[np.ndarray], List[float]]:
|
|
175
|
+
"""Retrieve frames sequentially from the start of the video"""
|
|
176
|
+
with self.lock:
|
|
177
|
+
if not self.decoders:
|
|
178
|
+
self.decoders = [nvc.SimpleDecoder(video_path, bWaitForSessionWarmUp=True) for _ in range(self.worker_count)]
|
|
179
|
+
else:
|
|
180
|
+
for decoder in self.decoders:
|
|
181
|
+
decoder.reconfigure_decoder(video_path)
|
|
182
|
+
self.video_file = video_path
|
|
183
|
+
|
|
184
|
+
nvc.PyNvDecoder.SetSessionCount(self.worker_count)
|
|
185
|
+
|
|
186
|
+
frames = []
|
|
187
|
+
thread_times = []
|
|
188
|
+
|
|
189
|
+
def decode_worker(decoder_idx: int, frame_count: int, target_timestamp: float = None) -> Tuple[List[np.ndarray], float]:
|
|
190
|
+
batch_frames = []
|
|
191
|
+
decoder = self.decoders[decoder_idx]
|
|
192
|
+
if target_timestamp:
|
|
193
|
+
frame_count = decoder.get_index_from_time_in_seconds(target_timestamp)
|
|
194
|
+
|
|
195
|
+
start_time = time.time()
|
|
196
|
+
for i in range(0, frame_count, 5):
|
|
197
|
+
decoder.seek_to_index(i)
|
|
198
|
+
batch = decoder.get_batch_frames(5)
|
|
199
|
+
batch_frames.extend(batch)
|
|
200
|
+
if len(batch_frames) >= frame_count:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
end_time = time.time()
|
|
204
|
+
elapsed_time = end_time - start_time
|
|
205
|
+
session_init_time = decoder.get_session_init_time()/1000
|
|
206
|
+
elapsed_time -= session_init_time
|
|
207
|
+
return batch_frames[:frame_count], elapsed_time
|
|
208
|
+
|
|
209
|
+
with ThreadPoolExecutor(max_workers=self.worker_count) as executor:
|
|
210
|
+
futures = []
|
|
211
|
+
for i in range(self.worker_count):
|
|
212
|
+
futures.append(executor.submit(decode_worker, i, frame_count, target_timestamp))
|
|
213
|
+
|
|
214
|
+
for future in futures:
|
|
215
|
+
batch_frames, thread_time = future.result()
|
|
216
|
+
frames.extend(batch_frames)
|
|
217
|
+
thread_times.append(thread_time)
|
|
218
|
+
|
|
219
|
+
return frames, thread_times
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_system_metadata() -> Dict[str, Any]:
|
|
223
|
+
"""Get system metadata for benchmarking results"""
|
|
224
|
+
return {
|
|
225
|
+
"cpu_count": os.cpu_count(),
|
|
226
|
+
"system": platform.system(),
|
|
227
|
+
"machine": platform.machine(),
|
|
228
|
+
"python_version": str(platform.python_version()),
|
|
229
|
+
"cuda": torch.cuda.get_device_properties(0).name if torch.cuda.is_available() else "not available",
|
|
230
|
+
"cuda_available": torch.cuda.is_available()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def generate_mandelbrot_video(width: int, height: int, duration: int, fps: int, gop: int, output_path: str) -> str:
|
|
234
|
+
"""
|
|
235
|
+
Generate a Mandelbrot set video using FFmpeg
|
|
236
|
+
|
|
237
|
+
Parameters:
|
|
238
|
+
width (int): Video width
|
|
239
|
+
height (int): Video height
|
|
240
|
+
duration (int): Video duration in seconds
|
|
241
|
+
fps (int): Frames per second
|
|
242
|
+
gop (int): GOP size
|
|
243
|
+
output_path (str): Path to save the generated video
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
str: Path to the generated video
|
|
247
|
+
"""
|
|
248
|
+
print(f"Generating Mandelbrot set video at {width}x{height}, {duration}s duration, {fps} fps, {gop} GOP size")
|
|
249
|
+
|
|
250
|
+
# Create the directory if it doesn't exist
|
|
251
|
+
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
|
|
252
|
+
|
|
253
|
+
# FFmpeg command to generate a Mandelbrot set video
|
|
254
|
+
cmd = [
|
|
255
|
+
"ffmpeg", "-y",
|
|
256
|
+
"-v", "warning",
|
|
257
|
+
# Video input from mandelbrot filter
|
|
258
|
+
"-f", "lavfi",
|
|
259
|
+
"-i", f"mandelbrot=s={width}x{height}",
|
|
260
|
+
# Set duration
|
|
261
|
+
"-t", str(duration),
|
|
262
|
+
# Video encoding with h264 and specific parameters
|
|
263
|
+
"-c:v", "h264_nvenc",
|
|
264
|
+
"-preset", "medium",
|
|
265
|
+
"-b:v", "8M",
|
|
266
|
+
"-bf", "3", # 3 B-frames
|
|
267
|
+
"-g", str(gop), # GOP size
|
|
268
|
+
"-rc", "vbr", # Variable bitrate
|
|
269
|
+
"-profile:v", "high", # High profile for better quality
|
|
270
|
+
output_path
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
# Execute FFmpeg command and wait for completion
|
|
275
|
+
print("Starting FFmpeg process to generate Mandelbrot video...")
|
|
276
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
277
|
+
stdout, stderr = process.communicate()
|
|
278
|
+
|
|
279
|
+
if process.returncode != 0:
|
|
280
|
+
print(f"Error generating video: {stderr.decode()}")
|
|
281
|
+
raise subprocess.CalledProcessError(process.returncode, cmd, stderr)
|
|
282
|
+
|
|
283
|
+
print(f"Video generation complete: {output_path}")
|
|
284
|
+
# Print video information
|
|
285
|
+
print(f"Video codec: h264_nvenc with {gop} GOP size and 3 B-frames")
|
|
286
|
+
return output_path
|
|
287
|
+
except subprocess.CalledProcessError as e:
|
|
288
|
+
print(f"FFmpeg error: {e}")
|
|
289
|
+
raise
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"Error: {e}")
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def generate_benchmark_videos(
|
|
296
|
+
resolution: str,
|
|
297
|
+
duration: int,
|
|
298
|
+
fps: int,
|
|
299
|
+
gop_sizes: List[int],
|
|
300
|
+
output_dir: str
|
|
301
|
+
) -> List[str]:
|
|
302
|
+
"""
|
|
303
|
+
Generate benchmark videos with different GOP sizes
|
|
304
|
+
|
|
305
|
+
Parameters:
|
|
306
|
+
resolution (str): Video resolution (e.g., '1920x1080')
|
|
307
|
+
duration (int): Video duration in seconds
|
|
308
|
+
fps (int): Frames per second
|
|
309
|
+
gop_sizes (List[int]): List of GOP sizes to test
|
|
310
|
+
output_dir (str): Directory to save the generated videos
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
List[str]: List of paths to the generated videos
|
|
314
|
+
"""
|
|
315
|
+
width, height = map(int, resolution.split('x'))
|
|
316
|
+
generated_videos = []
|
|
317
|
+
|
|
318
|
+
for gop in gop_sizes:
|
|
319
|
+
output_path = f"{output_dir}/mandelbrot_{resolution}_{duration}s_{fps}fps_{gop}gop.mp4"
|
|
320
|
+
|
|
321
|
+
# Skip if video already exists
|
|
322
|
+
if os.path.exists(output_path):
|
|
323
|
+
print(f"Skipping {output_path} - already exists")
|
|
324
|
+
generated_videos.append(output_path)
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
video_path = generate_mandelbrot_video(width, height, duration, fps, gop, output_path)
|
|
329
|
+
generated_videos.append(video_path)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
print(f"Failed to generate video with GOP {gop}: {e}")
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
return generated_videos
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_video_metadata(video_file: str) -> Dict[str, Any]:
|
|
338
|
+
"""Get metadata for a video file using PyNvVideoCodec"""
|
|
339
|
+
decoder = nvc.SimpleDecoder(video_file)
|
|
340
|
+
metadata = decoder.get_stream_metadata()
|
|
341
|
+
return {
|
|
342
|
+
"duration": metadata.duration,
|
|
343
|
+
"width": metadata.width,
|
|
344
|
+
"height": metadata.height,
|
|
345
|
+
"fps": metadata.average_fps,
|
|
346
|
+
"codec": metadata.codec_name
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def create_timestamp_list(video_file: str, num_samples: int, kind: str = "uniform") -> List[float]:
|
|
351
|
+
"""Generate list of timestamps for frame sampling"""
|
|
352
|
+
metadata = get_video_metadata(video_file)
|
|
353
|
+
duration = metadata["duration"]
|
|
354
|
+
|
|
355
|
+
if kind == "uniform":
|
|
356
|
+
return [i * duration / num_samples for i in range(num_samples)]
|
|
357
|
+
elif kind == "random":
|
|
358
|
+
# Generate unique random indices and scale to duration
|
|
359
|
+
indices = torch.randperm(int(duration * 1000))[:num_samples] # Convert to milliseconds for more precision
|
|
360
|
+
return (indices.float() / 1000).tolist() # Convert back to seconds
|
|
361
|
+
else:
|
|
362
|
+
raise ValueError(f"Unknown sampling kind: {kind}")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def run_benchmark_test(
|
|
366
|
+
decoder: VideoFrameDecoder,
|
|
367
|
+
video_file: str,
|
|
368
|
+
num_threads: int,
|
|
369
|
+
num_samples: int,
|
|
370
|
+
decode_pattern: str = "first_n_frames"
|
|
371
|
+
) -> DecoderPerformance:
|
|
372
|
+
"""Run a single benchmark test for a decoder"""
|
|
373
|
+
description = ""
|
|
374
|
+
sequential_time = None
|
|
375
|
+
efficiency = None
|
|
376
|
+
|
|
377
|
+
if decode_pattern == "first_n_frames":
|
|
378
|
+
description = decoder.get_benchmark_description(num_samples, "sequential")
|
|
379
|
+
frames, thread_times = decoder.get_sequential_frames(video_file, frame_count=num_samples)
|
|
380
|
+
pattern_str = f"first_{num_samples}_frames"
|
|
381
|
+
else: # uniform or random
|
|
382
|
+
num_samples = 30
|
|
383
|
+
description = decoder.get_benchmark_description(num_samples, decode_pattern)
|
|
384
|
+
timestamp_list = create_timestamp_list(video_file, num_samples, decode_pattern)
|
|
385
|
+
|
|
386
|
+
# First measure sequential decode time to the last timestamp
|
|
387
|
+
last_timestamp = max(timestamp_list)
|
|
388
|
+
_, seq_thread_times = decoder.get_sequential_frames(video_file, frame_count=None, target_timestamp=last_timestamp)
|
|
389
|
+
sequential_time = sum(seq_thread_times)
|
|
390
|
+
|
|
391
|
+
# Then measure random/uniform decode time
|
|
392
|
+
frames, thread_times = decoder.get_frames_at_timestamps(video_file, timestamp_list)
|
|
393
|
+
efficiency = sequential_time / sum(thread_times) if sum(thread_times) > 0 else 0
|
|
394
|
+
pattern_str = f"{num_samples}_{decode_pattern}_frames"
|
|
395
|
+
|
|
396
|
+
total_frames = len(frames)
|
|
397
|
+
thread_fps = [num_samples / t for t in thread_times]
|
|
398
|
+
total_fps = sum(thread_fps)
|
|
399
|
+
|
|
400
|
+
return DecoderPerformance(
|
|
401
|
+
decoder_name=decoder.__class__.__name__,
|
|
402
|
+
video_file=video_file,
|
|
403
|
+
frames_decoded=total_frames,
|
|
404
|
+
thread_execution_times=thread_times,
|
|
405
|
+
overall_fps=total_fps,
|
|
406
|
+
per_thread_fps=thread_fps,
|
|
407
|
+
thread_count=num_threads,
|
|
408
|
+
test_description=description,
|
|
409
|
+
sampling_method=pattern_str,
|
|
410
|
+
sequential_decode_time=sequential_time,
|
|
411
|
+
sampling_efficiency=efficiency
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def display_benchmark_results(results: List[DecoderPerformance], verbose: bool = False) -> None:
|
|
416
|
+
"""Display benchmark results in formatted tables"""
|
|
417
|
+
|
|
418
|
+
videos = sorted(set(r.video_file for r in results))
|
|
419
|
+
patterns = sorted(set(r.sampling_method for r in results))
|
|
420
|
+
thread_counts = sorted(set(r.thread_count for r in results))
|
|
421
|
+
|
|
422
|
+
print("\nBenchmark Results:")
|
|
423
|
+
|
|
424
|
+
table_data = []
|
|
425
|
+
headers = ["Video Config", "Pattern"]
|
|
426
|
+
|
|
427
|
+
for thread_count in thread_counts:
|
|
428
|
+
headers.extend([f"T{thread_count} FPS", f"T{thread_count} Eff"])
|
|
429
|
+
|
|
430
|
+
for video in videos:
|
|
431
|
+
# Get the filename without path and extension
|
|
432
|
+
filename = os.path.basename(video)
|
|
433
|
+
# Split by underscore and remove .mp4 from the last part
|
|
434
|
+
parts = filename.split('_')
|
|
435
|
+
resolution = parts[1]
|
|
436
|
+
duration = parts[2].replace('s', '')
|
|
437
|
+
gop = parts[4].replace('gop.mp4', '')
|
|
438
|
+
config_str = f"{resolution} {gop}gop {duration}s"
|
|
439
|
+
|
|
440
|
+
for pattern in patterns:
|
|
441
|
+
row = [config_str, pattern]
|
|
442
|
+
|
|
443
|
+
for thread_count in thread_counts:
|
|
444
|
+
result = next((r for r in results
|
|
445
|
+
if r.video_file == video
|
|
446
|
+
and r.sampling_method == pattern
|
|
447
|
+
and r.thread_count == thread_count), None)
|
|
448
|
+
if result:
|
|
449
|
+
row.append(f"{result.overall_fps:.1f}")
|
|
450
|
+
if result.sampling_efficiency is not None:
|
|
451
|
+
row.append(f"{result.sampling_efficiency:.2f}x")
|
|
452
|
+
else:
|
|
453
|
+
row.append("N/A")
|
|
454
|
+
else:
|
|
455
|
+
row.append("N/A")
|
|
456
|
+
row.append("N/A")
|
|
457
|
+
|
|
458
|
+
table_data.append(row)
|
|
459
|
+
|
|
460
|
+
print(tabulate(table_data, headers=headers, tablefmt="grid"))
|
|
461
|
+
|
|
462
|
+
# Only show thread-level details if verbose mode is enabled
|
|
463
|
+
if verbose:
|
|
464
|
+
print(f"\nThread Details:")
|
|
465
|
+
|
|
466
|
+
thread_headers = ["Config", "Threads", "FPS/Thread"]
|
|
467
|
+
thread_data = []
|
|
468
|
+
|
|
469
|
+
for video in videos:
|
|
470
|
+
# Get the filename without path and extension
|
|
471
|
+
filename = os.path.basename(video)
|
|
472
|
+
# Split by underscore and remove .mp4 from the last part
|
|
473
|
+
parts = filename.split('_')
|
|
474
|
+
resolution = parts[1]
|
|
475
|
+
duration = parts[2].replace('s', '')
|
|
476
|
+
gop = parts[4].replace('gop.mp4', '')
|
|
477
|
+
config_str = f"{resolution} {gop}gop {duration}s"
|
|
478
|
+
|
|
479
|
+
for pattern in patterns:
|
|
480
|
+
for thread_count in thread_counts:
|
|
481
|
+
result = next((r for r in results
|
|
482
|
+
if r.video_file == video
|
|
483
|
+
and r.sampling_method == pattern
|
|
484
|
+
and r.thread_count == thread_count), None)
|
|
485
|
+
|
|
486
|
+
if result:
|
|
487
|
+
thread_fps_str = ",".join([f"{fps:.1f}" for fps in result.per_thread_fps])
|
|
488
|
+
thread_data.append([
|
|
489
|
+
f"{config_str} {pattern}",
|
|
490
|
+
thread_count,
|
|
491
|
+
thread_fps_str
|
|
492
|
+
])
|
|
493
|
+
|
|
494
|
+
# Print thread-level details table
|
|
495
|
+
print(tabulate(thread_data, headers=thread_headers, tablefmt="grid"))
|
|
496
|
+
|
|
497
|
+
print("\nEfficiency Explanation:")
|
|
498
|
+
print("Efficiency (Eff) represents the performance comparison between two approaches:")
|
|
499
|
+
print("1. Direct sampling: Decoding specific frames directly using seek operations")
|
|
500
|
+
print("2. Sequential decode + sampling: Decoding all frames sequentially and then sampling the required frames")
|
|
501
|
+
print("The efficiency value shows how much faster direct sampling is compared to sequential decoding with sampling.")
|
|
502
|
+
print("Higher efficiency values indicate better performance of direct sampling approach.")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def save_benchmark_results(results: List[DecoderPerformance], output_file: str = "benchmark_results.json") -> None:
|
|
506
|
+
"""Save benchmark results to a JSON file"""
|
|
507
|
+
output_data = {
|
|
508
|
+
"system_info": get_system_metadata(),
|
|
509
|
+
"benchmark_results": [
|
|
510
|
+
{
|
|
511
|
+
"decoder": r.decoder_name,
|
|
512
|
+
"video": r.video_file,
|
|
513
|
+
"frames_decoded": r.frames_decoded,
|
|
514
|
+
"thread_execution_times": r.thread_execution_times,
|
|
515
|
+
"overall_fps": r.overall_fps,
|
|
516
|
+
"per_thread_fps": r.per_thread_fps,
|
|
517
|
+
"thread_count": r.thread_count,
|
|
518
|
+
"test_description": r.test_description,
|
|
519
|
+
"sampling_method": r.sampling_method,
|
|
520
|
+
"sequential_decode_time": r.sequential_decode_time,
|
|
521
|
+
"sampling_efficiency": r.sampling_efficiency
|
|
522
|
+
}
|
|
523
|
+
for r in results
|
|
524
|
+
]
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
with open(output_file, "w") as f:
|
|
528
|
+
json.dump(output_data, f, indent=4)
|
|
529
|
+
|
|
530
|
+
print(f"\nBenchmark results saved to {output_file}")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def main():
|
|
534
|
+
# Parse command line arguments
|
|
535
|
+
parser = argparse.ArgumentParser(description='PyNvVideoCodec Benchmarking Script')
|
|
536
|
+
parser.add_argument('--nvdecs', type=int, required=True,
|
|
537
|
+
help='Number of NVDecs available on the system')
|
|
538
|
+
parser.add_argument('--verbose', '-v', action='store_true',
|
|
539
|
+
help='Show detailed thread-level performance information (default: False)')
|
|
540
|
+
parser.add_argument('--resolution', '-res', type=str, default='1920x1080',
|
|
541
|
+
help='Video resolution (default: 1920x1080)')
|
|
542
|
+
parser.add_argument('--gop', '-g', type=int, nargs='+', default=[30, 250],
|
|
543
|
+
help='GOP sizes to test (space-separated list) (default: 30 250)')
|
|
544
|
+
parser.add_argument('--duration', '-d', type=int, default=30,
|
|
545
|
+
help='Video duration in seconds (default: 30)')
|
|
546
|
+
parser.add_argument('--fps', '-f', type=int, default=30,
|
|
547
|
+
help='Video frames per second (default: 30)')
|
|
548
|
+
parser.add_argument('--num-seq-frames', '-seq', type=int, default=100,
|
|
549
|
+
help='Number of frames to decode sequentially (default: 100)')
|
|
550
|
+
parser.add_argument('--num-samp-frames', '-samp', type=int, default=30,
|
|
551
|
+
help='Number of frames to sample for uniform/random patterns (default: 30)')
|
|
552
|
+
args = parser.parse_args()
|
|
553
|
+
|
|
554
|
+
# Set thread counts based on number of NVDecs
|
|
555
|
+
thread_counts = [1, args.nvdecs] if args.nvdecs > 1 else [1]
|
|
556
|
+
print(f"Using thread counts: {thread_counts} based on {args.nvdecs} NVDecs")
|
|
557
|
+
|
|
558
|
+
start_time = time.time()
|
|
559
|
+
|
|
560
|
+
# Create videos directory in the same location as the script
|
|
561
|
+
script_dir = Path(__file__).parent
|
|
562
|
+
videos_dir_path = script_dir / "benchmark_videos"
|
|
563
|
+
videos_dir_path.mkdir(parents=True, exist_ok=True)
|
|
564
|
+
print(f"Using videos directory: {videos_dir_path}")
|
|
565
|
+
|
|
566
|
+
# Generate benchmark videos
|
|
567
|
+
video_files = generate_benchmark_videos(
|
|
568
|
+
args.resolution,
|
|
569
|
+
args.duration,
|
|
570
|
+
args.fps,
|
|
571
|
+
args.gop,
|
|
572
|
+
str(videos_dir_path)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Benchmark parameters
|
|
576
|
+
decode_patterns = ["first_n_frames", "uniform", "random"]
|
|
577
|
+
|
|
578
|
+
results = []
|
|
579
|
+
|
|
580
|
+
# Run benchmarks
|
|
581
|
+
for video_file in video_files:
|
|
582
|
+
if not os.path.exists(video_file):
|
|
583
|
+
print(f"Skipping {video_file} - file not found")
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
for num_threads in thread_counts:
|
|
587
|
+
print(f"\nBenchmarking {video_file} with {num_threads} threads")
|
|
588
|
+
|
|
589
|
+
# PyNvVideoCodec benchmark
|
|
590
|
+
pynv_decoder = PyNvVideoCodecDecoder(num_threads)
|
|
591
|
+
for pattern in decode_patterns:
|
|
592
|
+
num_samples = args.num_seq_frames if pattern == "first_n_frames" else args.num_samp_frames
|
|
593
|
+
pynv_result = run_benchmark_test(pynv_decoder, video_file, num_threads, num_samples, pattern)
|
|
594
|
+
results.append(pynv_result)
|
|
595
|
+
|
|
596
|
+
# Display and save results
|
|
597
|
+
display_benchmark_results(results, verbose=args.verbose)
|
|
598
|
+
save_benchmark_results(results)
|
|
599
|
+
|
|
600
|
+
# Display total execution time
|
|
601
|
+
end_time = time.time()
|
|
602
|
+
total_time = end_time - start_time
|
|
603
|
+
hours = int(total_time // 3600)
|
|
604
|
+
minutes = int((total_time % 3600) // 60)
|
|
605
|
+
seconds = total_time % 60
|
|
606
|
+
print(f"\nTotal execution time: {hours:02d}:{minutes:02d}:{seconds:06.3f} ({total_time:.3f} seconds)")
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
if __name__ == "__main__":
|
|
610
|
+
main()
|