PyNvVideoCodec 2.0.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyNvVideoCodec/PyNvVideoCodec_121.cp313-win_amd64.pyd +0 -0
- PyNvVideoCodec/PyNvVideoCodec_130.cp313-win_amd64.pyd +0 -0
- PyNvVideoCodec/VersionCheck.cp313-win_amd64.pyd +0 -0
- PyNvVideoCodec/__init__.py +237 -0
- PyNvVideoCodec/avcodec-58.dll +0 -0
- PyNvVideoCodec/avcodec-61.dll +0 -0
- PyNvVideoCodec/avcodec.dll +0 -0
- PyNvVideoCodec/avdevice-58.dll +0 -0
- PyNvVideoCodec/avdevice-61.dll +0 -0
- PyNvVideoCodec/avdevice.dll +0 -0
- PyNvVideoCodec/avfilter-10.dll +0 -0
- PyNvVideoCodec/avfilter-7.dll +0 -0
- PyNvVideoCodec/avfilter.dll +0 -0
- PyNvVideoCodec/avformat-58.dll +0 -0
- PyNvVideoCodec/avformat-61.dll +0 -0
- PyNvVideoCodec/avformat.dll +0 -0
- PyNvVideoCodec/avutil-56.dll +0 -0
- PyNvVideoCodec/avutil-59.dll +0 -0
- PyNvVideoCodec/avutil.dll +0 -0
- PyNvVideoCodec/decoders/SimpleDecoder.py +225 -0
- PyNvVideoCodec/decoders/ThreadedDecoder.py +119 -0
- PyNvVideoCodec/decoders/__init__.py +22 -0
- PyNvVideoCodec/swresample-3.dll +0 -0
- PyNvVideoCodec/swresample-5.dll +0 -0
- PyNvVideoCodec/swresample.dll +0 -0
- PyNvVideoCodec/swscale-5.dll +0 -0
- PyNvVideoCodec/swscale-8.dll +0 -0
- PyNvVideoCodec/swscale.dll +0 -0
- PyNvVideoCodec/transcoder/Transcoder.py +55 -0
- PyNvVideoCodec/transcoder/__init__.py +22 -0
- PyNvVideoCodec-2.0.0.data/data/external/ffmpeg/readme.txt +10 -0
- PyNvVideoCodec-2.0.0.data/data/external/ffmpeg/src/ffmpeg-7.0.2.tar.xz +0 -0
- PyNvVideoCodec-2.0.0.dist-info/METADATA +77 -0
- PyNvVideoCodec-2.0.0.dist-info/RECORD +64 -0
- PyNvVideoCodec-2.0.0.dist-info/WHEEL +5 -0
- PyNvVideoCodec-2.0.0.dist-info/licenses/LICENSE.txt +22 -0
- PyNvVideoCodec-2.0.0.dist-info/licenses/NOTICES.txt +167 -0
- PyNvVideoCodec-2.0.0.dist-info/top_level.txt +3 -0
- benchmarks/cached_decoder_benchmark.py +635 -0
- benchmarks/frame_sampling_benchmark.py +610 -0
- benchmarks/segmented_transcode_benchmark.py +1072 -0
- samples/Decode.py +194 -0
- samples/DecodeMultiprocessing.py +162 -0
- samples/DecodePerf.py +215 -0
- samples/DecodeSEIMsgExtraction.py +338 -0
- samples/DemuxFromByteArray.py +185 -0
- samples/Encode.py +189 -0
- samples/EncodeFromCPUBuffer.py +220 -0
- samples/EncodeMultiprocessing.py +268 -0
- samples/EncodePerf.py +380 -0
- samples/EncodeReconfigure.py +201 -0
- samples/EncodeSEIMsgInsertion.py +189 -0
- samples/ObjectDetection.py +218 -0
- samples/SimpleDecoder/EnsembleApp.py +416 -0
- samples/SimpleDecoder/SubsamplingAndReconfigure.py +341 -0
- samples/SimpleDecoder/fileLists.txt +4 -0
- samples/SimpleDecoder/segments.txt +3 -0
- samples/SimpleDecoder/timeListInSeconds.txt +5 -0
- samples/SimpleDecoder/transcode_config.json +6 -0
- samples/Utils.py +290 -0
- samples/coco_classes.pickle +0 -0
- samples/encode_config.json +16 -0
- samples/encode_config_lowlatency.json +11 -0
- samples/encode_config_perf.json +9 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 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
|
|
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
|
+
|
|
46
|
+
"""
|
|
47
|
+
Cached Decoder Benchmark
|
|
48
|
+
|
|
49
|
+
This script measures and compares the performance of NVIDIA's video decoder in two modes:
|
|
50
|
+
1. Simple Decoder: Creates a new decoder instance for each video
|
|
51
|
+
2. Cached Decoder: Reuses the same decoder instance across multiple videos
|
|
52
|
+
|
|
53
|
+
The benchmark generates test videos in different resolutions (360p to 4K) and durations (2s and 30s),
|
|
54
|
+
then measures decoding performance in terms of FPS (Frames Per Second) and total frames decoded.
|
|
55
|
+
|
|
56
|
+
Features:
|
|
57
|
+
- Supports multiple video codecs (H.264, HEVC, AV1)
|
|
58
|
+
- Tests different thread counts (1 and N NVDECs)
|
|
59
|
+
- Generates performance plots comparing simple vs cached decoder
|
|
60
|
+
- Saves results in both JSON and CSV formats
|
|
61
|
+
- Creates bar graphs for visual performance comparison
|
|
62
|
+
|
|
63
|
+
Command Line Arguments:
|
|
64
|
+
--plot-only: Only generate plots from existing JSON files (default: False)
|
|
65
|
+
--codec: Video codec to use (h264, hevc, or av1) (default: h264)
|
|
66
|
+
--nvdecs: Number of NVDECs to use (default: 3)
|
|
67
|
+
--fps: Frame rate for video generation (default: 30)
|
|
68
|
+
--gop: GOP size for video generation (default: 60)
|
|
69
|
+
|
|
70
|
+
Usage Examples:
|
|
71
|
+
1. Run full benchmark with default settings (H.264, 3 threads):
|
|
72
|
+
python simple_dec_reuse_benchmark.py
|
|
73
|
+
|
|
74
|
+
2. Run benchmark with HEVC codec and [1, 4] threads:
|
|
75
|
+
python simple_dec_reuse_benchmark.py --codec hevc --nvdecs 4
|
|
76
|
+
|
|
77
|
+
3. Generate plots from existing results:
|
|
78
|
+
python simple_dec_reuse_benchmark.py --plot-only
|
|
79
|
+
|
|
80
|
+
4. Run benchmark with custom video settings:
|
|
81
|
+
python simple_dec_reuse_benchmark.py --codec av1 --fps 60 --gop 120
|
|
82
|
+
|
|
83
|
+
Output Files:
|
|
84
|
+
- JSON results: cached_decoder_performance_{codec}_{threads}_threads.json
|
|
85
|
+
- Performance plots: cached_decoder_performance_{codec}_{threads}_threads.png
|
|
86
|
+
- Test videos: test_videos_{codec}/short_videos/ and test_videos_{codec}/long_videos/
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class DecoderResult:
|
|
92
|
+
time_taken: float
|
|
93
|
+
fps: float
|
|
94
|
+
frames: int
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ResolutionResult:
|
|
98
|
+
resolution: str
|
|
99
|
+
new_decoder: DecoderResult
|
|
100
|
+
cached_decoder: DecoderResult
|
|
101
|
+
|
|
102
|
+
def to_dict(self):
|
|
103
|
+
return asdict(self)
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class DurationResult:
|
|
107
|
+
duration_type: str
|
|
108
|
+
resolution_results: List[ResolutionResult]
|
|
109
|
+
|
|
110
|
+
def to_dict(self):
|
|
111
|
+
return {
|
|
112
|
+
"duration_type": self.duration_type,
|
|
113
|
+
"resolution_results": [res.to_dict() for res in self.resolution_results]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def create_test_videos(
|
|
117
|
+
video_configs: List[Dict[str, Any]],
|
|
118
|
+
encoding: str,
|
|
119
|
+
pattern: str,
|
|
120
|
+
fps: int,
|
|
121
|
+
pix_fmt: str,
|
|
122
|
+
ffmpeg_path: str,
|
|
123
|
+
base_output_dir: str,
|
|
124
|
+
duration_type: str,
|
|
125
|
+
resolution: str,
|
|
126
|
+
target_count: int = 1,
|
|
127
|
+
cq: int = 25
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Create test videos with different configurations using ffmpeg
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
video_configs: List of video configuration dictionaries
|
|
133
|
+
encoding: Video encoding to use (e.g., 'h264_nvenc')
|
|
134
|
+
pattern: Test pattern to use (e.g., 'mandelbrot')
|
|
135
|
+
fps: Frames per second
|
|
136
|
+
pix_fmt: Pixel format (e.g., 'yuv420p')
|
|
137
|
+
ffmpeg_path: Path to ffmpeg executable
|
|
138
|
+
base_output_dir: Base directory for output
|
|
139
|
+
duration_type: Type of duration ('short' or 'long')
|
|
140
|
+
resolution: Resolution name (e.g., '360p')
|
|
141
|
+
target_count: Number of videos to generate per resolution (default: 1)
|
|
142
|
+
cq: Constant Quality value
|
|
143
|
+
"""
|
|
144
|
+
# Create resolution-specific output directory
|
|
145
|
+
output_dir = os.path.join(base_output_dir, resolution)
|
|
146
|
+
if not os.path.exists(output_dir):
|
|
147
|
+
os.makedirs(output_dir)
|
|
148
|
+
|
|
149
|
+
# Count existing videos
|
|
150
|
+
existing_videos = []
|
|
151
|
+
if os.path.exists(output_dir):
|
|
152
|
+
for file in os.listdir(output_dir):
|
|
153
|
+
if file.lower().endswith(".mp4"):
|
|
154
|
+
existing_videos.append(file)
|
|
155
|
+
|
|
156
|
+
existing_count = len(existing_videos)
|
|
157
|
+
videos_needed = target_count - existing_count
|
|
158
|
+
|
|
159
|
+
if videos_needed <= 0:
|
|
160
|
+
print(f"Already have {existing_count} video(s) for {resolution} resolution in {duration_type} duration folder")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
print(f"Generating {videos_needed} video(s) for {resolution} resolution in {duration_type} duration folder")
|
|
164
|
+
videos_created = 0
|
|
165
|
+
|
|
166
|
+
# Generate videos for this resolution
|
|
167
|
+
for config in video_configs:
|
|
168
|
+
if videos_created >= videos_needed:
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
# Add an index to the filename if generating multiple videos
|
|
172
|
+
index_suffix = f"_{videos_created + 1}" if target_count > 1 else ""
|
|
173
|
+
outfile = f"{output_dir}/{pattern}_{config['resolution']}_{config['duration']}s_{fps}fps_{config['gop']}gop_{encoding}_{pix_fmt}{index_suffix}.mp4"
|
|
174
|
+
|
|
175
|
+
ffmpeg_cmd = [
|
|
176
|
+
ffmpeg_path,
|
|
177
|
+
"-y",
|
|
178
|
+
"-f",
|
|
179
|
+
"lavfi",
|
|
180
|
+
"-i",
|
|
181
|
+
f"{pattern}=s={config['resolution']}",
|
|
182
|
+
"-t",
|
|
183
|
+
str(config['duration']),
|
|
184
|
+
"-c:v",
|
|
185
|
+
encoding,
|
|
186
|
+
"-r",
|
|
187
|
+
str(fps),
|
|
188
|
+
"-g",
|
|
189
|
+
str(config['gop']),
|
|
190
|
+
"-pix_fmt",
|
|
191
|
+
pix_fmt,
|
|
192
|
+
"-cq",
|
|
193
|
+
str(cq),
|
|
194
|
+
outfile,
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
print(f"Creating video {videos_created + 1}/{videos_needed}: {' '.join(ffmpeg_cmd)}")
|
|
198
|
+
subprocess.check_call(ffmpeg_cmd)
|
|
199
|
+
videos_created += 1
|
|
200
|
+
|
|
201
|
+
print(f"Created {videos_created} new video(s) for {resolution} resolution in {duration_type} duration folder")
|
|
202
|
+
|
|
203
|
+
def split_list(videos, n):
|
|
204
|
+
k, m = divmod(len(videos), n)
|
|
205
|
+
result = []
|
|
206
|
+
start = 0
|
|
207
|
+
for i in range(n):
|
|
208
|
+
end = start + k + (1 if i < m else 0)
|
|
209
|
+
result.append(videos[start:end])
|
|
210
|
+
start = end
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
def decode_using_new_decoder(clips):
|
|
214
|
+
device_id = 0
|
|
215
|
+
cuda_device = cuda.Device(device_id)
|
|
216
|
+
cuda_ctx = cuda_device.retain_primary_context()
|
|
217
|
+
cuda_ctx.push()
|
|
218
|
+
cuda_stream = cuda.Stream()
|
|
219
|
+
total_frames = 0
|
|
220
|
+
for video_file_path in clips:
|
|
221
|
+
decoder = nvc.SimpleDecoder(video_file_path, cuda_context=cuda_ctx.handle, cuda_stream=cuda_stream.handle)
|
|
222
|
+
metadata = decoder.get_stream_metadata()
|
|
223
|
+
num_frames = metadata.num_frames
|
|
224
|
+
for i in range(num_frames):
|
|
225
|
+
frame = decoder[i]
|
|
226
|
+
total_frames += num_frames
|
|
227
|
+
cuda_stream.synchronize()
|
|
228
|
+
cuda_ctx.pop()
|
|
229
|
+
return total_frames
|
|
230
|
+
|
|
231
|
+
def decode_using_cached_decoder(clips):
|
|
232
|
+
decoder = None
|
|
233
|
+
device_id = 0
|
|
234
|
+
cuda_device = cuda.Device(device_id)
|
|
235
|
+
cuda_ctx = cuda_device.retain_primary_context()
|
|
236
|
+
cuda_ctx.push()
|
|
237
|
+
cuda_stream = cuda.Stream()
|
|
238
|
+
total_frames = 0
|
|
239
|
+
for video_file_path in clips:
|
|
240
|
+
if decoder is None:
|
|
241
|
+
decoder = nvc.SimpleDecoder(video_file_path,
|
|
242
|
+
cuda_context=cuda_ctx.handle, cuda_stream=cuda_stream.handle)
|
|
243
|
+
else:
|
|
244
|
+
decoder.reconfigure_decoder(video_file_path)
|
|
245
|
+
metadata = decoder.get_stream_metadata()
|
|
246
|
+
num_frames = metadata.num_frames
|
|
247
|
+
for i in range(num_frames):
|
|
248
|
+
frame = decoder[i]
|
|
249
|
+
total_frames += num_frames
|
|
250
|
+
cuda_stream.synchronize()
|
|
251
|
+
|
|
252
|
+
cuda_ctx.pop()
|
|
253
|
+
return total_frames
|
|
254
|
+
|
|
255
|
+
def run_thread(func, clips_chunks, N):
|
|
256
|
+
threads = []
|
|
257
|
+
start_time = time.perf_counter()
|
|
258
|
+
total_frames = 0
|
|
259
|
+
|
|
260
|
+
# Use a list to store results from threads
|
|
261
|
+
results = [0] * N
|
|
262
|
+
|
|
263
|
+
def thread_wrapper(func, clips, index):
|
|
264
|
+
results[index] = func(clips)
|
|
265
|
+
|
|
266
|
+
for m in range(0, N):
|
|
267
|
+
t = threading.Thread(target=thread_wrapper, args=(func, clips_chunks[m], m))
|
|
268
|
+
t.start()
|
|
269
|
+
threads.append(t)
|
|
270
|
+
|
|
271
|
+
for t in threads:
|
|
272
|
+
t.join()
|
|
273
|
+
|
|
274
|
+
total_frames = sum(results)
|
|
275
|
+
elapsed_time = time.perf_counter() - start_time
|
|
276
|
+
return elapsed_time, total_frames
|
|
277
|
+
|
|
278
|
+
def decode_perf_results(clips_by_resolution: Dict[str, List[str]], N: int, duration_type: str) -> DurationResult:
|
|
279
|
+
resolution_results = []
|
|
280
|
+
|
|
281
|
+
for resolution, clips in clips_by_resolution.items():
|
|
282
|
+
clips_chunks = split_list(clips, N)
|
|
283
|
+
|
|
284
|
+
elapsed_time_case_1, frames_case_1 = run_thread(decode_using_new_decoder,
|
|
285
|
+
clips_chunks, N)
|
|
286
|
+
|
|
287
|
+
elapsed_time_case_2, frames_case_2 = run_thread(decode_using_cached_decoder,
|
|
288
|
+
clips_chunks, N)
|
|
289
|
+
|
|
290
|
+
fps_case_1 = round(frames_case_1 / elapsed_time_case_1)
|
|
291
|
+
fps_case_2 = round(frames_case_2 / elapsed_time_case_2)
|
|
292
|
+
|
|
293
|
+
res_result = ResolutionResult(
|
|
294
|
+
resolution=resolution,
|
|
295
|
+
new_decoder=DecoderResult(
|
|
296
|
+
time_taken=round(elapsed_time_case_1, 2),
|
|
297
|
+
fps=fps_case_1,
|
|
298
|
+
frames=frames_case_1
|
|
299
|
+
),
|
|
300
|
+
cached_decoder=DecoderResult(
|
|
301
|
+
time_taken=round(elapsed_time_case_2, 2),
|
|
302
|
+
fps=fps_case_2,
|
|
303
|
+
frames=frames_case_2
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
resolution_results.append(res_result)
|
|
307
|
+
|
|
308
|
+
return DurationResult(duration_type=duration_type, resolution_results=resolution_results)
|
|
309
|
+
|
|
310
|
+
def save_results_to_json(all_results: List[DurationResult]):
|
|
311
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
312
|
+
filename = f"decoder_performance_results_{timestamp}.json"
|
|
313
|
+
|
|
314
|
+
# Convert dataclass objects to dictionaries
|
|
315
|
+
results_dict = [result.to_dict() for result in all_results]
|
|
316
|
+
|
|
317
|
+
with open(filename, 'w') as f:
|
|
318
|
+
json.dump(results_dict, f, indent=4)
|
|
319
|
+
|
|
320
|
+
print(f"\nResults saved to {filename}")
|
|
321
|
+
|
|
322
|
+
def display_results_table(all_results: List[DurationResult]):
|
|
323
|
+
headers = ["Duration Type", "Resolution", "Decoder Type", "Time Taken (s)", "FPS", "Frames Decoded"]
|
|
324
|
+
table_data = []
|
|
325
|
+
|
|
326
|
+
duration_labels = {
|
|
327
|
+
"short": "Short (2 sec)",
|
|
328
|
+
"long": "Long (30 sec)"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for result in all_results:
|
|
332
|
+
duration_label = duration_labels.get(result.duration_type, result.duration_type)
|
|
333
|
+
|
|
334
|
+
# Add duration type header row
|
|
335
|
+
table_data.append([f"{duration_label} Videos"] + ["" for _ in range(len(headers)-1)])
|
|
336
|
+
|
|
337
|
+
# Sort resolution results by resolution (360p, 480p, etc.)
|
|
338
|
+
def resolution_key(res):
|
|
339
|
+
if res.resolution == '4k':
|
|
340
|
+
return 4000 # 4k is equivalent to 4000p
|
|
341
|
+
return int(res.resolution.replace("p", ""))
|
|
342
|
+
|
|
343
|
+
sorted_results = sorted(result.resolution_results, key=resolution_key)
|
|
344
|
+
|
|
345
|
+
for res_result in sorted_results:
|
|
346
|
+
# Add row for simple decoder
|
|
347
|
+
table_data.append([
|
|
348
|
+
"", # Empty duration type (already shown in header)
|
|
349
|
+
res_result.resolution,
|
|
350
|
+
"Simple",
|
|
351
|
+
res_result.new_decoder.time_taken,
|
|
352
|
+
int(res_result.new_decoder.fps), # Convert to integer
|
|
353
|
+
res_result.new_decoder.frames
|
|
354
|
+
])
|
|
355
|
+
# Add row for cached decoder
|
|
356
|
+
table_data.append([
|
|
357
|
+
"", # Empty duration type (already shown in header)
|
|
358
|
+
res_result.resolution,
|
|
359
|
+
"Cached",
|
|
360
|
+
res_result.cached_decoder.time_taken,
|
|
361
|
+
int(res_result.cached_decoder.fps), # Convert to integer
|
|
362
|
+
res_result.cached_decoder.frames
|
|
363
|
+
])
|
|
364
|
+
|
|
365
|
+
# Add separator between duration types using the same style as header
|
|
366
|
+
if result.duration_type == "short":
|
|
367
|
+
table_data.append(["-" * 20, "-" * 12, "-" * 13, "-" * 16, "-" * 5, "-" * 15])
|
|
368
|
+
|
|
369
|
+
print("\nDecoder Performance Results:")
|
|
370
|
+
print(tabulate(table_data, headers=headers, tablefmt="simple", floatfmt=".2f"))
|
|
371
|
+
|
|
372
|
+
def collect_videos(base_dir, resolutions):
|
|
373
|
+
videos_by_resolution = {}
|
|
374
|
+
for res_name, _ in resolutions:
|
|
375
|
+
res_dir = os.path.join(base_dir, res_name)
|
|
376
|
+
if os.path.exists(res_dir):
|
|
377
|
+
videos = []
|
|
378
|
+
for file in os.listdir(res_dir):
|
|
379
|
+
if file.lower().endswith(".mp4"):
|
|
380
|
+
full_path = os.path.join(res_dir, file)
|
|
381
|
+
# Add the same video 50 times to the list
|
|
382
|
+
videos.extend([full_path] * 50)
|
|
383
|
+
if videos:
|
|
384
|
+
videos_by_resolution[res_name] = videos
|
|
385
|
+
return videos_by_resolution
|
|
386
|
+
|
|
387
|
+
def plot_decoder_performance():
|
|
388
|
+
"""Create bar plots for decoder performance across resolutions for each thread count"""
|
|
389
|
+
# Read JSON files for the specific codec
|
|
390
|
+
results = {}
|
|
391
|
+
for filename in glob.glob(f"cached_decoder_performance_{args.codec}_*_threads.json"):
|
|
392
|
+
# Extract thread count from filename
|
|
393
|
+
thread_count = int(filename.split('_')[4])
|
|
394
|
+
with open(filename, 'r') as f:
|
|
395
|
+
results[thread_count] = json.load(f)
|
|
396
|
+
|
|
397
|
+
if not results:
|
|
398
|
+
print(f"No JSON files found for codec {args.codec}")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
# Sort thread counts to ensure correct order
|
|
402
|
+
thread_counts = sorted(results.keys())
|
|
403
|
+
|
|
404
|
+
# Define resolution order for consistent plotting
|
|
405
|
+
resolution_order = ['360p', '480p', '720p', '1080p', '4k']
|
|
406
|
+
|
|
407
|
+
for thread_count in thread_counts:
|
|
408
|
+
# Create a figure for each thread count
|
|
409
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
|
|
410
|
+
|
|
411
|
+
# Extract data for this thread count
|
|
412
|
+
data = {
|
|
413
|
+
'short': {'new': [], 'cached': [], 'resolutions': []},
|
|
414
|
+
'long': {'new': [], 'cached': [], 'resolutions': []}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Process each duration type
|
|
418
|
+
for duration_result in results[thread_count]:
|
|
419
|
+
duration_type = duration_result['duration_type']
|
|
420
|
+
|
|
421
|
+
# Sort results by resolution order
|
|
422
|
+
sorted_results = sorted(
|
|
423
|
+
duration_result['resolution_results'],
|
|
424
|
+
key=lambda x: resolution_order.index(x['resolution'])
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
for res_result in sorted_results:
|
|
428
|
+
data[duration_type]['resolutions'].append(res_result['resolution'])
|
|
429
|
+
data[duration_type]['new'].append(res_result['new_decoder']['fps'])
|
|
430
|
+
data[duration_type]['cached'].append(res_result['cached_decoder']['fps'])
|
|
431
|
+
|
|
432
|
+
# Plot for short duration videos
|
|
433
|
+
x = np.arange(len(data['short']['resolutions']))
|
|
434
|
+
width = 0.35
|
|
435
|
+
|
|
436
|
+
ax1.bar(x - width/2, data['short']['new'], width, label='Simple Decoder')
|
|
437
|
+
ax1.bar(x + width/2, data['short']['cached'], width, label='Cached Decoder')
|
|
438
|
+
ax1.set_title(f'Short Duration Videos (2s) - {thread_count} Thread(s) - {args.codec.upper()}')
|
|
439
|
+
ax1.set_xlabel('Resolution')
|
|
440
|
+
ax1.set_ylabel('FPS')
|
|
441
|
+
ax1.set_xticks(x)
|
|
442
|
+
ax1.set_xticklabels(data['short']['resolutions'])
|
|
443
|
+
ax1.grid(True, axis='y')
|
|
444
|
+
ax1.legend()
|
|
445
|
+
|
|
446
|
+
# Plot for long duration videos
|
|
447
|
+
x = np.arange(len(data['long']['resolutions']))
|
|
448
|
+
ax2.bar(x - width/2, data['long']['new'], width, label='Simple Decoder')
|
|
449
|
+
ax2.bar(x + width/2, data['long']['cached'], width, label='Cached Decoder')
|
|
450
|
+
ax2.set_title(f'Long Duration Videos (30s) - {thread_count} Thread(s) - {args.codec.upper()}')
|
|
451
|
+
ax2.set_xlabel('Resolution')
|
|
452
|
+
ax2.set_ylabel('FPS')
|
|
453
|
+
ax2.set_xticks(x)
|
|
454
|
+
ax2.set_xticklabels(data['long']['resolutions'])
|
|
455
|
+
ax2.grid(True, axis='y')
|
|
456
|
+
ax2.legend()
|
|
457
|
+
|
|
458
|
+
# Adjust layout and save
|
|
459
|
+
plt.tight_layout()
|
|
460
|
+
plt.savefig(f'cached_decoder_performance_{args.codec}_{thread_count}_threads.png')
|
|
461
|
+
plt.close()
|
|
462
|
+
|
|
463
|
+
print(f"Plot saved as 'cached_decoder_performance_{args.codec}_{thread_count}_threads.png'")
|
|
464
|
+
|
|
465
|
+
if __name__=="__main__":
|
|
466
|
+
script_start_time = time.perf_counter()
|
|
467
|
+
|
|
468
|
+
parser = argparse.ArgumentParser(
|
|
469
|
+
'This sample application measures decoding performance in FPS.'
|
|
470
|
+
)
|
|
471
|
+
parser.add_argument(
|
|
472
|
+
"--plot-only", action="store_true", default=False,
|
|
473
|
+
help="only generate plots from existing JSON files (default: False)")
|
|
474
|
+
parser.add_argument(
|
|
475
|
+
"--codec", type=str, choices=['h264', 'hevc', 'av1'], default='h264',
|
|
476
|
+
help="video codec to use (h264, hevc, or av1)")
|
|
477
|
+
parser.add_argument(
|
|
478
|
+
"--nvdecs", type=int, default=3, help="number of NVDECs to use (default: 3)")
|
|
479
|
+
parser.add_argument(
|
|
480
|
+
"--fps", type=int, default=30, help="frame rate for video generation (default: 30)")
|
|
481
|
+
parser.add_argument(
|
|
482
|
+
"--gop", type=int, default=60, help="GOP size for video generation (default: 60)")
|
|
483
|
+
|
|
484
|
+
args = parser.parse_args()
|
|
485
|
+
|
|
486
|
+
if args.plot_only:
|
|
487
|
+
plot_decoder_performance()
|
|
488
|
+
exit(0)
|
|
489
|
+
|
|
490
|
+
# Define thread counts to test - only 1 and max threads
|
|
491
|
+
num_of_threads = [1, args.nvdecs] if args.nvdecs > 1 else [1]
|
|
492
|
+
|
|
493
|
+
# Define resolutions with their dimensions
|
|
494
|
+
resolutions = [
|
|
495
|
+
("360p", "640x360"),
|
|
496
|
+
("480p", "854x480"),
|
|
497
|
+
("720p", "1280x720"),
|
|
498
|
+
("1080p", "1920x1080"),
|
|
499
|
+
("4k", "3840x2160")
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
# Use the test videos directory in the same directory as the script
|
|
503
|
+
base_videos_dir = os.path.join(os.path.dirname(__file__), f"test_videos_{args.codec}")
|
|
504
|
+
short_videos_dir = os.path.join(base_videos_dir, "short_videos")
|
|
505
|
+
long_videos_dir = os.path.join(base_videos_dir, "long_videos")
|
|
506
|
+
|
|
507
|
+
# Create base directories if they don't exist
|
|
508
|
+
os.makedirs(short_videos_dir, exist_ok=True)
|
|
509
|
+
os.makedirs(long_videos_dir, exist_ok=True)
|
|
510
|
+
|
|
511
|
+
# Define video generation parameters
|
|
512
|
+
codec_map = {
|
|
513
|
+
'h264': 'h264_nvenc',
|
|
514
|
+
'hevc': 'hevc_nvenc',
|
|
515
|
+
'av1': 'av1_nvenc'
|
|
516
|
+
}
|
|
517
|
+
encoding = codec_map[args.codec] # Use selected codec
|
|
518
|
+
pattern = "mandelbrot" # Test pattern
|
|
519
|
+
fps = args.fps # Frame rate from command line
|
|
520
|
+
pix_fmt = "yuv420p" # Standard pixel format
|
|
521
|
+
gop_size = args.gop # GOP size from command line
|
|
522
|
+
cq = 25 # Constant Quality value
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# Find ffmpeg in a platform-independent way
|
|
526
|
+
ffmpeg_path = "ffmpeg"
|
|
527
|
+
if platform.system() == "Windows":
|
|
528
|
+
# Try to find ffmpeg in PATH
|
|
529
|
+
for path in os.environ["PATH"].split(os.pathsep):
|
|
530
|
+
ffmpeg_exe = Path(path) / "ffmpeg.exe"
|
|
531
|
+
if ffmpeg_exe.exists():
|
|
532
|
+
ffmpeg_path = str(ffmpeg_exe)
|
|
533
|
+
break
|
|
534
|
+
|
|
535
|
+
# Generate videos for each resolution
|
|
536
|
+
for res_name, res_dim in resolutions:
|
|
537
|
+
# Short duration videos
|
|
538
|
+
short_video_configs = [{
|
|
539
|
+
"resolution": res_dim,
|
|
540
|
+
"gop": gop_size,
|
|
541
|
+
"duration": 2 # Fixed 2 seconds
|
|
542
|
+
}]
|
|
543
|
+
create_test_videos(
|
|
544
|
+
short_video_configs,
|
|
545
|
+
encoding,
|
|
546
|
+
pattern,
|
|
547
|
+
fps,
|
|
548
|
+
pix_fmt,
|
|
549
|
+
ffmpeg_path,
|
|
550
|
+
str(short_videos_dir),
|
|
551
|
+
"short",
|
|
552
|
+
res_name,
|
|
553
|
+
1, # Always generate 1 video per resolution
|
|
554
|
+
cq
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Long duration videos
|
|
558
|
+
long_video_configs = [{
|
|
559
|
+
"resolution": res_dim,
|
|
560
|
+
"gop": gop_size,
|
|
561
|
+
"duration": 30 # Fixed 30 seconds
|
|
562
|
+
}]
|
|
563
|
+
create_test_videos(
|
|
564
|
+
long_video_configs,
|
|
565
|
+
encoding,
|
|
566
|
+
pattern,
|
|
567
|
+
fps,
|
|
568
|
+
pix_fmt,
|
|
569
|
+
ffmpeg_path,
|
|
570
|
+
str(long_videos_dir),
|
|
571
|
+
"long",
|
|
572
|
+
res_name,
|
|
573
|
+
1, # Always generate 1 video per resolution
|
|
574
|
+
cq
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# Collect videos from all resolution directories
|
|
578
|
+
short_clips = collect_videos(short_videos_dir, resolutions)
|
|
579
|
+
long_clips = collect_videos(long_videos_dir, resolutions)
|
|
580
|
+
|
|
581
|
+
all_results = []
|
|
582
|
+
|
|
583
|
+
# Run benchmark for each thread count
|
|
584
|
+
for thread_count in num_of_threads:
|
|
585
|
+
print(f"\nRunning benchmark with {thread_count} thread(s)")
|
|
586
|
+
|
|
587
|
+
if short_clips:
|
|
588
|
+
print(f"\nProcessing short duration videos:")
|
|
589
|
+
for res, clips in short_clips.items():
|
|
590
|
+
print(f" Generating stats for {res} short videos")
|
|
591
|
+
print(f" {res}: {len(clips)} video(s)")
|
|
592
|
+
short_results = decode_perf_results(short_clips, thread_count, "short")
|
|
593
|
+
all_results.append(short_results)
|
|
594
|
+
|
|
595
|
+
if long_clips:
|
|
596
|
+
print(f"\nProcessing long duration videos:")
|
|
597
|
+
for res, clips in long_clips.items():
|
|
598
|
+
print(f" Generating stats for {res} long videos")
|
|
599
|
+
print(f" {res}: {len(clips)} video(s)")
|
|
600
|
+
long_results = decode_perf_results(long_clips, thread_count, "long")
|
|
601
|
+
all_results.append(long_results)
|
|
602
|
+
|
|
603
|
+
# Print video configuration details
|
|
604
|
+
print("\nVideo Configuration:")
|
|
605
|
+
print(f"Pattern: {pattern}")
|
|
606
|
+
print(f"Frame Rate: {fps} fps")
|
|
607
|
+
print(f"GOP Size: {gop_size}")
|
|
608
|
+
print(f"Number of threads: {thread_count}")
|
|
609
|
+
|
|
610
|
+
if all_results:
|
|
611
|
+
# Display tabulated results
|
|
612
|
+
display_results_table(all_results)
|
|
613
|
+
|
|
614
|
+
# Save results to JSON file with codec and thread count in filename
|
|
615
|
+
filename = f"cached_decoder_performance_{args.codec}_{thread_count}_threads.json"
|
|
616
|
+
|
|
617
|
+
# Convert dataclass objects to dictionaries
|
|
618
|
+
results_dict = [result.to_dict() for result in all_results]
|
|
619
|
+
|
|
620
|
+
with open(filename, 'w') as f:
|
|
621
|
+
json.dump(results_dict, f, indent=4)
|
|
622
|
+
|
|
623
|
+
print(f"\nResults saved to {filename}")
|
|
624
|
+
|
|
625
|
+
# Clear results for next thread count
|
|
626
|
+
all_results = []
|
|
627
|
+
else:
|
|
628
|
+
print("No videos were found or generated. Please check the directory structure and video generation process.")
|
|
629
|
+
|
|
630
|
+
# After running the benchmark, generate plots
|
|
631
|
+
plot_decoder_performance()
|
|
632
|
+
|
|
633
|
+
# Print total execution time
|
|
634
|
+
total_time = time.perf_counter() - script_start_time
|
|
635
|
+
print(f"\nTotal execution time: {total_time:.2f} seconds")
|