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,813 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a
5
+ # copy of this software and associated documentation files (the "Software"),
6
+ # to deal in the Software without restriction, including without limitation
7
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
8
+ # and/or sell copies of the Software, and to permit persons to whom the
9
+ # Software is furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
17
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20
+ # DEALINGS IN THE SOFTWARE.
21
+
22
+ from fileinput import filename
23
+ import argparse
24
+ from pathlib import Path
25
+ import os
26
+ import subprocess
27
+ import time
28
+ import random
29
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
30
+ from concurrent.futures import wait
31
+ import pycuda.autoinit
32
+ import pycuda.driver as cuda
33
+ import PyNvVideoCodec as nvc
34
+ from itertools import product
35
+ import platform
36
+ from typing import List, Dict, Any, Optional
37
+ from tabulate import tabulate
38
+ import json
39
+ from datetime import datetime
40
+ from dataclasses import dataclass, asdict
41
+ import threading
42
+ import matplotlib.pyplot as plt
43
+ import numpy as np
44
+ import glob
45
+ from tqdm import tqdm
46
+
47
+ """
48
+ Cached Decoder Benchmark
49
+
50
+ This script measures and compares the performance of NVIDIA's video decoder in two modes:
51
+ 1. Simple Decoder: Creates a new decoder instance for each video
52
+ 2. Cached Decoder: Reuses the same decoder instance across multiple videos
53
+
54
+ The benchmark generates test videos in different resolutions (360p, 480p, 720p, 1080p, 4K) and durations (2s and 30s),
55
+ then measures decoding performance in terms of FPS (Frames Per Second) and total frames decoded.
56
+
57
+ Features:
58
+ - Supports multiple video codecs (H.264, HEVC, AV1)
59
+ - Tests different thread counts (N NVDECs)
60
+ - Generates performance plots comparing simple vs cached decoder
61
+ - Saves results in both JSON and CSV formats
62
+ - Creates bar graphs for visual performance comparison
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
+
66
+ NOTE 2: The clips are stored in the benchmark_videos directory. The script generates 1 video clip per resolution per duration type and reuse the same clip 500 times.
67
+
68
+ NOTE 3: The script will run the decoders in multiple threads. Number of threads is determined by the --nvdecs argument.
69
+ 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.
70
+ Use this link to check the number of NVDecs available on the system: https://developer.nvidia.com/video-encode-decode-support-matrix
71
+
72
+ NOTE 4: Since the script benchmarks decoding performance on 500 videos per resolution per duration type, it takes some time to run.
73
+
74
+ NOTE 5: If the progress bar is affecting the performance measurements, you can disable it by passing --no-progress argument.
75
+
76
+ Required Arguments:
77
+ --nvdecs: Number of NVDECs to use
78
+ --ffmpeg-dir: Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).
79
+
80
+ Command Line Arguments:
81
+ --plot-only: Only generate plots from existing JSON files (default: False)
82
+ --generate-videos-only: Only generate test videos; do not run the benchmark (default: False)
83
+ --codec: Video codec to use (h264, hevc, or av1) (default: h264)
84
+ --fps: Frame rate for video generation (default: 30)
85
+ --gop: GOP size for video generation (default: 60)
86
+ --no-progress: Disable progress bars during decoding (default: False)
87
+
88
+ Usage Examples:
89
+ 1. Run full benchmark with default settings (H.264):
90
+ python cached_decoder_benchmark.py --nvdecs 3 --ffmpeg-dir /path/to/ffmpeg (for 3 threads)
91
+
92
+ 2. Run benchmark with HEVC codec and 4 threads:
93
+ python cached_decoder_benchmark.py --nvdecs 4 --codec hevc
94
+
95
+ 3. Generate plots from existing results:
96
+ python cached_decoder_benchmark.py --nvdecs 3 --plot-only
97
+
98
+ 4. Only generate test videos (no benchmark):
99
+ python cached_decoder_benchmark.py --nvdecs 3 --ffmpeg-dir /path/to/ffmpeg --generate-videos-only
100
+
101
+ 5. Run benchmark with custom video settings:
102
+ python cached_decoder_benchmark.py --nvdecs 2 --codec av1 --fps 60 --gop 120
103
+
104
+ Output Files:
105
+ - JSON results: cached_decoder_performance_{codec}_{threads}_threads.json
106
+ - Performance plots: cached_decoder_performance_{codec}_{threads}_threads.png
107
+ - Test videos: test_videos_{codec}/short_videos/ and test_videos_{codec}/long_videos/
108
+
109
+
110
+ """
111
+
112
+ @dataclass
113
+ class DecoderResult:
114
+ time_taken: float
115
+ fps: float
116
+ frames: int
117
+
118
+ @dataclass
119
+ class ResolutionResult:
120
+ resolution: str
121
+ new_decoder: DecoderResult
122
+ cached_decoder: DecoderResult
123
+
124
+ def to_dict(self):
125
+ return asdict(self)
126
+
127
+ @dataclass
128
+ class DurationResult:
129
+ duration_type: str
130
+ resolution_results: List[ResolutionResult]
131
+
132
+ def to_dict(self):
133
+ return {
134
+ "duration_type": self.duration_type,
135
+ "resolution_results": [res.to_dict() for res in self.resolution_results]
136
+ }
137
+
138
+ def parse_ffmpeg_progress(line: str) -> Optional[Dict[str, str]]:
139
+ """Parse FFmpeg progress line and extract key information"""
140
+ if not line.strip():
141
+ return None
142
+
143
+ progress_data = {}
144
+
145
+ if '=' in line:
146
+ for item in line.split():
147
+ if '=' in item:
148
+ key, value = item.split('=', 1)
149
+ progress_data[key] = value
150
+
151
+ return progress_data if progress_data else None
152
+
153
+ def create_test_videos(
154
+ video_configs: List[Dict[str, Any]],
155
+ encoding: str,
156
+ pattern: str,
157
+ fps: int,
158
+ pix_fmt: str,
159
+ ffmpeg_path: str,
160
+ base_output_dir: str,
161
+ duration_type: str,
162
+ resolution: str,
163
+ target_count: int = 1,
164
+ cq: int = 25
165
+ ) -> bool:
166
+ """Create test videos with different configurations using ffmpeg with progress tracking.
167
+
168
+ Returns:
169
+ True if all requested videos were created (or already existed), False if any FFmpeg run failed.
170
+
171
+ Args:
172
+ video_configs: List of video configuration dictionaries
173
+ encoding: Video encoding to use (e.g., 'h264_nvenc')
174
+ pattern: Test pattern to use (e.g., 'mandelbrot')
175
+ fps: Frames per second
176
+ pix_fmt: Pixel format (e.g., 'yuv420p')
177
+ ffmpeg_path: Path to the ffmpeg executable (from --ffmpeg-dir: bin/ffmpeg or bin/ffmpeg.exe).
178
+ base_output_dir: Base directory for output
179
+ duration_type: Type of duration ('short' or 'long')
180
+ resolution: Resolution name (e.g., '360p')
181
+ target_count: Number of videos to generate per resolution (default: 1)
182
+ cq: Constant Quality value
183
+ """
184
+ # Create resolution-specific output directory
185
+ output_dir = os.path.join(base_output_dir, resolution)
186
+ if not os.path.exists(output_dir):
187
+ os.makedirs(output_dir)
188
+
189
+ # Count existing videos
190
+ existing_videos = []
191
+ if os.path.exists(output_dir):
192
+ for file in os.listdir(output_dir):
193
+ if file.lower().endswith(".mp4"):
194
+ existing_videos.append(file)
195
+
196
+ existing_count = len(existing_videos)
197
+ videos_needed = target_count - existing_count
198
+
199
+ if videos_needed <= 0:
200
+ print(f"Already have {existing_count} video(s) for {resolution} resolution in {duration_type} duration folder")
201
+ return True
202
+
203
+ print(f"Generating {videos_needed} video(s) for {resolution} resolution in {duration_type} duration folder")
204
+ videos_created = 0
205
+ encoding_failed = False
206
+
207
+ # Generate videos for this resolution with progress tracking
208
+ for config in video_configs:
209
+ if videos_created >= videos_needed:
210
+ break
211
+
212
+ # Add an index to the filename if generating multiple videos
213
+ index_suffix = f"_{videos_created + 1}" if target_count > 1 else ""
214
+ outfile = f"{output_dir}/{pattern}_{config['resolution']}_{config['duration']}s_{fps}fps_{config['gop']}gop_{encoding}_{pix_fmt}{index_suffix}.mp4"
215
+
216
+ # Calculate estimated frames for progress tracking
217
+ estimated_frames = config['duration'] * fps
218
+
219
+ ffmpeg_cmd = [
220
+ ffmpeg_path,
221
+ "-y",
222
+ "-progress", "pipe:2", # Output progress to stderr
223
+ "-f",
224
+ "lavfi",
225
+ "-i",
226
+ f"{pattern}=s={config['resolution']}",
227
+ "-t",
228
+ str(config['duration']),
229
+ "-c:v",
230
+ encoding,
231
+ "-r",
232
+ str(fps),
233
+ "-g",
234
+ str(config['gop']),
235
+ "-pix_fmt",
236
+ pix_fmt,
237
+ "-cq",
238
+ str(cq),
239
+ outfile,
240
+ ]
241
+
242
+ try:
243
+ # For custom FFmpeg builds, ensure the subprocess finds shared libs (Unix: LD_LIBRARY_PATH; Windows: PATH).
244
+ env = os.environ.copy()
245
+ if os.path.isfile(ffmpeg_path):
246
+ ffmpeg_dir = os.path.dirname(ffmpeg_path)
247
+ if platform.system() == "Windows":
248
+ env["PATH"] = ffmpeg_dir + os.pathsep + env.get("PATH", "")
249
+ else:
250
+ if os.path.sep + "bin" + os.path.sep in ffmpeg_path:
251
+ install_dir = os.path.dirname(ffmpeg_dir)
252
+ lib_dir = os.path.join(install_dir, "lib")
253
+ if os.path.isdir(lib_dir):
254
+ env["LD_LIBRARY_PATH"] = lib_dir + (os.pathsep + env.get("LD_LIBRARY_PATH", ""))
255
+ # Start FFmpeg process with progress tracking
256
+ process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
257
+ universal_newlines=True, bufsize=1, env=env)
258
+
259
+ # Progress bar setup - single line that updates in place
260
+ video_name = f"{pattern}_{config['resolution']}_{config['duration']}s_{fps}fps_{config['gop']}gop_{encoding}_{pix_fmt}{index_suffix}.mp4"
261
+ pbar = tqdm(total=estimated_frames, unit='frames',
262
+ desc=f"Creating {video_name}",
263
+ ncols=80, leave=True, position=0)
264
+
265
+ # Read stderr line by line to get progress
266
+ current_frame = 0
267
+ max_frame_seen = 0
268
+ last_update_time = time.time()
269
+ update_interval = 0.5 # Update progress bar every 0.5 seconds maximum
270
+
271
+ while True:
272
+ line = process.stderr.readline()
273
+ if not line:
274
+ break
275
+
276
+ # Parse progress information
277
+ progress_data = parse_ffmpeg_progress(line.strip())
278
+ if progress_data and 'frame' in progress_data:
279
+ try:
280
+ frame_num = int(progress_data['frame'])
281
+ max_frame_seen = max(max_frame_seen, frame_num)
282
+
283
+ # Only update progress bar if enough time has passed or significant progress made
284
+ current_time = time.time()
285
+ if (current_time - last_update_time >= update_interval or
286
+ frame_num - current_frame >= 10):
287
+
288
+ # If we see more frames than expected, adjust the total
289
+ if frame_num > estimated_frames:
290
+ pbar.total = frame_num + 50 # Add buffer for final frames
291
+
292
+ if frame_num > current_frame:
293
+ # Update progress bar
294
+ pbar.update(frame_num - current_frame)
295
+ current_frame = frame_num
296
+ last_update_time = current_time
297
+
298
+ except (ValueError, KeyError):
299
+ continue
300
+
301
+ # Wait for process to complete
302
+ process.wait()
303
+
304
+ # Ensure progress bar reaches 100%
305
+ if max_frame_seen > current_frame:
306
+ pbar.update(max_frame_seen - current_frame)
307
+ current_frame = max_frame_seen
308
+
309
+ # If we still haven't reached the total, complete it
310
+ if current_frame < pbar.total:
311
+ pbar.update(pbar.total - current_frame)
312
+
313
+ pbar.close()
314
+
315
+ if process.returncode != 0:
316
+ # Read any remaining stderr
317
+ remaining_stderr = process.stderr.read()
318
+ print(f"Error creating video: {remaining_stderr}")
319
+ raise subprocess.CalledProcessError(process.returncode, ffmpeg_cmd, remaining_stderr)
320
+
321
+ print(f"✓ Video creation complete: {os.path.basename(outfile)}")
322
+ print(f" Frames encoded: {max_frame_seen}")
323
+ videos_created += 1
324
+
325
+ except subprocess.CalledProcessError as e:
326
+ print(f"FFmpeg error: {e}")
327
+ encoding_failed = True
328
+ continue
329
+ except Exception as e:
330
+ print(f"Error: {e}")
331
+ encoding_failed = True
332
+ continue
333
+
334
+ print(f"Created {videos_created} new video(s) for {resolution} resolution in {duration_type} duration folder")
335
+ return not encoding_failed
336
+
337
+ def split_list(videos, n):
338
+ k, m = divmod(len(videos), n)
339
+ result = []
340
+ start = 0
341
+ for i in range(n):
342
+ end = start + k + (1 if i < m else 0)
343
+ result.append(videos[start:end])
344
+ start = end
345
+ return result
346
+
347
+ def decode_using_new_decoder(clips, progress_callback=None):
348
+ device_id = 0
349
+ cuda_device = cuda.Device(device_id)
350
+ cuda_ctx = cuda_device.retain_primary_context()
351
+ cuda_ctx.push()
352
+ cuda_stream = cuda.Stream()
353
+ total_frames = 0
354
+ for video_file_path in clips:
355
+ decoder = nvc.SimpleDecoder(video_file_path, cuda_context=cuda_ctx.handle, cuda_stream=cuda_stream.handle, bWaitForSessionWarmUp=True)
356
+ metadata = decoder.get_stream_metadata()
357
+ num_frames = metadata.num_frames
358
+ for i in range(num_frames):
359
+ frame = decoder[i]
360
+ total_frames += num_frames
361
+ if progress_callback:
362
+ progress_callback(1)
363
+ cuda_stream.synchronize()
364
+ cuda_ctx.pop()
365
+ return total_frames
366
+
367
+ def decode_using_cached_decoder(clips, progress_callback=None):
368
+ decoder = None
369
+ device_id = 0
370
+ cuda_device = cuda.Device(device_id)
371
+ cuda_ctx = cuda_device.retain_primary_context()
372
+ cuda_ctx.push()
373
+ cuda_stream = cuda.Stream()
374
+ total_frames = 0
375
+ for video_file_path in clips:
376
+ if decoder is None:
377
+ decoder = nvc.SimpleDecoder(video_file_path,
378
+ cuda_context=cuda_ctx.handle, cuda_stream=cuda_stream.handle, bWaitForSessionWarmUp=True, max_width=3840, max_height=2160)
379
+ else:
380
+ decoder.reconfigure_decoder(video_file_path)
381
+ metadata = decoder.get_stream_metadata()
382
+ num_frames = metadata.num_frames
383
+ for i in range(num_frames):
384
+ frame = decoder[i]
385
+ total_frames += num_frames
386
+ cuda_stream.synchronize()
387
+ if progress_callback:
388
+ progress_callback(1)
389
+
390
+ cuda_ctx.pop()
391
+ return total_frames
392
+
393
+ def run_thread(func, clips_chunks, N, desc="Decoding", show_progress=True):
394
+ threads = []
395
+ start_time = time.perf_counter()
396
+ total_frames = 0
397
+
398
+ # Use a list to store results from threads
399
+ results = [0] * N
400
+
401
+ # Calculate total videos across all chunks
402
+ total_videos = sum(len(chunk) for chunk in clips_chunks)
403
+
404
+ # Create a thread-safe progress bar (disabled if show_progress is False)
405
+ pbar = tqdm(total=total_videos, unit='videos', desc=desc, ncols=100, leave=True, disable=not show_progress)
406
+ pbar_lock = threading.Lock()
407
+
408
+ def progress_callback(n):
409
+ with pbar_lock:
410
+ pbar.update(n)
411
+
412
+ def thread_wrapper(func, clips, index, progress_cb):
413
+ results[index] = func(clips, progress_callback=progress_cb)
414
+
415
+ for m in range(0, N):
416
+ t = threading.Thread(target=thread_wrapper, args=(func, clips_chunks[m], m, progress_callback))
417
+ t.start()
418
+ threads.append(t)
419
+
420
+ for t in threads:
421
+ t.join()
422
+
423
+ pbar.close()
424
+
425
+ total_frames = sum(results)
426
+ elapsed_time = time.perf_counter() - start_time
427
+ return elapsed_time, total_frames
428
+
429
+ def decode_perf_results(clips_by_resolution: Dict[str, List[str]], N: int, duration_type: str, show_progress: bool = True) -> DurationResult:
430
+ resolution_results = []
431
+
432
+ duration_label = "2s" if duration_type == "short" else "30s"
433
+
434
+ for resolution, clips in clips_by_resolution.items():
435
+ clips_chunks = split_list(clips, N)
436
+
437
+ elapsed_time_case_2, frames_case_2 = run_thread(decode_using_cached_decoder,
438
+ clips_chunks, N,
439
+ desc=f"Cached Decoder [{resolution}, {duration_label}]",
440
+ show_progress=show_progress)
441
+
442
+ elapsed_time_case_1, frames_case_1 = run_thread(decode_using_new_decoder,
443
+ clips_chunks, N,
444
+ desc=f"Simple Decoder [{resolution}, {duration_label}]",
445
+ show_progress=show_progress)
446
+
447
+
448
+ fps_case_1 = round(frames_case_1 / elapsed_time_case_1)
449
+ fps_case_2 = round(frames_case_2 / elapsed_time_case_2)
450
+
451
+ res_result = ResolutionResult(
452
+ resolution=resolution,
453
+ new_decoder=DecoderResult(
454
+ time_taken=round(elapsed_time_case_1, 2),
455
+ fps=fps_case_1,
456
+ frames=frames_case_1
457
+ ),
458
+ cached_decoder=DecoderResult(
459
+ time_taken=round(elapsed_time_case_2, 2),
460
+ fps=fps_case_2,
461
+ frames=frames_case_2
462
+ )
463
+ )
464
+ resolution_results.append(res_result)
465
+
466
+ return DurationResult(duration_type=duration_type, resolution_results=resolution_results)
467
+
468
+ def display_results_table(all_results: List[DurationResult]):
469
+ headers = ["Duration Type", "Resolution", "Decoder Type", "Time Taken (s)", "FPS"]
470
+ table_data = []
471
+
472
+ duration_labels = {
473
+ "short": "Short (2 sec)",
474
+ "long": "Long (30 sec)"
475
+ }
476
+
477
+ for result in all_results:
478
+ duration_label = duration_labels.get(result.duration_type, result.duration_type)
479
+
480
+ # Add duration type header row
481
+ table_data.append([f"{duration_label} Videos"] + ["" for _ in range(len(headers)-1)])
482
+
483
+ print(f"\n{result.duration_type.capitalize()} duration frames decoded: {result.resolution_results[0].new_decoder.frames}\n")
484
+
485
+ # Sort resolution results by resolution (360p, 480p, etc.)
486
+ def resolution_key(res):
487
+ if res.resolution == '4k':
488
+ return 4000 # 4k is equivalent to 4000p
489
+ return int(res.resolution.replace("p", ""))
490
+
491
+ sorted_results = sorted(result.resolution_results, key=resolution_key)
492
+
493
+ for res_result in sorted_results:
494
+ # Add row for simple decoder
495
+ table_data.append([
496
+ "", # Empty duration type (already shown in header)
497
+ res_result.resolution,
498
+ "Simple",
499
+ res_result.new_decoder.time_taken,
500
+ int(res_result.new_decoder.fps), # Convert to integer
501
+ ])
502
+ # Add row for cached decoder
503
+ table_data.append([
504
+ "", # Empty duration type (already shown in header)
505
+ res_result.resolution,
506
+ "Cached",
507
+ res_result.cached_decoder.time_taken,
508
+ int(res_result.cached_decoder.fps), # Convert to integer
509
+ ])
510
+
511
+ # Add separator between duration types using the same style as header
512
+ if result.duration_type == "short":
513
+ table_data.append(["-" * 20, "-" * 12, "-" * 13, "-" * 16, "-" * 5, "-" * 15])
514
+
515
+ print("\nDecoder Performance Results:")
516
+ print(tabulate(table_data, headers=headers, tablefmt="simple", floatfmt=".2f"))
517
+
518
+ def collect_videos(base_dir, resolutions):
519
+ videos_by_resolution = {}
520
+ for res_name, _ in resolutions:
521
+ res_dir = os.path.join(base_dir, res_name)
522
+ if os.path.exists(res_dir):
523
+ videos = []
524
+ for file in os.listdir(res_dir):
525
+ if file.lower().endswith(".mp4"):
526
+ full_path = os.path.join(res_dir, file)
527
+ # Add the same video 500 times to the list
528
+ videos.extend([full_path] * 500)
529
+ if videos:
530
+ videos_by_resolution[res_name] = videos
531
+ return videos_by_resolution
532
+
533
+ def plot_decoder_performance():
534
+ """Create bar plots for decoder performance across resolutions for each thread count"""
535
+ # Read JSON files for the specific codec
536
+ results = {}
537
+ for filename in glob.glob(f"cached_decoder_performance_{args.codec}_*_threads.json"):
538
+ # Extract thread count from filename
539
+ thread_count = int(filename.split('_')[4])
540
+ with open(filename, 'r') as f:
541
+ results[thread_count] = json.load(f)
542
+
543
+ if not results:
544
+ print(f"No JSON files found for codec {args.codec}")
545
+ return
546
+
547
+ # Sort thread counts to ensure correct order
548
+ thread_counts = sorted(results.keys())
549
+
550
+ # Define resolution order for consistent plotting
551
+ resolution_order = ['360p', '480p', '720p', '1080p', '4k']
552
+
553
+ for thread_count in thread_counts:
554
+ # Create a figure for each thread count
555
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
556
+
557
+ # Extract data for this thread count
558
+ data = {
559
+ 'short': {'new': [], 'cached': [], 'resolutions': []},
560
+ 'long': {'new': [], 'cached': [], 'resolutions': []}
561
+ }
562
+
563
+ # Process each duration type
564
+ for duration_result in results[thread_count]:
565
+ duration_type = duration_result['duration_type']
566
+
567
+ # Sort results by resolution order
568
+ sorted_results = sorted(
569
+ duration_result['resolution_results'],
570
+ key=lambda x: resolution_order.index(x['resolution'])
571
+ )
572
+
573
+ for res_result in sorted_results:
574
+ data[duration_type]['resolutions'].append(res_result['resolution'])
575
+ data[duration_type]['new'].append(res_result['new_decoder']['fps'])
576
+ data[duration_type]['cached'].append(res_result['cached_decoder']['fps'])
577
+
578
+ # Plot for short duration videos
579
+ x = np.arange(len(data['short']['resolutions']))
580
+ width = 0.35
581
+
582
+ ax1.bar(x - width/2, data['short']['new'], width, label='Simple Decoder')
583
+ ax1.bar(x + width/2, data['short']['cached'], width, label='Cached Decoder')
584
+ ax1.set_title(f'Short Duration Videos (2s) - {thread_count} Thread(s) - {args.codec.upper()}')
585
+ ax1.set_xlabel('Resolution')
586
+ ax1.set_ylabel('FPS')
587
+ ax1.set_xticks(x)
588
+ ax1.set_xticklabels(data['short']['resolutions'])
589
+ ax1.grid(True, axis='y')
590
+ ax1.legend()
591
+
592
+ # Plot for long duration videos
593
+ x = np.arange(len(data['long']['resolutions']))
594
+ ax2.bar(x - width/2, data['long']['new'], width, label='Simple Decoder')
595
+ ax2.bar(x + width/2, data['long']['cached'], width, label='Cached Decoder')
596
+ ax2.set_title(f'Long Duration Videos (30s) - {thread_count} Thread(s) - {args.codec.upper()}')
597
+ ax2.set_xlabel('Resolution')
598
+ ax2.set_ylabel('FPS')
599
+ ax2.set_xticks(x)
600
+ ax2.set_xticklabels(data['long']['resolutions'])
601
+ ax2.grid(True, axis='y')
602
+ ax2.legend()
603
+
604
+ # Adjust layout and save
605
+ plt.tight_layout()
606
+ plt.savefig(f'cached_decoder_performance_{args.codec}_{thread_count}_threads.png')
607
+ plt.close()
608
+
609
+ print(f"Plot saved as 'cached_decoder_performance_{args.codec}_{thread_count}_threads.png'")
610
+
611
+ if __name__=="__main__":
612
+ script_start_time = time.perf_counter()
613
+
614
+ parser = argparse.ArgumentParser(
615
+ 'This sample application measures decoding performance in FPS.'
616
+ )
617
+ parser.add_argument(
618
+ "--plot-only", action="store_true", default=False,
619
+ help="only generate plots from existing JSON files (default: False)")
620
+ parser.add_argument(
621
+ "--codec", type=str, choices=['h264', 'hevc', 'av1'], default='h264',
622
+ help="video codec to use (h264, hevc, or av1). Default: h264")
623
+ parser.add_argument(
624
+ "--nvdecs", type=int, required=True, help="number of NVDECs to use")
625
+ parser.add_argument(
626
+ "--ffmpeg-dir", type=str, required=True,
627
+ help="Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).")
628
+ parser.add_argument(
629
+ "--fps", type=int, default=30, help="frame rate for video generation (default: 30)")
630
+ parser.add_argument(
631
+ "--gop", type=int, default=60, help="GOP size for video generation (default: 60)")
632
+ parser.add_argument(
633
+ "--no-progress", action="store_true", default=False,
634
+ help="disable progress bars during decoding (default: False)")
635
+ parser.add_argument(
636
+ "--generate-videos-only", action="store_true", default=False,
637
+ help="only generate test videos; do not run the benchmark (default: False)")
638
+
639
+ args = parser.parse_args()
640
+
641
+ if args.plot_only:
642
+ plot_decoder_performance()
643
+ exit(0)
644
+
645
+ # Define thread counts to test - only 1 and max threads
646
+ num_of_threads = [args.nvdecs] if args.nvdecs > 1 else [1]
647
+
648
+ # Define resolutions with their dimensions
649
+ resolutions = [
650
+ ("360p", "640x360"),
651
+ ("480p", "854x480"),
652
+ ("720p", "1280x720"),
653
+ ("1080p", "1920x1080"),
654
+ ("4k", "3840x2160")
655
+ ]
656
+
657
+ # Use the test videos directory in the same directory as the script
658
+ base_videos_dir = os.path.join(os.path.dirname(__file__), f"test_videos_{args.codec}")
659
+ short_videos_dir = os.path.join(base_videos_dir, "short_videos")
660
+ long_videos_dir = os.path.join(base_videos_dir, "long_videos")
661
+
662
+ # Create base directories if they don't exist
663
+ os.makedirs(short_videos_dir, exist_ok=True)
664
+ os.makedirs(long_videos_dir, exist_ok=True)
665
+
666
+ # Define video generation parameters
667
+ codec_map = {
668
+ 'h264': 'h264_nvenc',
669
+ 'hevc': 'hevc_nvenc',
670
+ 'av1': 'av1_nvenc'
671
+ }
672
+ encoding = codec_map[args.codec] # Use selected codec
673
+ pattern = "mandelbrot" # Test pattern
674
+ fps = args.fps # Frame rate from command line
675
+ pix_fmt = "yuv420p" # Standard pixel format
676
+ gop_size = args.gop # GOP size from command line
677
+ cq = 25 # Constant Quality value
678
+
679
+
680
+ # Resolve FFmpeg executable from the required installation directory
681
+ bin_dir = os.path.join(args.ffmpeg_dir, "bin")
682
+ if platform.system() == "Windows":
683
+ ffmpeg_path = os.path.join(bin_dir, "ffmpeg.exe")
684
+ if not os.path.isfile(ffmpeg_path):
685
+ ffmpeg_path = os.path.join(bin_dir, "ffmpeg")
686
+ else:
687
+ ffmpeg_path = os.path.join(bin_dir, "ffmpeg")
688
+ if not os.path.isfile(ffmpeg_path):
689
+ expected = "bin/ffmpeg.exe or bin/ffmpeg" if platform.system() == "Windows" else "bin/ffmpeg"
690
+ raise FileNotFoundError(
691
+ f"FFmpeg executable not found at {ffmpeg_path}. "
692
+ f"The given FFmpeg installation directory must contain {expected}."
693
+ )
694
+
695
+ # Generate videos for each resolution
696
+ av1_generation_failed = False
697
+ for res_name, res_dim in resolutions:
698
+ # Short duration videos
699
+ short_video_configs = [{
700
+ "resolution": res_dim,
701
+ "gop": gop_size,
702
+ "duration": 2 # Fixed 2 seconds
703
+ }]
704
+ ok_short = create_test_videos(
705
+ short_video_configs,
706
+ encoding,
707
+ pattern,
708
+ fps,
709
+ pix_fmt,
710
+ ffmpeg_path,
711
+ str(short_videos_dir),
712
+ "short",
713
+ res_name,
714
+ 1, # Always generate 1 video per resolution
715
+ cq
716
+ )
717
+
718
+ # Long duration videos
719
+ long_video_configs = [{
720
+ "resolution": res_dim,
721
+ "gop": gop_size,
722
+ "duration": 30 # Fixed 30 seconds
723
+ }]
724
+ ok_long = create_test_videos(
725
+ long_video_configs,
726
+ encoding,
727
+ pattern,
728
+ fps,
729
+ pix_fmt,
730
+ ffmpeg_path,
731
+ str(long_videos_dir),
732
+ "long",
733
+ res_name,
734
+ 1, # Always generate 1 video per resolution
735
+ cq
736
+ )
737
+
738
+ if args.codec == "av1" and (not ok_short or not ok_long):
739
+ av1_generation_failed = True
740
+
741
+ if av1_generation_failed and args.codec == "av1":
742
+ print("\nAV1 video generation failed (e.g. AV1 encoder not available or yuv420 not supported on this device, such as Jetson).")
743
+ print("Workaround: Generate videos on a machine with AV1 encoder support using --generate-videos-only,")
744
+ print("then copy the test_videos_av1 directory to this device and run the benchmark without that flag.")
745
+ exit(1)
746
+
747
+ if args.generate_videos_only:
748
+ print("\nVideos generated. Exiting (--generate-videos-only was set).")
749
+ exit(0)
750
+
751
+ # Collect videos from all resolution directories
752
+ short_clips = collect_videos(short_videos_dir, resolutions)
753
+ long_clips = collect_videos(long_videos_dir, resolutions)
754
+
755
+ all_results = []
756
+
757
+ # Run benchmark for each thread count
758
+ for thread_count in num_of_threads:
759
+ print(f"\nRunning benchmark with {thread_count} thread(s)")
760
+
761
+ if short_clips:
762
+ print(f"\nProcessing short duration videos:")
763
+ for res, clips in short_clips.items():
764
+ print(f" Generating stats for {res} short videos")
765
+ print(f" {res}: {len(clips)} video(s)")
766
+ short_results = decode_perf_results(short_clips, thread_count, "short", show_progress=not args.no_progress)
767
+ all_results.append(short_results)
768
+
769
+ if long_clips:
770
+ print(f"\nProcessing long duration videos:")
771
+ for res, clips in long_clips.items():
772
+ print(f" Generating stats for {res} long videos")
773
+ print(f" {res}: {len(clips)} video(s)")
774
+ long_results = decode_perf_results(long_clips, thread_count, "long", show_progress=not args.no_progress)
775
+ all_results.append(long_results)
776
+
777
+ # Print video configuration details
778
+ print("\nVideo Configuration:")
779
+ print(f"Pattern: {pattern}")
780
+ print(f"Frame Rate: {fps} fps")
781
+ print(f"GOP Size: {gop_size}")
782
+ print(f"Number of threads: {thread_count}")
783
+
784
+
785
+ if all_results:
786
+ # Display tabulated results
787
+ display_results_table(all_results)
788
+
789
+ # Save results to JSON file with codec and thread count in filename
790
+ filename = f"cached_decoder_performance_{args.codec}_{thread_count}_threads.json"
791
+
792
+ # Convert dataclass objects to dictionaries
793
+ results_dict = [result.to_dict() for result in all_results]
794
+
795
+ with open(filename, 'w') as f:
796
+ json.dump(results_dict, f, indent=4)
797
+
798
+ print(f"\nResults saved to {filename}")
799
+
800
+ # Clear results for next thread count
801
+ all_results = []
802
+ else:
803
+ print("No videos were found or generated. Please check the directory structure and video generation process.")
804
+
805
+ # After running the benchmark, generate plots
806
+ plot_decoder_performance()
807
+
808
+ # Display total execution time
809
+ total_time = time.perf_counter() - script_start_time
810
+ hours = int(total_time // 3600)
811
+ minutes = int((total_time % 3600) // 60)
812
+ seconds = total_time % 60
813
+ print(f"\nTotal execution time: {hours:02d}:{minutes:02d}:{seconds:06.3f} ({total_time:.3f} seconds)")