PyNvVideoCodec 2.0.4__cp314-cp314-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.cp314-win_amd64.pyd +0 -0
- PyNvVideoCodec/PyNvVideoCodec_130.cp314-win_amd64.pyd +0 -0
- PyNvVideoCodec/VersionCheck.cp314-win_amd64.pyd +0 -0
- PyNvVideoCodec/__init__.py +246 -0
- PyNvVideoCodec/avcodec-62.dll +0 -0
- PyNvVideoCodec/avdevice-62.dll +0 -0
- PyNvVideoCodec/avfilter-11.dll +0 -0
- PyNvVideoCodec/avformat-62.dll +0 -0
- PyNvVideoCodec/avutil-60.dll +0 -0
- PyNvVideoCodec/decoders/SimpleDecoder.py +230 -0
- PyNvVideoCodec/decoders/ThreadedDecoder.py +129 -0
- PyNvVideoCodec/decoders/__init__.py +22 -0
- PyNvVideoCodec/swresample-6.dll +0 -0
- PyNvVideoCodec/swscale-9.dll +0 -0
- PyNvVideoCodec/transcoder/Transcoder.py +55 -0
- PyNvVideoCodec/transcoder/__init__.py +22 -0
- benchmarks/cached_decoder_benchmark.py +813 -0
- benchmarks/frame_sampling_benchmark.py +765 -0
- benchmarks/requirements.txt +47 -0
- benchmarks/segmented_transcode_benchmark.py +1546 -0
- pynvvideocodec-2.0.4.data/data/external/ffmpeg/readme.txt +15 -0
- pynvvideocodec-2.0.4.data/data/external/ffmpeg/src/ffmpeg-8.1.1.tar.xz +0 -0
- pynvvideocodec-2.0.4.dist-info/METADATA +84 -0
- pynvvideocodec-2.0.4.dist-info/RECORD +65 -0
- pynvvideocodec-2.0.4.dist-info/WHEEL +5 -0
- pynvvideocodec-2.0.4.dist-info/licenses/LICENSE.txt +22 -0
- pynvvideocodec-2.0.4.dist-info/licenses/NOTICES.txt +167 -0
- pynvvideocodec-2.0.4.dist-info/top_level.txt +3 -0
- samples/advanced/decode.py +173 -0
- samples/advanced/decode_from_memory_buffer.py +204 -0
- samples/advanced/decode_perf.py +299 -0
- samples/advanced/decode_reconfigure.py +184 -0
- samples/advanced/decode_sei_msg.py +193 -0
- samples/advanced/decode_with_cuda_control.py +214 -0
- samples/advanced/decode_with_low_latency.py +183 -0
- samples/advanced/encode_config.json +9 -0
- samples/advanced/encode_config_lowlatency.json +11 -0
- samples/advanced/encode_perf.py +338 -0
- samples/advanced/encode_reconfigure.py +211 -0
- samples/advanced/encode_sei_msg.py +188 -0
- samples/advanced/simple_decode_stats.py +179 -0
- samples/advanced/transcode.py +288 -0
- samples/basic/create_video_segments.py +187 -0
- samples/basic/encode.py +347 -0
- samples/basic/encode_config.json +9 -0
- samples/basic/segments.txt +3 -0
- samples/basic/simple_decode_sampling.py +182 -0
- samples/basic/transcode_config.json +6 -0
- samples/jupyter/ReconfigureDecoder.png +0 -0
- samples/jupyter/SimpleDecoderAPIs.png +0 -0
- samples/jupyter/ThreadedDecoder.png +0 -0
- samples/jupyter/coco_classes.pickle +0 -0
- samples/jupyter/object_detection_tutorial.ipynb +762 -0
- samples/jupyter/simple_decode_tutorial.ipynb +651 -0
- samples/requirements.txt +78 -0
- samples/utils/Utils.py +405 -0
- samples/utils/__init__.py +36 -0
- samples/utils/decode_parser.py +642 -0
- samples/utils/decode_stats_writer.py +177 -0
- samples/utils/encode_parallel_utils.py +174 -0
- samples/utils/encode_parser.py +357 -0
- samples/utils/frame_utils.py +89 -0
- samples/utils/sei_message_parser.py +223 -0
- samples/utils/sei_parser.py +223 -0
- 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)")
|