PyNvVideoCodec 2.2.1__cp313-cp313-win_arm64.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.
Files changed (63) hide show
  1. PyNvVideoCodec/PyNvVideoCodec_121.cp313-win_arm64.pyd +0 -0
  2. PyNvVideoCodec/PyNvVideoCodec_130.cp313-win_arm64.pyd +0 -0
  3. PyNvVideoCodec/VersionCheck.cp313-win_arm64.pyd +0 -0
  4. PyNvVideoCodec/__init__.py +251 -0
  5. PyNvVideoCodec/avformat-62.dll +0 -0
  6. PyNvVideoCodec/avutil-60.dll +0 -0
  7. PyNvVideoCodec/decoders/SimpleDecoder.py +273 -0
  8. PyNvVideoCodec/decoders/ThreadedDecoder.py +129 -0
  9. PyNvVideoCodec/decoders/__init__.py +22 -0
  10. PyNvVideoCodec/transcoder/Transcoder.py +55 -0
  11. PyNvVideoCodec/transcoder/__init__.py +22 -0
  12. benchmarks/cached_decoder_benchmark.py +813 -0
  13. benchmarks/frame_sampling_benchmark.py +765 -0
  14. benchmarks/requirements.txt +52 -0
  15. benchmarks/segmented_transcode_benchmark.py +1546 -0
  16. benchmarks/setup_pycuda_win_arm64.bat +531 -0
  17. pynvvideocodec-2.2.1.data/data/external/ffmpeg/readme.txt +39 -0
  18. pynvvideocodec-2.2.1.data/data/external/ffmpeg/src/ffmpeg-8.1.2.tar.xz +0 -0
  19. pynvvideocodec-2.2.1.dist-info/METADATA +97 -0
  20. pynvvideocodec-2.2.1.dist-info/RECORD +63 -0
  21. pynvvideocodec-2.2.1.dist-info/WHEEL +5 -0
  22. pynvvideocodec-2.2.1.dist-info/licenses/LICENSE.txt +22 -0
  23. pynvvideocodec-2.2.1.dist-info/licenses/NOTICES.txt +167 -0
  24. pynvvideocodec-2.2.1.dist-info/top_level.txt +3 -0
  25. samples/advanced/decode.py +173 -0
  26. samples/advanced/decode_from_memory_buffer.py +204 -0
  27. samples/advanced/decode_perf.py +299 -0
  28. samples/advanced/decode_reconfigure.py +184 -0
  29. samples/advanced/decode_sei_msg.py +193 -0
  30. samples/advanced/decode_with_cuda_control.py +214 -0
  31. samples/advanced/decode_with_low_latency.py +183 -0
  32. samples/advanced/encode_config.json +12 -0
  33. samples/advanced/encode_config_lowlatency.json +13 -0
  34. samples/advanced/encode_perf.py +338 -0
  35. samples/advanced/encode_reconfigure.py +211 -0
  36. samples/advanced/encode_sei_msg.py +188 -0
  37. samples/advanced/simple_decode_from_memory.py +151 -0
  38. samples/advanced/simple_decode_stats.py +179 -0
  39. samples/advanced/transcode.py +288 -0
  40. samples/basic/create_video_segments.py +187 -0
  41. samples/basic/encode.py +574 -0
  42. samples/basic/segments.txt +3 -0
  43. samples/basic/simple_decode_sampling.py +182 -0
  44. samples/basic/transcode_config.json +6 -0
  45. samples/jupyter/ReconfigureDecoder.png +0 -0
  46. samples/jupyter/SimpleDecoderAPIs.png +0 -0
  47. samples/jupyter/ThreadedDecoder.png +0 -0
  48. samples/jupyter/coco_classes.pickle +0 -0
  49. samples/jupyter/face_protect_tutorial.ipynb +881 -0
  50. samples/jupyter/object_detection_tutorial.ipynb +781 -0
  51. samples/jupyter/simple_decode_tutorial.ipynb +674 -0
  52. samples/requirements.txt +84 -0
  53. samples/setup_pycuda_win_arm64.bat +531 -0
  54. samples/utils/Utils.py +405 -0
  55. samples/utils/__init__.py +36 -0
  56. samples/utils/decode_parser.py +642 -0
  57. samples/utils/decode_stats_writer.py +177 -0
  58. samples/utils/encode_parallel_utils.py +174 -0
  59. samples/utils/encode_parser.py +379 -0
  60. samples/utils/frame_utils.py +89 -0
  61. samples/utils/sei_message_parser.py +223 -0
  62. samples/utils/sei_parser.py +223 -0
  63. samples/utils/transcode_parser.py +218 -0
@@ -0,0 +1,765 @@
1
+ # This copyright notice applies to this file only
2
+ #
3
+ # SPDX-FileCopyrightText: Copyright (c) 2026 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
+ import re
43
+ from tqdm import tqdm
44
+
45
+ """
46
+ Objective of this benchmark is to measure sampling performance of PyNvVideoCodec in various sampling scenarios:
47
+ - Sequential decoding of first N frames.
48
+ - Uniform sampling: Sample frames at regular intervals.
49
+ - Random sampling: Sample frames at random intervals.
50
+
51
+ This script performs benchmarking tests on the PyNvVideoCodec decoder frame sampling to evaluate its performance
52
+ under different conditions. It measures:
53
+ 1. Decoding performance with different thread counts (1, n where n is the number of NVDecs)
54
+ 2. Efficiency of direct frame sampling vs sequential decoding
55
+ 3. Performance across different video configurations (resolution, GOP size)
56
+
57
+ The script creates test videos with various configurations and then runs benchmarks with:
58
+ - Different thread counts (1, n where n is the number of NVDecs)
59
+ - Different decoding patterns:
60
+ * Sequential decoding of first N frames
61
+ * Uniform sampling of frames
62
+ * Random sampling of frames
63
+
64
+ NOTE 1: The video clips are generated using FFmpeg only for the first run. Subsequent runs will use the generated video clips.
65
+ The clips are stored in the benchmark_videos directory.
66
+
67
+ NOTE 2: To saturate all the NVDECs and get maximum benefit, run the script with --nvdecs <number_of_nvdecs> as a multiple of the number of NVDecs available on the system.
68
+ Use this link to check the number of NVDecs available on the system: https://developer.nvidia.com/video-encode-decode-support-matrix
69
+
70
+ Usage:
71
+ python frame_sampling_benchmark.py --nvdecs <number_of_nvdecs> --ffmpeg-dir <path> [options]
72
+
73
+ Required Arguments:
74
+ --nvdecs <number> Number of NVDecs available on the system
75
+ --ffmpeg-dir Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).
76
+
77
+ Optional Arguments:
78
+ --verbose, -v Show detailed thread-level performance information (default: False)
79
+ --resolution, -res Video resolution (default: 1920x1080)
80
+ --gop, -g GOP sizes to test, space-separated list (default: 30 250)
81
+ --duration, -d Video duration in seconds (default: 30)
82
+ --fps, -f Video frames per second (default: 30)
83
+ --num-seq-frames, -seq Number of frames to decode sequentially (default: 100)
84
+ --num-samp-frames, -samp Number of frames to sample for uniform/random patterns (default: 30)
85
+
86
+ Please note that the following arguments are required for video generation:
87
+ --resolution, -res Video resolution (default: 1920x1080)
88
+ --gop, -g GOP sizes to test, space-separated list (default: 30 250)
89
+ --duration, -d Video duration in seconds (default: 30)
90
+ --fps, -f Video frames per second (default: 30)
91
+
92
+ Example:
93
+ python frame_sampling_benchmark.py --nvdecs 3 --resolution 1280x720 --gop 30 60 --verbose
94
+ This will run benchmarks with thread counts [1, 3], 720p resolution, GOP sizes 30 and 60,
95
+ and show detailed thread information.
96
+
97
+ python frame_sampling_benchmark.py --nvdecs 2 --ffmpeg-dir /path/to/ffmpeg-n7.1-linux64-lgpl-shared
98
+ Use FFmpeg from the given installation directory for generating benchmark videos.
99
+
100
+ Output:
101
+ The script generates two types of tables:
102
+ 1. Main Results Table: Shows FPS and efficiency for each configuration
103
+ 2. Thread Details Table (with --verbose): Shows per-thread FPS performance
104
+
105
+ Results are also saved to a JSON file named benchmark_results.json for further analysis.
106
+ """
107
+
108
+
109
+ @dataclass
110
+ class DecoderPerformance:
111
+ decoder_name: str
112
+ video_file: str
113
+ frames_decoded: int
114
+ thread_execution_times: List[float]
115
+ overall_fps: float
116
+ per_thread_fps: List[float]
117
+ thread_count: int
118
+ test_description: str
119
+ sampling_method: str # "sequential", "uniform", or "random"
120
+ sequential_decode_time: Optional[float] = None
121
+ sampling_efficiency: Optional[float] = None # sequential_time / actual_time
122
+
123
+ class VideoFrameDecoder(ABC):
124
+ def __init__(self, worker_count: int = 4):
125
+ self.worker_count = worker_count
126
+
127
+ @abstractmethod
128
+ def get_frames_at_timestamps(self, video_path: str, timestamps: List[float]) -> Tuple[List[np.ndarray], List[float]]:
129
+ """Retrieve video frames at specified timestamps"""
130
+ pass
131
+
132
+ @abstractmethod
133
+ def get_sequential_frames(self, video_path: str, frame_count: int, target_timestamp: float = None) -> Tuple[List[np.ndarray], List[float]]:
134
+ """Retrieve frames sequentially from the start of the video"""
135
+ pass
136
+
137
+ def get_benchmark_description(self, frame_count: int, pattern_type: str) -> str:
138
+ """Generate description for the benchmark test case"""
139
+ if pattern_type == "sequential":
140
+ return f"Sequential retrieval of first {frame_count} frames"
141
+ return f"Sampling {frame_count} frames with {pattern_type} pattern"
142
+
143
+ class PyNvVideoCodecDecoder(VideoFrameDecoder):
144
+ def __init__(self, worker_count: int = 4):
145
+ super().__init__(worker_count)
146
+ self.decoders = []
147
+ self.video_file = None
148
+ self.lock = threading.Lock() # For thread-safe decoder initialization
149
+
150
+ def get_frames_at_timestamps(self, video_path: str, timestamps: List[float]) -> Tuple[List[np.ndarray], List[float]]:
151
+ """Retrieve video frames at specified timestamps"""
152
+ with self.lock:
153
+ if not self.decoders:
154
+ self.decoders = [nvc.SimpleDecoder(video_path, bWaitForSessionWarmUp=True) for _ in range(self.worker_count)]
155
+ else:
156
+ for decoder in self.decoders:
157
+ decoder.reconfigure_decoder(video_path)
158
+ self.video_file = video_path
159
+
160
+ nvc.PyNvDecoder.SetSessionCount(self.worker_count)
161
+
162
+ frames = []
163
+ thread_times = []
164
+
165
+ def decode_worker(decoder_idx: int, timestamps: List[float]) -> Tuple[List[np.ndarray], float]:
166
+ start_time = time.time()
167
+ batch_frames = []
168
+ decoder = self.decoders[decoder_idx]
169
+ target_indices = []
170
+
171
+ for timestamp in timestamps:
172
+ index = decoder.get_index_from_time_in_seconds(timestamp)
173
+ target_indices.append(index)
174
+
175
+ batch_frames = decoder.get_batch_frames_by_index(target_indices)
176
+
177
+ end_time = time.time()
178
+ elapsed_time = end_time - start_time
179
+ session_init_time = decoder.get_session_init_time()/1000
180
+ elapsed_time -= session_init_time
181
+ return batch_frames, elapsed_time
182
+
183
+ with ThreadPoolExecutor(max_workers=self.worker_count) as executor:
184
+ futures = []
185
+ for i in range(self.worker_count):
186
+ futures.append(executor.submit(decode_worker, i, timestamps))
187
+
188
+ for future in futures:
189
+ batch_frames, thread_time = future.result()
190
+ frames.extend(batch_frames)
191
+ thread_times.append(thread_time)
192
+
193
+ return frames, thread_times
194
+
195
+ def get_sequential_frames(self, video_path: str, frame_count: int = None, target_timestamp: float = None) -> Tuple[List[np.ndarray], List[float]]:
196
+ """Retrieve frames sequentially from the start of the video"""
197
+ with self.lock:
198
+ if not self.decoders:
199
+ self.decoders = [nvc.SimpleDecoder(video_path, bWaitForSessionWarmUp=True) for _ in range(self.worker_count)]
200
+ else:
201
+ for decoder in self.decoders:
202
+ decoder.reconfigure_decoder(video_path)
203
+ self.video_file = video_path
204
+
205
+ nvc.PyNvDecoder.SetSessionCount(self.worker_count)
206
+
207
+ frames = []
208
+ thread_times = []
209
+
210
+ def decode_worker(decoder_idx: int, frame_count: int, target_timestamp: float = None) -> Tuple[List[np.ndarray], float]:
211
+ batch_frames = []
212
+ decoder = self.decoders[decoder_idx]
213
+ if target_timestamp:
214
+ frame_count = decoder.get_index_from_time_in_seconds(target_timestamp)
215
+
216
+ start_time = time.time()
217
+ for i in range(0, frame_count, 5):
218
+ batch = decoder.get_batch_frames(5)
219
+ batch_frames.extend(batch)
220
+ if len(batch_frames) >= frame_count:
221
+ break
222
+
223
+ end_time = time.time()
224
+ elapsed_time = end_time - start_time
225
+ session_init_time = decoder.get_session_init_time()/1000
226
+ elapsed_time -= session_init_time
227
+ return batch_frames[:frame_count], elapsed_time
228
+
229
+ with ThreadPoolExecutor(max_workers=self.worker_count) as executor:
230
+ futures = []
231
+ for i in range(self.worker_count):
232
+ futures.append(executor.submit(decode_worker, i, frame_count, target_timestamp))
233
+
234
+ for future in futures:
235
+ batch_frames, thread_time = future.result()
236
+ frames.extend(batch_frames)
237
+ thread_times.append(thread_time)
238
+
239
+ return frames, thread_times
240
+
241
+
242
+ def get_system_metadata() -> Dict[str, Any]:
243
+ """Get system metadata for benchmarking results"""
244
+ return {
245
+ "cpu_count": os.cpu_count(),
246
+ "system": platform.system(),
247
+ "machine": platform.machine(),
248
+ "python_version": str(platform.python_version()),
249
+ "cuda": torch.cuda.get_device_properties(0).name if torch.cuda.is_available() else "not available",
250
+ "cuda_available": torch.cuda.is_available()
251
+ }
252
+
253
+ def parse_ffmpeg_progress(line: str) -> Optional[Dict[str, str]]:
254
+ """Parse FFmpeg progress line and extract key information"""
255
+ if not line.strip():
256
+ return None
257
+
258
+ # FFmpeg progress format: key=value key=value ...
259
+ # We're interested in lines that contain progress data
260
+ progress_data = {}
261
+
262
+ # Handle both single line progress (frame=123) and multi-key progress lines
263
+ if '=' in line:
264
+ for item in line.split():
265
+ if '=' in item:
266
+ key, value = item.split('=', 1)
267
+ progress_data[key] = value
268
+
269
+ return progress_data if progress_data else None
270
+
271
+ def generate_mandelbrot_video(
272
+ width: int, height: int, duration: int, fps: int, gop: int, output_path: str,
273
+ ffmpeg_path: str = "ffmpeg"
274
+ ) -> str:
275
+ """
276
+ Generate a Mandelbrot set video using FFmpeg with progress tracking.
277
+
278
+ Parameters:
279
+ width (int): Video width
280
+ height (int): Video height
281
+ duration (int): Video duration in seconds
282
+ fps (int): Frames per second
283
+ gop (int): GOP size
284
+ output_path (str): Path to save the generated video
285
+ ffmpeg_path (str): Path to the ffmpeg executable (resolved from the required
286
+ --ffmpeg-dir as <dir>/bin/ffmpeg on Unix, <dir>/bin/ffmpeg.exe on Windows).
287
+
288
+ Returns:
289
+ str: Path to the generated video
290
+ """
291
+ print(f"Generating Mandelbrot set video at {width}x{height}, {duration}s duration, {fps} fps, {gop} GOP size")
292
+
293
+ # Create the directory if it doesn't exist
294
+ os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
295
+
296
+ # FFmpeg command: ffmpeg_path is the executable (from --ffmpeg-dir: bin/ffmpeg or bin/ffmpeg.exe)
297
+ cmd = [
298
+ ffmpeg_path, "-y",
299
+ "-progress", "pipe:2", # Output progress to stderr
300
+ "-v", "warning",
301
+ # Video input from mandelbrot filter
302
+ "-f", "lavfi",
303
+ "-i", f"mandelbrot=s={width}x{height}",
304
+ # Set duration
305
+ "-t", str(duration),
306
+ # Video encoding with h264 and specific parameters
307
+ "-c:v", "h264_nvenc",
308
+ "-preset", "medium",
309
+ "-b:v", "8M",
310
+ "-bf", "3", # 3 B-frames
311
+ "-g", str(gop), # GOP size
312
+ "-rc", "vbr", # Variable bitrate
313
+ "-profile:v", "high", # High profile for better quality
314
+ output_path
315
+ ]
316
+
317
+ # For custom FFmpeg builds, ensure the subprocess finds shared libs:
318
+ # - Unix: add <install_dir>/lib to LD_LIBRARY_PATH.
319
+ # - Windows: add <install_dir>/bin to PATH.
320
+ env = os.environ.copy()
321
+ if os.path.isfile(ffmpeg_path):
322
+ ffmpeg_dir = os.path.dirname(ffmpeg_path)
323
+ if platform.system() == "Windows":
324
+ # DLLs are typically in the same bin/ folder as ffmpeg.exe
325
+ env["PATH"] = ffmpeg_dir + os.pathsep + env.get("PATH", "")
326
+ else:
327
+ # Check for .../bin/ffmpeg layout and add sibling lib/ to LD_LIBRARY_PATH
328
+ if os.path.sep + "bin" + os.path.sep in ffmpeg_path:
329
+ install_dir = os.path.dirname(ffmpeg_dir)
330
+ lib_dir = os.path.join(install_dir, "lib")
331
+ if os.path.isdir(lib_dir):
332
+ env["LD_LIBRARY_PATH"] = lib_dir + (os.pathsep + env.get("LD_LIBRARY_PATH", ""))
333
+
334
+ try:
335
+ # Calculate estimated total frames for progress tracking
336
+ estimated_frames = duration * fps
337
+
338
+ print("Starting FFmpeg process to generate Mandelbrot video...")
339
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
340
+ universal_newlines=True, bufsize=1, env=env)
341
+
342
+ # Progress bar setup - single line that updates in place
343
+ pbar = tqdm(total=estimated_frames, unit='frames',
344
+ desc=f"Generating {os.path.basename(output_path)}",
345
+ ncols=80, leave=True, position=0)
346
+
347
+ # Read stderr line by line to get progress
348
+ current_frame = 0
349
+ max_frame_seen = 0
350
+ last_update_time = time.time()
351
+ update_interval = 0.5 # Update progress bar every 0.5 seconds maximum
352
+
353
+ while True:
354
+ line = process.stderr.readline()
355
+ if not line:
356
+ break
357
+
358
+ # Parse progress information
359
+ progress_data = parse_ffmpeg_progress(line.strip())
360
+ if progress_data and 'frame' in progress_data:
361
+ try:
362
+ frame_num = int(progress_data['frame'])
363
+ max_frame_seen = max(max_frame_seen, frame_num)
364
+
365
+ # Only update progress bar if enough time has passed or significant progress made
366
+ current_time = time.time()
367
+ if (current_time - last_update_time >= update_interval or
368
+ frame_num - current_frame >= 10):
369
+
370
+ # If we see more frames than expected, adjust the total
371
+ if frame_num > estimated_frames:
372
+ pbar.total = frame_num + 50 # Add buffer for final frames
373
+
374
+ if frame_num > current_frame:
375
+ # Update progress bar
376
+ pbar.update(frame_num - current_frame)
377
+ current_frame = frame_num
378
+ last_update_time = current_time
379
+
380
+ except (ValueError, KeyError):
381
+ continue
382
+
383
+ # Wait for process to complete
384
+ process.wait()
385
+
386
+ # Ensure progress bar reaches 100%
387
+ if max_frame_seen > current_frame:
388
+ pbar.update(max_frame_seen - current_frame)
389
+ current_frame = max_frame_seen
390
+
391
+ # If we still haven't reached the total, complete it
392
+ if current_frame < pbar.total:
393
+ pbar.update(pbar.total - current_frame)
394
+
395
+ pbar.close()
396
+
397
+ if process.returncode != 0:
398
+ # Read any remaining stderr
399
+ remaining_stderr = process.stderr.read()
400
+ print(f"Error generating video: {remaining_stderr}")
401
+ raise subprocess.CalledProcessError(process.returncode, cmd, remaining_stderr)
402
+
403
+ # Use regular print since progress bar is closed
404
+ print(f"✓ Video generation complete: {os.path.basename(output_path)}")
405
+ print(f" Video codec: h264_nvenc with {gop} GOP size and 3 B-frames")
406
+ print(f" Frames encoded: {max_frame_seen}")
407
+ return output_path
408
+
409
+ except subprocess.CalledProcessError as e:
410
+ print(f"FFmpeg error: {e}")
411
+ raise
412
+ except Exception as e:
413
+ print(f"Error: {e}")
414
+ raise
415
+
416
+
417
+ def generate_benchmark_videos(
418
+ resolution: str,
419
+ duration: int,
420
+ fps: int,
421
+ gop_sizes: List[int],
422
+ output_dir: str,
423
+ ffmpeg_path: str = "ffmpeg",
424
+ ) -> List[str]:
425
+ """
426
+ Generate benchmark videos with different GOP sizes using FFmpeg.
427
+
428
+ Parameters:
429
+ resolution (str): Video resolution (e.g., '1920x1080')
430
+ duration (int): Video duration in seconds
431
+ fps (int): Frames per second
432
+ gop_sizes (List[int]): List of GOP sizes to test
433
+ output_dir (str): Directory to save the generated videos
434
+ ffmpeg_path (str): Path to the ffmpeg executable (resolved from the required
435
+ --ffmpeg-dir as <dir>/bin/ffmpeg on Unix, <dir>/bin/ffmpeg.exe on Windows).
436
+
437
+ Returns:
438
+ List[str]: List of paths to the generated videos
439
+ """
440
+ width, height = map(int, resolution.split('x'))
441
+ generated_videos = []
442
+
443
+ print(f"\nGenerating {len(gop_sizes)} benchmark videos...")
444
+
445
+ # Create a clean progress bar for overall video generation
446
+ with tqdm(total=len(gop_sizes), desc="Overall Progress",
447
+ ncols=60, leave=True, position=1) as overall_pbar:
448
+ for gop in gop_sizes:
449
+ output_path = f"{output_dir}/mandelbrot_{resolution}_{duration}s_{fps}fps_{gop}gop.mp4"
450
+
451
+ # Skip if video already exists
452
+ if os.path.exists(output_path):
453
+ overall_pbar.write(f"⏭️ Skipping {os.path.basename(output_path)} - already exists")
454
+ generated_videos.append(output_path)
455
+ overall_pbar.update(1)
456
+ continue
457
+
458
+ try:
459
+ # Use the ffmpeg executable (resolved from --ffmpeg-dir or system) for video generation
460
+ video_path = generate_mandelbrot_video(
461
+ width, height, duration, fps, gop, output_path, ffmpeg_path=ffmpeg_path
462
+ )
463
+ generated_videos.append(video_path)
464
+ overall_pbar.update(1)
465
+ except Exception as e:
466
+ overall_pbar.write(f"❌ Failed to generate video with GOP {gop}: {e}")
467
+ overall_pbar.update(1)
468
+ continue
469
+
470
+ print(f"✅ Generated {len(generated_videos)} videos successfully!")
471
+ return generated_videos
472
+
473
+
474
+ def get_video_metadata(video_file: str) -> Dict[str, Any]:
475
+ """Get metadata for a video file using PyNvVideoCodec"""
476
+ decoder = nvc.SimpleDecoder(video_file)
477
+ metadata = decoder.get_stream_metadata()
478
+ return {
479
+ "duration": metadata.duration,
480
+ "width": metadata.width,
481
+ "height": metadata.height,
482
+ "fps": metadata.average_fps,
483
+ "codec": metadata.codec_name
484
+ }
485
+
486
+
487
+ def create_timestamp_list(video_file: str, num_samples: int, kind: str = "uniform") -> List[float]:
488
+ """Generate list of timestamps for frame sampling"""
489
+ metadata = get_video_metadata(video_file)
490
+ duration = metadata["duration"]
491
+
492
+ if kind == "uniform":
493
+ return [i * duration / num_samples for i in range(num_samples)]
494
+ elif kind == "random":
495
+ # Generate unique random indices and scale to duration
496
+ indices = torch.randperm(int(duration * 1000))[:num_samples] # Convert to milliseconds for more precision
497
+ return (indices.float() / 1000).tolist() # Convert back to seconds
498
+ else:
499
+ raise ValueError(f"Unknown sampling kind: {kind}")
500
+
501
+
502
+ def run_benchmark_test(
503
+ decoder: VideoFrameDecoder,
504
+ video_file: str,
505
+ num_threads: int,
506
+ num_samples: int,
507
+ decode_pattern: str = "first_n_frames"
508
+ ) -> DecoderPerformance:
509
+ """Run a single benchmark test for a decoder"""
510
+ description = ""
511
+ sequential_time = None
512
+ efficiency = None
513
+
514
+ if decode_pattern == "first_n_frames":
515
+ description = decoder.get_benchmark_description(num_samples, "sequential")
516
+ frames, thread_times = decoder.get_sequential_frames(video_file, frame_count=num_samples)
517
+ pattern_str = f"first_{num_samples}_frames"
518
+ else: # uniform or random
519
+ description = decoder.get_benchmark_description(num_samples, decode_pattern)
520
+ timestamp_list = create_timestamp_list(video_file, num_samples, decode_pattern)
521
+
522
+ # First measure sequential decode time to the last timestamp
523
+ last_timestamp = max(timestamp_list)
524
+ _, seq_thread_times = decoder.get_sequential_frames(video_file, frame_count=None, target_timestamp=last_timestamp)
525
+ sequential_time = sum(seq_thread_times)
526
+
527
+ # Then measure random/uniform decode time
528
+ frames, thread_times = decoder.get_frames_at_timestamps(video_file, timestamp_list)
529
+ efficiency = sequential_time / sum(thread_times) if sum(thread_times) > 0 else 0
530
+ pattern_str = f"{num_samples}_{decode_pattern}_frames"
531
+
532
+ total_frames = len(frames)
533
+ thread_fps = [num_samples / t for t in thread_times]
534
+ total_fps = sum(thread_fps)
535
+
536
+ return DecoderPerformance(
537
+ decoder_name=decoder.__class__.__name__,
538
+ video_file=video_file,
539
+ frames_decoded=total_frames,
540
+ thread_execution_times=thread_times,
541
+ overall_fps=total_fps,
542
+ per_thread_fps=thread_fps,
543
+ thread_count=num_threads,
544
+ test_description=description,
545
+ sampling_method=pattern_str,
546
+ sequential_decode_time=sequential_time,
547
+ sampling_efficiency=efficiency
548
+ )
549
+
550
+
551
+ def display_benchmark_results(results: List[DecoderPerformance], verbose: bool = False) -> None:
552
+ """Display benchmark results in formatted tables"""
553
+
554
+ videos = sorted(set(r.video_file for r in results))
555
+ patterns = sorted(set(r.sampling_method for r in results))
556
+ thread_counts = sorted(set(r.thread_count for r in results))
557
+
558
+ print("\nBenchmark Results:")
559
+
560
+ table_data = []
561
+ headers = ["Video Config", "Pattern"]
562
+
563
+ for thread_count in thread_counts:
564
+ headers.extend([f"T{thread_count} FPS", f"T{thread_count} Eff"])
565
+
566
+ for video in videos:
567
+ # Get the filename without path and extension
568
+ filename = os.path.basename(video)
569
+ # Split by underscore and remove .mp4 from the last part
570
+ parts = filename.split('_')
571
+ resolution = parts[1]
572
+ duration = parts[2].replace('s', '')
573
+ gop = parts[4].replace('gop.mp4', '')
574
+ config_str = f"{resolution} {gop}gop {duration}s"
575
+
576
+ for pattern in patterns:
577
+ row = [config_str, pattern]
578
+
579
+ for thread_count in thread_counts:
580
+ result = next((r for r in results
581
+ if r.video_file == video
582
+ and r.sampling_method == pattern
583
+ and r.thread_count == thread_count), None)
584
+ if result:
585
+ row.append(f"{result.overall_fps:.1f}")
586
+ if result.sampling_efficiency is not None:
587
+ row.append(f"{result.sampling_efficiency:.2f}x")
588
+ else:
589
+ row.append("N/A")
590
+ else:
591
+ row.append("N/A")
592
+ row.append("N/A")
593
+
594
+ table_data.append(row)
595
+
596
+ print(tabulate(table_data, headers=headers, tablefmt="grid"))
597
+
598
+ # Only show thread-level details if verbose mode is enabled
599
+ if verbose:
600
+ print(f"\nThread Details:")
601
+
602
+ thread_headers = ["Config", "Threads", "FPS per Thread"]
603
+ thread_data = []
604
+
605
+ for video in videos:
606
+ # Get the filename without path and extension
607
+ filename = os.path.basename(video)
608
+ # Split by underscore and remove .mp4 from the last part
609
+ parts = filename.split('_')
610
+ resolution = parts[1]
611
+ duration = parts[2].replace('s', '')
612
+ gop = parts[4].replace('gop.mp4', '')
613
+ config_str = f"{resolution} {gop}gop {duration}s"
614
+
615
+ for pattern in patterns:
616
+ for thread_count in thread_counts:
617
+ result = next((r for r in results
618
+ if r.video_file == video
619
+ and r.sampling_method == pattern
620
+ and r.thread_count == thread_count), None)
621
+
622
+ if result:
623
+ thread_fps_str = ",".join([f"{fps:.1f}" for fps in result.per_thread_fps])
624
+ thread_data.append([
625
+ f"{config_str} {pattern}",
626
+ thread_count,
627
+ thread_fps_str
628
+ ])
629
+
630
+ # Print thread-level details table
631
+ print(tabulate(thread_data, headers=thread_headers, tablefmt="grid"))
632
+
633
+ print("\nEfficiency Explanation:")
634
+ print("Efficiency (Eff) represents the performance comparison between two approaches:")
635
+ print("1. Direct sampling: Decoding specific frames directly using seek operations")
636
+ print("2. Sequential decode + sampling: Decoding all frames sequentially and then sampling the required frames")
637
+ print("The efficiency value shows how much faster direct sampling is compared to sequential decoding with sampling.")
638
+ print("Higher efficiency values indicate better performance of direct sampling approach.")
639
+
640
+
641
+ def save_benchmark_results(results: List[DecoderPerformance], output_file: str = "benchmark_results.json") -> None:
642
+ """Save benchmark results to a JSON file"""
643
+ output_data = {
644
+ "system_info": get_system_metadata(),
645
+ "benchmark_results": [
646
+ {
647
+ "decoder": r.decoder_name,
648
+ "video": r.video_file,
649
+ "frames_decoded": r.frames_decoded,
650
+ "thread_execution_times": r.thread_execution_times,
651
+ "overall_fps": r.overall_fps,
652
+ "per_thread_fps": r.per_thread_fps,
653
+ "thread_count": r.thread_count,
654
+ "test_description": r.test_description,
655
+ "sampling_method": r.sampling_method,
656
+ "sequential_decode_time": r.sequential_decode_time,
657
+ "sampling_efficiency": r.sampling_efficiency
658
+ }
659
+ for r in results
660
+ ]
661
+ }
662
+
663
+ with open(output_file, "w") as f:
664
+ json.dump(output_data, f, indent=4)
665
+
666
+ print(f"\nBenchmark results saved to {output_file}")
667
+
668
+
669
+ def main():
670
+ # Parse command line arguments
671
+ parser = argparse.ArgumentParser(description='Frame Sampling Benchmarking Script')
672
+ parser.add_argument('--nvdecs', type=int, required=True,
673
+ help='Number of NVDecs available on the system')
674
+ parser.add_argument('--ffmpeg-dir', type=str, required=True,
675
+ help='Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).')
676
+ parser.add_argument('--verbose', '-v', action='store_true',
677
+ help='Show detailed thread-level performance information (default: False)')
678
+ parser.add_argument('--resolution', '-res', type=str, default='1920x1080',
679
+ help='Video resolution (default: 1920x1080)')
680
+ parser.add_argument('--gop', '-g', type=int, nargs='+', default=[30, 250],
681
+ help='GOP sizes to test (space-separated list) (default: 30 250)')
682
+ parser.add_argument('--duration', '-d', type=int, default=30,
683
+ help='Video duration in seconds (default: 30)')
684
+ parser.add_argument('--fps', '-f', type=int, default=30,
685
+ help='Video frames per second (default: 30)')
686
+ parser.add_argument('--num-seq-frames', '-seq', type=int, default=100,
687
+ help='Number of frames to decode sequentially (default: 100)')
688
+ parser.add_argument('--num-samp-frames', '-samp', type=int, default=30,
689
+ help='Number of frames to sample for uniform/random patterns (default: 30)')
690
+ args = parser.parse_args()
691
+
692
+ # Resolve FFmpeg executable from the required installation directory
693
+ bin_dir = os.path.join(args.ffmpeg_dir, "bin")
694
+ # On Windows the executable is ffmpeg.exe; on Unix it is ffmpeg
695
+ if platform.system() == "Windows":
696
+ ffmpeg_exe = os.path.join(bin_dir, "ffmpeg.exe")
697
+ if not os.path.isfile(ffmpeg_exe):
698
+ ffmpeg_exe = os.path.join(bin_dir, "ffmpeg")
699
+ else:
700
+ ffmpeg_exe = os.path.join(bin_dir, "ffmpeg")
701
+ if not os.path.isfile(ffmpeg_exe):
702
+ expected = "bin/ffmpeg.exe or bin/ffmpeg" if platform.system() == "Windows" else "bin/ffmpeg"
703
+ raise FileNotFoundError(
704
+ f"FFmpeg executable not found at {ffmpeg_exe}. "
705
+ f"The given FFmpeg installation directory must contain {expected}."
706
+ )
707
+
708
+ # Set thread counts based on number of NVDecs
709
+ thread_counts = [1, args.nvdecs] if args.nvdecs > 1 else [1]
710
+ print(f"Using thread counts: {thread_counts} based on {args.nvdecs} NVDecs")
711
+
712
+ start_time = time.time()
713
+
714
+ # Create videos directory in the same location as the script
715
+ script_dir = Path(__file__).parent
716
+ videos_dir_path = script_dir / "benchmark_videos"
717
+ videos_dir_path.mkdir(parents=True, exist_ok=True)
718
+ print(f"Using videos directory: {videos_dir_path}")
719
+
720
+ # Generate benchmark videos using FFmpeg from the required --ffmpeg-dir
721
+ video_files = generate_benchmark_videos(
722
+ args.resolution,
723
+ args.duration,
724
+ args.fps,
725
+ args.gop,
726
+ str(videos_dir_path),
727
+ ffmpeg_path=ffmpeg_exe,
728
+ )
729
+
730
+ # Benchmark parameters
731
+ decode_patterns = ["first_n_frames", "uniform", "random"]
732
+
733
+ results = []
734
+
735
+ # Run benchmarks
736
+ for video_file in video_files:
737
+ if not os.path.exists(video_file):
738
+ print(f"Skipping {video_file} - file not found")
739
+ continue
740
+
741
+ for num_threads in thread_counts:
742
+ print(f"\nBenchmarking {video_file} with {num_threads} threads")
743
+
744
+ # PyNvVideoCodec benchmark
745
+ pynv_decoder = PyNvVideoCodecDecoder(num_threads)
746
+ for pattern in decode_patterns:
747
+ num_samples = args.num_seq_frames if pattern == "first_n_frames" else args.num_samp_frames
748
+ pynv_result = run_benchmark_test(pynv_decoder, video_file, num_threads, num_samples, pattern)
749
+ results.append(pynv_result)
750
+
751
+ # Display and save results
752
+ display_benchmark_results(results, verbose=args.verbose)
753
+ save_benchmark_results(results)
754
+
755
+ # Display total execution time
756
+ end_time = time.time()
757
+ total_time = end_time - start_time
758
+ hours = int(total_time // 3600)
759
+ minutes = int((total_time % 3600) // 60)
760
+ seconds = total_time % 60
761
+ print(f"\nTotal execution time: {hours:02d}:{minutes:02d}:{seconds:06.3f} ({total_time:.3f} seconds)")
762
+
763
+
764
+ if __name__ == "__main__":
765
+ main()