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,1072 @@
|
|
|
1
|
+
# This copyright notice applies to this file only
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
8
|
+
# to deal in the Software without restriction, including without limitation
|
|
9
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
10
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
11
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
|
14
|
+
# all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
19
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
22
|
+
# DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
import time
|
|
28
|
+
import random
|
|
29
|
+
import json
|
|
30
|
+
import datetime
|
|
31
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
32
|
+
from concurrent.futures import wait
|
|
33
|
+
import PyNvVideoCodec as nvc
|
|
34
|
+
|
|
35
|
+
import pycuda.driver as cuda
|
|
36
|
+
import threading
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
This script demonstrates generating a Mandelbrot set video with FFmpeg
|
|
41
|
+
and then transcoding it using multiple threads with PyNvVideoCodec.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
- gpu_id (int): Ordinal of GPU to use
|
|
45
|
+
- width (int): Width of the generated video
|
|
46
|
+
- height (int): Height of the generated video
|
|
47
|
+
- duration (int): Duration of the generated video in seconds
|
|
48
|
+
- fps (int): Frames per second
|
|
49
|
+
- preset (str): Encoder preset (P1-P7)
|
|
50
|
+
- codec (str): Output codec (h264, hevc, av1)
|
|
51
|
+
- numthreads (int): Number of concurrent threads for parallel processing
|
|
52
|
+
- segments (int): Number of random segments to create
|
|
53
|
+
- usage (int): Transcoding mode selection (can specify multiple):
|
|
54
|
+
0: PyNVC transcoding (output in pynvc_out/)
|
|
55
|
+
1: FFmpeg without map (output in ffmpeg_out/)
|
|
56
|
+
2: FFmpeg with map, no audio (output in ffmpeg_fc_out/)
|
|
57
|
+
3: FFmpeg with map and audio (output in ffmpeg_fc_out/)
|
|
58
|
+
- log (str): Path to save the execution log (default: logs/run_{timestamp}.json)
|
|
59
|
+
- replay (str): Path to a log file to replay a previous run
|
|
60
|
+
|
|
61
|
+
Returns: - None.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> python mandelbrot_transcode.py -g 0 -W 1920 -H 1080 -d 10 -fps 30 -p P4 -c h264 -n 2 -s 5 -u 0
|
|
65
|
+
|
|
66
|
+
Replay mode:
|
|
67
|
+
>>> python mandelbrot_transcode.py --replay logs/run_20240615_123045.json
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Global log file handle
|
|
71
|
+
log_file = None
|
|
72
|
+
log_data = {
|
|
73
|
+
"timestamp": "",
|
|
74
|
+
"args": {},
|
|
75
|
+
"steps": [],
|
|
76
|
+
"results": {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def log_step(step_type, description, params=None, results=None):
|
|
80
|
+
"""
|
|
81
|
+
Log a step to both console and log file
|
|
82
|
+
|
|
83
|
+
Parameters:
|
|
84
|
+
step_type (str): Type of step (video_generation, segment_creation, transcoding)
|
|
85
|
+
description (str): Human-readable description
|
|
86
|
+
params (dict): Parameters used for this step
|
|
87
|
+
results (dict): Results from this step
|
|
88
|
+
"""
|
|
89
|
+
global log_file, log_data
|
|
90
|
+
|
|
91
|
+
step = {
|
|
92
|
+
"type": step_type,
|
|
93
|
+
"description": description,
|
|
94
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
95
|
+
"params": params or {},
|
|
96
|
+
"results": results or {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Add to log data
|
|
100
|
+
log_data["steps"].append(step)
|
|
101
|
+
|
|
102
|
+
# Print to console
|
|
103
|
+
print(f"[LOG] {description}")
|
|
104
|
+
|
|
105
|
+
# Write to log file if open
|
|
106
|
+
if log_file:
|
|
107
|
+
# Update the file with the latest log data
|
|
108
|
+
log_file.seek(0)
|
|
109
|
+
json.dump(log_data, log_file, indent=2)
|
|
110
|
+
log_file.truncate()
|
|
111
|
+
log_file.flush()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def generate_mandelbrot_video(width, height, duration, fps, output_path, gop_size, input_codec="h264", input_bframes=2):
|
|
115
|
+
"""
|
|
116
|
+
Generate a Mandelbrot set zoom video with audio using FFmpeg
|
|
117
|
+
|
|
118
|
+
Parameters:
|
|
119
|
+
width (int): Video width
|
|
120
|
+
height (int): Video height
|
|
121
|
+
duration (int): Video duration in seconds
|
|
122
|
+
fps (int): Frames per second
|
|
123
|
+
output_path (str): Path to save the generated video
|
|
124
|
+
gop_size (int): GOP size for encoding
|
|
125
|
+
input_codec (str): Codec to use for video generation (h264, hevc, av1)
|
|
126
|
+
input_bframes (int): Number of B-frames for video generation
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: Path to the generated video
|
|
130
|
+
"""
|
|
131
|
+
# Map codec to FFmpeg encoder name
|
|
132
|
+
encoder_map = {
|
|
133
|
+
"h264": "h264_nvenc",
|
|
134
|
+
"hevc": "hevc_nvenc",
|
|
135
|
+
"av1": "av1_nvenc"
|
|
136
|
+
}
|
|
137
|
+
encoder = encoder_map.get(input_codec.lower(), "h264_nvenc")
|
|
138
|
+
|
|
139
|
+
# Update output path to include codec information
|
|
140
|
+
base, ext = os.path.splitext(output_path)
|
|
141
|
+
codec_output_path = f"{base}_{input_codec}_{width}x{height}{ext}"
|
|
142
|
+
|
|
143
|
+
# Log the step
|
|
144
|
+
log_step("video_generation", f"Generating Mandelbrot video ({width}x{height}, {duration}s, {fps} fps, {input_codec}, B-frames: {input_bframes})",
|
|
145
|
+
params={"width": width, "height": height, "duration": duration, "fps": fps,
|
|
146
|
+
"output_path": codec_output_path, "gop_size": gop_size, "codec": input_codec, "bframes": input_bframes})
|
|
147
|
+
|
|
148
|
+
print(f"Generating Mandelbrot set video with audio at {width}x{height}, {duration}s duration, {fps} fps")
|
|
149
|
+
print(f"Using codec: {input_codec}, encoder: {encoder}, B-frames: {input_bframes}")
|
|
150
|
+
|
|
151
|
+
# Create the directory if it doesn't exist
|
|
152
|
+
os.makedirs(os.path.dirname(codec_output_path) or '.', exist_ok=True)
|
|
153
|
+
|
|
154
|
+
print(f"Using GOP size: {gop_size} and {input_bframes} B-frames")
|
|
155
|
+
|
|
156
|
+
# FFmpeg command to generate a Mandelbrot set zoom video with audio
|
|
157
|
+
cmd = [
|
|
158
|
+
"ffmpeg", "-y",
|
|
159
|
+
"-v", "warning",
|
|
160
|
+
# Video input from mandelbrot filter
|
|
161
|
+
"-f", "lavfi",
|
|
162
|
+
"-i", f"mandelbrot=size={width}x{height}:rate={fps}:start_scale=3:end_scale=0.1",
|
|
163
|
+
# Audio input - sine wave that increases in frequency during the video
|
|
164
|
+
"-f", "lavfi",
|
|
165
|
+
"-i", f"sine=frequency=220:sample_rate=44100:duration={duration}",
|
|
166
|
+
# Set duration
|
|
167
|
+
"-t", str(duration),
|
|
168
|
+
# Video encoding with specified codec
|
|
169
|
+
"-c:v", encoder,
|
|
170
|
+
"-preset", "medium",
|
|
171
|
+
"-g", str(gop_size), # Convert gop_size to string here
|
|
172
|
+
"-bf", str(input_bframes), # Set B-frames from parameter
|
|
173
|
+
# Audio encoding
|
|
174
|
+
"-c:a", "aac",
|
|
175
|
+
"-b:a", "128k",
|
|
176
|
+
# Output
|
|
177
|
+
codec_output_path
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# Execute FFmpeg command and wait for completion
|
|
182
|
+
res = ' '.join(cmd)
|
|
183
|
+
print(f"{res=}")
|
|
184
|
+
print("Starting FFmpeg process to generate Mandelbrot video with audio...")
|
|
185
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
186
|
+
stdout, stderr = process.communicate()
|
|
187
|
+
|
|
188
|
+
if process.returncode != 0:
|
|
189
|
+
print(f"Error generating video: {stderr.decode()}")
|
|
190
|
+
raise subprocess.CalledProcessError(process.returncode, cmd, stderr)
|
|
191
|
+
|
|
192
|
+
print(f"Video generation with audio complete: {codec_output_path}")
|
|
193
|
+
# Print video information
|
|
194
|
+
print(f"Video codec: {encoder} with {gop_size} GOP size and {input_bframes} B-frames")
|
|
195
|
+
return codec_output_path
|
|
196
|
+
except subprocess.CalledProcessError as e:
|
|
197
|
+
print(f"FFmpeg error: {e}")
|
|
198
|
+
raise
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"Error: {e}")
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def generate_random_segments(duration, num_segments, min_segment_duration=1.0):
|
|
205
|
+
"""
|
|
206
|
+
Generate random non-overlapping start and end timestamps for video segments
|
|
207
|
+
|
|
208
|
+
Parameters:
|
|
209
|
+
duration (float): Total video duration in seconds
|
|
210
|
+
num_segments (int): Number of segments to generate
|
|
211
|
+
min_segment_duration (float): Minimum duration of each segment in seconds
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
list: List of tuples containing (start_time, end_time) for each segment
|
|
215
|
+
"""
|
|
216
|
+
segments = []
|
|
217
|
+
|
|
218
|
+
# Make sure we can fit all segments with minimum duration
|
|
219
|
+
if num_segments * min_segment_duration > duration:
|
|
220
|
+
num_segments = int(duration / min_segment_duration)
|
|
221
|
+
print(f"Warning: Reduced number of segments to {num_segments} to ensure minimum segment duration")
|
|
222
|
+
|
|
223
|
+
# Calculate max segment duration to ensure all segments fit
|
|
224
|
+
max_segment_duration = (duration / num_segments) * 2
|
|
225
|
+
|
|
226
|
+
# Divide the video into regions to ensure better distribution
|
|
227
|
+
time_points = sorted([random.uniform(0, duration) for _ in range(num_segments * 2)])
|
|
228
|
+
time_points = [0] + time_points + [duration]
|
|
229
|
+
|
|
230
|
+
# Track available time regions
|
|
231
|
+
available_regions = [(time_points[i], time_points[i+1])
|
|
232
|
+
for i in range(len(time_points)-1)
|
|
233
|
+
if time_points[i+1] - time_points[i] >= min_segment_duration]
|
|
234
|
+
|
|
235
|
+
# Generate segments
|
|
236
|
+
for _ in range(num_segments):
|
|
237
|
+
if not available_regions:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# Select a random region
|
|
241
|
+
region_idx = random.randint(0, len(available_regions) - 1)
|
|
242
|
+
region_start, region_end = available_regions.pop(region_idx)
|
|
243
|
+
|
|
244
|
+
# Make sure region is large enough
|
|
245
|
+
if region_end - region_start < min_segment_duration:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Calculate segment within the region
|
|
249
|
+
segment_duration = min(
|
|
250
|
+
random.uniform(min_segment_duration, max_segment_duration),
|
|
251
|
+
region_end - region_start
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Randomize start position within the region constraint
|
|
255
|
+
max_start = region_end - segment_duration
|
|
256
|
+
start_time = random.uniform(region_start, max_start) if region_start < max_start else region_start
|
|
257
|
+
end_time = start_time + segment_duration
|
|
258
|
+
|
|
259
|
+
segments.append((start_time, end_time))
|
|
260
|
+
|
|
261
|
+
# If we didn't get enough segments, add more deterministically
|
|
262
|
+
if len(segments) < num_segments:
|
|
263
|
+
remaining = num_segments - len(segments)
|
|
264
|
+
segment_duration = max(min_segment_duration, duration / (remaining * 2))
|
|
265
|
+
|
|
266
|
+
# Find gaps between existing segments
|
|
267
|
+
all_times = [(s, True) for s, e in segments] + [(e, False) for s, e in segments]
|
|
268
|
+
all_times.sort()
|
|
269
|
+
|
|
270
|
+
# Add video start and end if not already included
|
|
271
|
+
if not all_times or all_times[0][0] > 0:
|
|
272
|
+
all_times.insert(0, (0, False))
|
|
273
|
+
if not all_times or all_times[-1][0] < duration:
|
|
274
|
+
all_times.append((duration, True))
|
|
275
|
+
|
|
276
|
+
# Find gaps
|
|
277
|
+
gaps = []
|
|
278
|
+
for i in range(len(all_times) - 1):
|
|
279
|
+
if all_times[i][1] and not all_times[i+1][1]: # End followed by start
|
|
280
|
+
gap_start = all_times[i][0]
|
|
281
|
+
gap_end = all_times[i+1][0]
|
|
282
|
+
if gap_end - gap_start >= segment_duration:
|
|
283
|
+
gaps.append((gap_start, gap_end))
|
|
284
|
+
|
|
285
|
+
# Add segments in gaps
|
|
286
|
+
for gap_start, gap_end in gaps:
|
|
287
|
+
if len(segments) >= num_segments:
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
available_duration = gap_end - gap_start
|
|
291
|
+
num_to_add = min(remaining, int(available_duration / segment_duration))
|
|
292
|
+
|
|
293
|
+
for i in range(num_to_add):
|
|
294
|
+
start = gap_start + (i * segment_duration)
|
|
295
|
+
end = start + segment_duration
|
|
296
|
+
segments.append((start, end))
|
|
297
|
+
remaining -= 1
|
|
298
|
+
|
|
299
|
+
# Sort segments by start time
|
|
300
|
+
segments.sort()
|
|
301
|
+
|
|
302
|
+
return segments[:num_segments] # Ensure we don't return more than requested
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_subprocess(cmd, shell=False, description=""):
|
|
306
|
+
"""
|
|
307
|
+
Run a subprocess and wait for completion, with proper error handling
|
|
308
|
+
|
|
309
|
+
Parameters:
|
|
310
|
+
cmd: Command to run (list or string)
|
|
311
|
+
shell: Whether to use shell execution
|
|
312
|
+
description: Description of the process for logging
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
tuple: (stdout, stderr) from the process
|
|
316
|
+
"""
|
|
317
|
+
try:
|
|
318
|
+
print(f"Starting {description}...")
|
|
319
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
320
|
+
text=True, shell=shell)
|
|
321
|
+
|
|
322
|
+
# Wait for the process to complete
|
|
323
|
+
stdout, stderr = process.communicate()
|
|
324
|
+
|
|
325
|
+
# Check if process completed successfully
|
|
326
|
+
if process.returncode != 0:
|
|
327
|
+
print(f"Error in {description}: {stderr}")
|
|
328
|
+
print(f"Command was: {cmd}")
|
|
329
|
+
else:
|
|
330
|
+
print(f"{description} completed successfully")
|
|
331
|
+
|
|
332
|
+
return stdout, stderr
|
|
333
|
+
except Exception as e:
|
|
334
|
+
print(f"Exception in {description}: {e}")
|
|
335
|
+
raise
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def segmented_transcode(args):
|
|
339
|
+
"""
|
|
340
|
+
Transcode the input video using the specified method
|
|
341
|
+
|
|
342
|
+
Parameters:
|
|
343
|
+
args (list): List of parameters including video path, clip_start_end, config, usage, etc.
|
|
344
|
+
"""
|
|
345
|
+
video_path, clip_start_end, config, fps, usage, chunk_size = args
|
|
346
|
+
print(f"Segmented transcode for {video_path}")
|
|
347
|
+
print(f"Number of segments: {len(clip_start_end)}")
|
|
348
|
+
print(f"Using mode: {usage}")
|
|
349
|
+
|
|
350
|
+
# Get B-frames setting from config
|
|
351
|
+
bframes = config.get("bf", 0) # Default to 0 if not specified
|
|
352
|
+
print(f"Using {bframes} B-frames for transcoding")
|
|
353
|
+
|
|
354
|
+
# Properly extract base filename without extension
|
|
355
|
+
base, _ = os.path.splitext(os.path.basename(video_path))
|
|
356
|
+
|
|
357
|
+
# Create output directory if it doesn't exist
|
|
358
|
+
os.makedirs(os.path.dirname(os.path.abspath(video_path)), exist_ok=True)
|
|
359
|
+
|
|
360
|
+
# Start timing
|
|
361
|
+
start_time = time.time()
|
|
362
|
+
|
|
363
|
+
# Get codec and resolution from config
|
|
364
|
+
codec = config.get("codec", "h264")
|
|
365
|
+
width = config.get("width", 1920)
|
|
366
|
+
height = config.get("height", 1080)
|
|
367
|
+
resolution = f"{width}x{height}"
|
|
368
|
+
|
|
369
|
+
# Create a resolution-codec-bframes suffix for file names
|
|
370
|
+
suffix = f"_{codec}_{resolution}_bf{bframes}"
|
|
371
|
+
|
|
372
|
+
# Determine FFmpeg encoder based on output codec
|
|
373
|
+
encoder_map = {
|
|
374
|
+
"h264": "h264_nvenc",
|
|
375
|
+
"hevc": "hevc_nvenc",
|
|
376
|
+
"av1": "av1_nvenc"
|
|
377
|
+
}
|
|
378
|
+
encoder = encoder_map.get(codec.lower(), "h264_nvenc")
|
|
379
|
+
|
|
380
|
+
# Determine input video codec and decoder to use
|
|
381
|
+
# First try to get from input file using ffprobe
|
|
382
|
+
input_codec = "h264" # Default assumption
|
|
383
|
+
try:
|
|
384
|
+
probe_cmd = ["ffprobe", "-v", "error", "-select_streams", "v:0",
|
|
385
|
+
"-show_entries", "stream=codec_name", "-of", "csv=p=0", video_path]
|
|
386
|
+
probe_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
387
|
+
if probe_result.returncode == 0 and probe_result.stdout.strip():
|
|
388
|
+
input_codec = probe_result.stdout.strip().lower()
|
|
389
|
+
print(f"Detected input codec: {input_codec}")
|
|
390
|
+
except Exception as e:
|
|
391
|
+
print(f"Error detecting input codec: {str(e)}. Using default h264 decoder.")
|
|
392
|
+
|
|
393
|
+
# Map to cuvid decoder
|
|
394
|
+
decoder_map = {
|
|
395
|
+
"h264": "h264_cuvid",
|
|
396
|
+
"avc": "h264_cuvid",
|
|
397
|
+
"hevc": "hevc_cuvid",
|
|
398
|
+
"av1": "av1_cuvid"
|
|
399
|
+
}
|
|
400
|
+
decoder = decoder_map.get(input_codec, "h264_cuvid")
|
|
401
|
+
print(f"Using decoder: {decoder}")
|
|
402
|
+
|
|
403
|
+
# Create output directories if they don't exist
|
|
404
|
+
os.makedirs("pynvc_out", exist_ok=True)
|
|
405
|
+
os.makedirs("ffmpeg_out", exist_ok=True)
|
|
406
|
+
os.makedirs("ffmpeg_fc_out", exist_ok=True)
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
if usage == 0:
|
|
410
|
+
# PyNVC Usage
|
|
411
|
+
out_file_path = os.path.join("pynvc_out", f"{base}{suffix}_transcoded.mp4")
|
|
412
|
+
print(f"Output will be saved to: ./pynvc_out/")
|
|
413
|
+
|
|
414
|
+
# Make a copy of the config and remove width and height
|
|
415
|
+
transcoder_config = config.copy()
|
|
416
|
+
if 'width' in transcoder_config:
|
|
417
|
+
del transcoder_config['width']
|
|
418
|
+
if 'height' in transcoder_config:
|
|
419
|
+
del transcoder_config['height']
|
|
420
|
+
|
|
421
|
+
# Pass the modified config to the Transcoder
|
|
422
|
+
tr = nvc.Transcoder(video_path, out_file_path, 0, 0, 0, **transcoder_config)
|
|
423
|
+
for start, end in clip_start_end:
|
|
424
|
+
tr.segmented_transcode(start, end)
|
|
425
|
+
|
|
426
|
+
elif usage == 1:
|
|
427
|
+
print(f"FFmpeg without map")
|
|
428
|
+
print(f"Output files will be saved to: ./ffmpeg_out/")
|
|
429
|
+
for i in range(0, len(clip_start_end), chunk_size):
|
|
430
|
+
chunk = clip_start_end[i:i+chunk_size]
|
|
431
|
+
for start, end in chunk:
|
|
432
|
+
# Include codec and resolution in output file path
|
|
433
|
+
out_file_path = f"./ffmpeg_out/{base}{suffix}_{start:.2f}_{end:.2f}.mp4"
|
|
434
|
+
ff = (" ffmpeg -loglevel warning -hide_banner -y "
|
|
435
|
+
f" -ss {str(start)} "
|
|
436
|
+
f" -to {str(end)} "
|
|
437
|
+
f" -c:v {decoder} " # Use appropriate decoder based on input
|
|
438
|
+
f" -i {video_path} "
|
|
439
|
+
f" -bf {bframes}" # Use B-frames from config
|
|
440
|
+
f" -c:v {encoder} " # Use the correct encoder based on output codec
|
|
441
|
+
" -c:a copy "
|
|
442
|
+
f"{out_file_path}")
|
|
443
|
+
|
|
444
|
+
desc = f"FFmpeg transcode segment {start:.2f}s - {end:.2f}s"
|
|
445
|
+
run_subprocess(ff, shell=True, description=desc)
|
|
446
|
+
|
|
447
|
+
elif usage == 2:
|
|
448
|
+
print(f"FFmpeg with map but no audio")
|
|
449
|
+
print(f"Output files will be saved to: ./ffmpeg_fc_out/")
|
|
450
|
+
for i in range(0, len(clip_start_end), chunk_size):
|
|
451
|
+
chunk = clip_start_end[i:i+chunk_size]
|
|
452
|
+
|
|
453
|
+
segment_cmd = ["ffmpeg", "-loglevel", "warning", "-hide_banner", "-y",
|
|
454
|
+
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
|
|
455
|
+
"-c:v", decoder, # Use appropriate decoder
|
|
456
|
+
"-i", video_path]
|
|
457
|
+
|
|
458
|
+
filter_complex = []
|
|
459
|
+
map_cmds = []
|
|
460
|
+
output_files = []
|
|
461
|
+
|
|
462
|
+
for j, (start, end) in enumerate(chunk):
|
|
463
|
+
# Include codec and resolution in output file path
|
|
464
|
+
out_file_path = f"./ffmpeg_fc_out/{base}{suffix}_{start:.2f}_{end:.2f}.mp4"
|
|
465
|
+
output_files.append(out_file_path)
|
|
466
|
+
|
|
467
|
+
# Add video filter
|
|
468
|
+
filter_complex.append(f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{j}]")
|
|
469
|
+
map_cmds.extend(["-map", f"[v{j}]", "-c:v", encoder, "-an", out_file_path])
|
|
470
|
+
|
|
471
|
+
# Combine all filters
|
|
472
|
+
segment_cmd.extend(["-filter_complex", ";".join(filter_complex)])
|
|
473
|
+
segment_cmd.extend(map_cmds)
|
|
474
|
+
|
|
475
|
+
# Convert the list to a shell command string
|
|
476
|
+
cmd_str = " ".join([
|
|
477
|
+
f'"{arg}"' if (" " in arg or ";" in arg or "[" in arg) else arg
|
|
478
|
+
for arg in segment_cmd
|
|
479
|
+
])
|
|
480
|
+
|
|
481
|
+
# Execute FFmpeg command
|
|
482
|
+
desc = f"FFmpeg batch transcode {len(chunk)} segments (no audio)"
|
|
483
|
+
run_subprocess(cmd_str, shell=True, description=desc)
|
|
484
|
+
|
|
485
|
+
elif usage == 3:
|
|
486
|
+
print(f"FFmpeg with map and audio")
|
|
487
|
+
print(f"Output files will be saved to: ./ffmpeg_fc_out/")
|
|
488
|
+
for i in range(0, len(clip_start_end), chunk_size):
|
|
489
|
+
chunk = clip_start_end[i:i+chunk_size]
|
|
490
|
+
|
|
491
|
+
segment_cmd = ["ffmpeg", "-y", "-loglevel", "warning",
|
|
492
|
+
"-hwaccel", "cuda",
|
|
493
|
+
"-c:v", decoder, # Use appropriate decoder
|
|
494
|
+
"-i", video_path]
|
|
495
|
+
|
|
496
|
+
filter_complex = []
|
|
497
|
+
map_cmds = []
|
|
498
|
+
output_files = []
|
|
499
|
+
|
|
500
|
+
for j, (start, end) in enumerate(chunk):
|
|
501
|
+
# Include codec and resolution in output file path
|
|
502
|
+
out_file_path = f"./ffmpeg_fc_out/{base}{suffix}_{start:.2f}_{end:.2f}.mp4"
|
|
503
|
+
output_files.append(out_file_path)
|
|
504
|
+
|
|
505
|
+
# Modify filters to include accurate timing
|
|
506
|
+
filter_complex.append(f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS[v{j}]")
|
|
507
|
+
|
|
508
|
+
# Check if the video has audio
|
|
509
|
+
if os.path.exists(video_path):
|
|
510
|
+
probe_cmd = ["ffprobe", "-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "csv=p=0", video_path]
|
|
511
|
+
probe_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
512
|
+
has_audio = "audio" in probe_result.stdout
|
|
513
|
+
|
|
514
|
+
if has_audio:
|
|
515
|
+
filter_complex.append(f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[a{j}]")
|
|
516
|
+
map_cmds.extend(["-map", f"[v{j}]", "-map", f"[a{j}]"])
|
|
517
|
+
else:
|
|
518
|
+
map_cmds.extend(["-map", f"[v{j}]"])
|
|
519
|
+
else:
|
|
520
|
+
map_cmds.extend(["-map", f"[v{j}]"])
|
|
521
|
+
|
|
522
|
+
# Convert P4 to p4 (lowercase) for FFmpeg preset
|
|
523
|
+
ffmpeg_preset = config.get("preset", "p4").lower()
|
|
524
|
+
|
|
525
|
+
# Add encoding parameters
|
|
526
|
+
map_cmds.extend([
|
|
527
|
+
"-c:v", encoder, # Use appropriate encoder
|
|
528
|
+
"-preset", ffmpeg_preset,
|
|
529
|
+
"-rc", "vbr",
|
|
530
|
+
"-b:v", "5M",
|
|
531
|
+
"-maxrate", "10M",
|
|
532
|
+
"-bufsize", "10M",
|
|
533
|
+
])
|
|
534
|
+
|
|
535
|
+
# Add audio encoding if we have audio
|
|
536
|
+
audio_filter = f"a{j}" in "".join(filter_complex)
|
|
537
|
+
if audio_filter:
|
|
538
|
+
map_cmds.extend(["-c:a", "aac", "-b:a", "192k"])
|
|
539
|
+
|
|
540
|
+
map_cmds.append(out_file_path)
|
|
541
|
+
|
|
542
|
+
# Combine all filters
|
|
543
|
+
segment_cmd.extend(["-filter_complex", ";".join(filter_complex)])
|
|
544
|
+
segment_cmd.extend(map_cmds)
|
|
545
|
+
|
|
546
|
+
# Convert the list to a shell command string
|
|
547
|
+
cmd_str = " ".join([
|
|
548
|
+
f'"{arg}"' if (" " in arg or ";" in arg or "[" in arg) else arg
|
|
549
|
+
for arg in segment_cmd
|
|
550
|
+
])
|
|
551
|
+
|
|
552
|
+
# Execute FFmpeg command
|
|
553
|
+
desc = f"FFmpeg batch transcode {len(chunk)} segments (with audio)"
|
|
554
|
+
run_subprocess(cmd_str, shell=True, description=desc)
|
|
555
|
+
|
|
556
|
+
else:
|
|
557
|
+
print(f"Error: Invalid usage mode {usage}. Must be 0, 1, 2, or 3")
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
print(f"Error processing {video_path}: {str(e)}")
|
|
561
|
+
raise
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def extract_segments_from_log(log_data):
|
|
565
|
+
"""
|
|
566
|
+
Extract segment information from log data
|
|
567
|
+
|
|
568
|
+
Parameters:
|
|
569
|
+
log_data (dict): Log data from a previous run
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
list: List of (start_time, end_time) tuples
|
|
573
|
+
"""
|
|
574
|
+
try:
|
|
575
|
+
# Find segment creation step
|
|
576
|
+
for step in log_data.get("steps", []):
|
|
577
|
+
if step.get("type") == "segment_creation":
|
|
578
|
+
if "results" in step and "segments" in step["results"]:
|
|
579
|
+
segments = step["results"]["segments"]
|
|
580
|
+
return [(segment["start"], segment["end"]) for segment in segments]
|
|
581
|
+
|
|
582
|
+
return []
|
|
583
|
+
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print(f"Error parsing segments from log: {e}")
|
|
586
|
+
return []
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def replay_from_log(log_path):
|
|
590
|
+
"""
|
|
591
|
+
Replay a previous run from a log file
|
|
592
|
+
|
|
593
|
+
Parameters:
|
|
594
|
+
log_path (str): Path to the log file
|
|
595
|
+
"""
|
|
596
|
+
global log_data
|
|
597
|
+
|
|
598
|
+
print(f"Replaying from log: {log_path}")
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
with open(log_path, 'r') as f:
|
|
602
|
+
log_data = json.load(f)
|
|
603
|
+
|
|
604
|
+
print(f"Log timestamp: {log_data['timestamp']}")
|
|
605
|
+
print(f"Original arguments: {json.dumps(log_data['args'], indent=2)}")
|
|
606
|
+
|
|
607
|
+
# Extract video path from the first video generation step
|
|
608
|
+
video_path = None
|
|
609
|
+
for step in log_data['steps']:
|
|
610
|
+
if step['type'] == 'video_generation':
|
|
611
|
+
video_path = step['params']['output_path']
|
|
612
|
+
break
|
|
613
|
+
|
|
614
|
+
if not video_path:
|
|
615
|
+
print("Error: No video generation step found in log")
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
# Check if the video exists
|
|
619
|
+
if not os.path.exists(video_path):
|
|
620
|
+
print(f"Video file not found: {video_path}")
|
|
621
|
+
print("Regenerating video...")
|
|
622
|
+
|
|
623
|
+
# Find the video generation step and extract parameters
|
|
624
|
+
for step in log_data['steps']:
|
|
625
|
+
if step['type'] == 'video_generation':
|
|
626
|
+
params = step['params']
|
|
627
|
+
video_path = generate_mandelbrot_video(
|
|
628
|
+
params['width'],
|
|
629
|
+
params['height'],
|
|
630
|
+
params['duration'],
|
|
631
|
+
params['fps'],
|
|
632
|
+
params['output_path'],
|
|
633
|
+
params['gop_size'],
|
|
634
|
+
params['input_codec'],
|
|
635
|
+
params['input_bframes']
|
|
636
|
+
)
|
|
637
|
+
break
|
|
638
|
+
else:
|
|
639
|
+
print(f"Using existing video: {video_path}")
|
|
640
|
+
|
|
641
|
+
# Extract segments
|
|
642
|
+
clip_start_end = extract_segments_from_log(log_data)
|
|
643
|
+
if not clip_start_end:
|
|
644
|
+
print("Error: No segments found in log")
|
|
645
|
+
return
|
|
646
|
+
|
|
647
|
+
# Extract usage modes
|
|
648
|
+
usage_modes = []
|
|
649
|
+
for step in log_data['steps']:
|
|
650
|
+
if step['type'] == 'transcoding' and 'usage' in step['params']:
|
|
651
|
+
if step['params']['usage'] not in usage_modes:
|
|
652
|
+
usage_modes.append(step['params']['usage'])
|
|
653
|
+
|
|
654
|
+
if not usage_modes:
|
|
655
|
+
print("Error: No transcoding steps found in log")
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
print(f"Replay will use these usage modes: {usage_modes}")
|
|
659
|
+
|
|
660
|
+
# Extract config
|
|
661
|
+
config = log_data['args'].get('config', {"preset": "P4", "codec": "h264", "tuning_info": "high_quality", "bf": "0"})
|
|
662
|
+
|
|
663
|
+
# Calculate total frames
|
|
664
|
+
fps = log_data['args'].get('fps', 30)
|
|
665
|
+
segment_frames = sum((end - start) * fps for start, end in clip_start_end)
|
|
666
|
+
|
|
667
|
+
# Store results for comparison
|
|
668
|
+
results = {}
|
|
669
|
+
|
|
670
|
+
# Run transcoding for each usage mode
|
|
671
|
+
for usage in usage_modes:
|
|
672
|
+
if usage not in [0, 1, 2, 3]:
|
|
673
|
+
print(f"Warning: Invalid usage mode {usage}. Skipping...")
|
|
674
|
+
continue
|
|
675
|
+
|
|
676
|
+
print("\n" + "=" * 80)
|
|
677
|
+
print(f"REPLAY: TRANSCODING SEGMENTS USING MODE {usage}")
|
|
678
|
+
print("=" * 80)
|
|
679
|
+
|
|
680
|
+
# Get mode description
|
|
681
|
+
mode_desc = {
|
|
682
|
+
0: "PyNVC transcoding",
|
|
683
|
+
1: "FFmpeg without map",
|
|
684
|
+
2: "FFmpeg with map (no audio)",
|
|
685
|
+
3: "FFmpeg with map (with audio)"
|
|
686
|
+
}.get(usage, "Unknown mode")
|
|
687
|
+
|
|
688
|
+
print(f"Running: {mode_desc}")
|
|
689
|
+
|
|
690
|
+
futures_list = []
|
|
691
|
+
starttime = time.time()
|
|
692
|
+
|
|
693
|
+
# Extract numthreads and chunk_size
|
|
694
|
+
numthreads = log_data['args'].get('numthreads', 1)
|
|
695
|
+
chunk_size = log_data['args'].get('chunk_size', 5)
|
|
696
|
+
|
|
697
|
+
with ThreadPoolExecutor(max_workers=numthreads) as executor:
|
|
698
|
+
# Submit task to thread pool
|
|
699
|
+
futures = [executor.submit(segmented_transcode, [
|
|
700
|
+
video_path,
|
|
701
|
+
clip_start_end,
|
|
702
|
+
config,
|
|
703
|
+
fps,
|
|
704
|
+
usage,
|
|
705
|
+
chunk_size
|
|
706
|
+
])]
|
|
707
|
+
futures_list.append(futures)
|
|
708
|
+
|
|
709
|
+
# Wait for all tasks to complete
|
|
710
|
+
for future in futures_list:
|
|
711
|
+
wait(future)
|
|
712
|
+
|
|
713
|
+
endtime = time.time()
|
|
714
|
+
total_time = endtime - starttime
|
|
715
|
+
throughput = segment_frames / total_time if total_time > 0 else 0
|
|
716
|
+
|
|
717
|
+
# Store results
|
|
718
|
+
results[usage] = {
|
|
719
|
+
'mode': mode_desc,
|
|
720
|
+
'time': total_time,
|
|
721
|
+
'throughput': throughput
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
print(f"Completed {mode_desc}")
|
|
725
|
+
print(f"Processing time: {total_time:.2f} seconds")
|
|
726
|
+
print(f"Throughput: {throughput:.2f} frames/second")
|
|
727
|
+
|
|
728
|
+
# Log the results
|
|
729
|
+
log_step("replay_results", f"Replay results for mode {usage}",
|
|
730
|
+
params={"usage": usage, "mode": mode_desc},
|
|
731
|
+
results={"time": total_time, "throughput": throughput})
|
|
732
|
+
|
|
733
|
+
# Print comparison results with additional details
|
|
734
|
+
print("\n" + "=" * 80)
|
|
735
|
+
print("REPLAY COMPARISON RESULTS")
|
|
736
|
+
print("=" * 80)
|
|
737
|
+
|
|
738
|
+
# Add video details from the log data
|
|
739
|
+
width = log_data['args'].get('width', 'unknown')
|
|
740
|
+
height = log_data['args'].get('height', 'unknown')
|
|
741
|
+
codec = log_data['args'].get('codec', 'unknown')
|
|
742
|
+
duration = log_data['args'].get('duration', 'unknown')
|
|
743
|
+
gop_size = log_data['args'].get('gop_size', 'unknown')
|
|
744
|
+
num_segments = len(clip_start_end)
|
|
745
|
+
|
|
746
|
+
print(f"Video Resolution: {width}x{height}")
|
|
747
|
+
print(f"Codec: {codec}")
|
|
748
|
+
print(f"Duration: {duration} seconds")
|
|
749
|
+
print(f"Number of Segments: {num_segments}")
|
|
750
|
+
print(f"GOP Size: {gop_size}")
|
|
751
|
+
print()
|
|
752
|
+
|
|
753
|
+
print(f"{'Mode':<30} {'Time (s)':<15} {'Throughput (fps)':<20}")
|
|
754
|
+
print("-" * 65)
|
|
755
|
+
|
|
756
|
+
for usage, data in sorted(results.items()):
|
|
757
|
+
print(f"{data['mode']:<30} {data['time']:<15.2f} {data['throughput']:<20.2f}")
|
|
758
|
+
|
|
759
|
+
# Find fastest method
|
|
760
|
+
if results:
|
|
761
|
+
fastest = min(results.items(), key=lambda x: x[1]['time'])
|
|
762
|
+
highest_throughput = max(results.items(), key=lambda x: x[1]['throughput'])
|
|
763
|
+
|
|
764
|
+
print("\nResults Summary:")
|
|
765
|
+
print(f"Total frames transcoded: {segment_frames:.0f}")
|
|
766
|
+
print(f"Fastest method: {results[fastest[0]]['mode']} ({fastest[1]['time']:.2f}s)")
|
|
767
|
+
print(f"Highest throughput: {results[highest_throughput[0]]['mode']} ({highest_throughput[1]['throughput']:.2f} fps)")
|
|
768
|
+
|
|
769
|
+
# Log summary with additional details
|
|
770
|
+
log_step("summary", "Transcoding summary",
|
|
771
|
+
results={
|
|
772
|
+
"resolution": f"{width}x{height}",
|
|
773
|
+
"codec": codec,
|
|
774
|
+
"duration": duration,
|
|
775
|
+
"num_segments": num_segments,
|
|
776
|
+
"gop_size": gop_size,
|
|
777
|
+
"total_frames": segment_frames,
|
|
778
|
+
"fastest_method": results[fastest[0]]['mode'],
|
|
779
|
+
"fastest_time": fastest[1]['time'],
|
|
780
|
+
"highest_throughput_method": results[highest_throughput[0]]['mode'],
|
|
781
|
+
"highest_throughput": highest_throughput[1]['throughput']
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
print("=" * 80)
|
|
785
|
+
|
|
786
|
+
# Compare with original results if available
|
|
787
|
+
if 'results' in log_data and log_data['results']:
|
|
788
|
+
print("\n" + "=" * 80)
|
|
789
|
+
print("COMPARISON WITH ORIGINAL RUN")
|
|
790
|
+
print("=" * 80)
|
|
791
|
+
|
|
792
|
+
print(f"{'Mode':<30} {'Original Time':<15} {'Replay Time':<15} {'Original FPS':<15} {'Replay FPS':<15}")
|
|
793
|
+
print("-" * 90)
|
|
794
|
+
|
|
795
|
+
for usage, data in sorted(results.items()):
|
|
796
|
+
if str(usage) in log_data['results']:
|
|
797
|
+
orig_data = log_data['results'][str(usage)]
|
|
798
|
+
print(f"{data['mode']:<30} {orig_data['time']:<15.2f} {data['time']:<15.2f} {orig_data['throughput']:<15.2f} {data['throughput']:<15.2f}")
|
|
799
|
+
|
|
800
|
+
print("=" * 90)
|
|
801
|
+
|
|
802
|
+
except Exception as e:
|
|
803
|
+
print(f"Error during replay: {e}")
|
|
804
|
+
raise
|
|
805
|
+
|
|
806
|
+
def split_list(n, clip_start_end):
|
|
807
|
+
k, m = divmod(len(clip_start_end), n)
|
|
808
|
+
result = []
|
|
809
|
+
start = 0
|
|
810
|
+
for i in range(n):
|
|
811
|
+
end = start + k + (1 if i < m else 0)
|
|
812
|
+
result.append(clip_start_end[start:end])
|
|
813
|
+
start = end
|
|
814
|
+
return result
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def parse_args():
|
|
818
|
+
"""Parse command-line arguments."""
|
|
819
|
+
parser = argparse.ArgumentParser(description="Benchmark segmented transcoding approaches")
|
|
820
|
+
parser.add_argument("-g", "--gpuid", type=int, default=0, help="GPU ID")
|
|
821
|
+
parser.add_argument("-W", "--width", type=int, default=1920, help="Video width")
|
|
822
|
+
parser.add_argument("-H", "--height", type=int, default=1080, help="Video height")
|
|
823
|
+
parser.add_argument("-d", "--duration", type=int, default=10, help="Video duration in seconds")
|
|
824
|
+
parser.add_argument("-fps", "--fps", type=int, default=30, help="Frames per second")
|
|
825
|
+
parser.add_argument("-p", "--preset", type=str, default="P4", help="Encoding preset (P1-P7)")
|
|
826
|
+
parser.add_argument("-ic", "--input-codec", type=str, default="h264",
|
|
827
|
+
choices=["h264", "hevc", "av1"],
|
|
828
|
+
help="Input codec to use for generating the video")
|
|
829
|
+
parser.add_argument("-c", "--codec", type=str, default="h264",
|
|
830
|
+
choices=["h264", "hevc", "av1"],
|
|
831
|
+
help="Output codec to use for transcoding")
|
|
832
|
+
parser.add_argument("-ibf", "--input-bframes", type=int, default=2,
|
|
833
|
+
help="Number of B-frames for input video generation")
|
|
834
|
+
parser.add_argument("-bf", "--bframes", type=int, default=0,
|
|
835
|
+
help="Number of B-frames for output transcoding")
|
|
836
|
+
parser.add_argument("-n", "--numthreads", type=int, default=1, help="Number of threads")
|
|
837
|
+
parser.add_argument("-s", "--segments", type=int, default=3, help="Number of random segments")
|
|
838
|
+
parser.add_argument("-u", "--usage", type=int, action="append", default=[],
|
|
839
|
+
help="Transcoding method (0: PyNVC, 1: FFmpeg without map, 2: FFmpeg stream-aware, 3: FFmpeg frame-by-frame)")
|
|
840
|
+
parser.add_argument("--chunk-size", type=int, default=5, help="Number of segments to process in a single FFmpeg command")
|
|
841
|
+
parser.add_argument("--segment-duration", type=int, default=5, help="Duration of each segment in seconds")
|
|
842
|
+
parser.add_argument("--log", type=str, help="Path to save log file")
|
|
843
|
+
parser.add_argument("--random-seed", type=int, help="Random seed for reproducible segment generation")
|
|
844
|
+
parser.add_argument("--gop-size", type=int, default=250, help="GOP size for encoding")
|
|
845
|
+
parser.add_argument("-i", "--input", type=str, help="Input video file (instead of generating Mandelbrot)")
|
|
846
|
+
parser.add_argument("--replay", type=str, help="Path to log file for replay")
|
|
847
|
+
|
|
848
|
+
args = parser.parse_args()
|
|
849
|
+
|
|
850
|
+
# Set default usages if none provided
|
|
851
|
+
if not args.usage:
|
|
852
|
+
args.usage = [0, 1, 2, 3]
|
|
853
|
+
|
|
854
|
+
return args
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
if __name__ == "__main__":
|
|
858
|
+
args = parse_args()
|
|
859
|
+
|
|
860
|
+
# Create logs directory if it doesn't exist
|
|
861
|
+
os.makedirs("logs", exist_ok=True)
|
|
862
|
+
|
|
863
|
+
# Generate default log filename if not specified
|
|
864
|
+
if not args.log and not args.replay:
|
|
865
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
866
|
+
args.log = f"logs/run_{timestamp}.json"
|
|
867
|
+
|
|
868
|
+
# Initialize log data
|
|
869
|
+
log_data["timestamp"] = datetime.datetime.now().isoformat()
|
|
870
|
+
log_data["args"] = vars(args)
|
|
871
|
+
|
|
872
|
+
# Replay mode
|
|
873
|
+
if args.replay:
|
|
874
|
+
# Open log file in append mode to add replay results
|
|
875
|
+
replay_log_path = f"logs/replay_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
876
|
+
with open(replay_log_path, 'w') as log_file:
|
|
877
|
+
log_data = {
|
|
878
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
879
|
+
"replay_source": args.replay,
|
|
880
|
+
"steps": [],
|
|
881
|
+
"results": {}
|
|
882
|
+
}
|
|
883
|
+
json.dump(log_data, log_file, indent=2)
|
|
884
|
+
replay_from_log(args.replay)
|
|
885
|
+
else:
|
|
886
|
+
# Normal execution mode
|
|
887
|
+
# Open log file
|
|
888
|
+
log_file = open(args.log, 'w')
|
|
889
|
+
json.dump(log_data, log_file, indent=2)
|
|
890
|
+
|
|
891
|
+
print(f"Logging to: {args.log}")
|
|
892
|
+
|
|
893
|
+
# In the video generation part
|
|
894
|
+
if not args.input:
|
|
895
|
+
# Generate the Mandelbrot video with the specified input codec
|
|
896
|
+
video_path = "source_videos/mandelbrot.mp4"
|
|
897
|
+
os.makedirs("source_videos", exist_ok=True)
|
|
898
|
+
|
|
899
|
+
print("=" * 80)
|
|
900
|
+
print(f"STEP 1: GENERATING MANDELBROT VIDEO WITH {args.input_codec.upper()} CODEC")
|
|
901
|
+
print(f"Using {args.input_bframes} B-frames for input video generation")
|
|
902
|
+
print("=" * 80)
|
|
903
|
+
|
|
904
|
+
video_path = generate_mandelbrot_video(
|
|
905
|
+
args.width, args.height, args.duration, args.fps,
|
|
906
|
+
video_path, args.gop_size, args.input_codec, args.input_bframes
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Configure encoding parameters based on output codec
|
|
910
|
+
config = {
|
|
911
|
+
"preset": args.preset.upper(),
|
|
912
|
+
"width": args.width,
|
|
913
|
+
"height": args.height,
|
|
914
|
+
"codec": args.codec.lower(), # This is the output codec
|
|
915
|
+
"tuning_info": "high_quality",
|
|
916
|
+
"bf": str(args.bframes), # Use the bframes parameter from args
|
|
917
|
+
"gop": str(args.gop_size)
|
|
918
|
+
}
|
|
919
|
+
log_data["args"]["config"] = config
|
|
920
|
+
|
|
921
|
+
# Set random seed if provided for reproducible segment generation
|
|
922
|
+
if args.random_seed is not None:
|
|
923
|
+
random.seed(args.random_seed)
|
|
924
|
+
log_step("initialization", f"Set random seed to {args.random_seed}")
|
|
925
|
+
|
|
926
|
+
# Initialize CUDA
|
|
927
|
+
cuda.init()
|
|
928
|
+
log_step("initialization", "Initialized CUDA")
|
|
929
|
+
|
|
930
|
+
# Create random segments
|
|
931
|
+
print("\n" + "=" * 80)
|
|
932
|
+
print("STEP 2: CREATING RANDOM SEGMENTS")
|
|
933
|
+
print("=" * 80)
|
|
934
|
+
|
|
935
|
+
clip_start_end = generate_random_segments(args.duration, args.segments, min_segment_duration=args.segment_duration)
|
|
936
|
+
|
|
937
|
+
# Log segment creation
|
|
938
|
+
segment_data = [{"start": start, "end": end} for start, end in clip_start_end]
|
|
939
|
+
log_step("segment_creation", f"Created {len(clip_start_end)} random segments",
|
|
940
|
+
params={"num_segments": args.segments, "min_duration": 1.0, "duration": args.duration},
|
|
941
|
+
results={"segments": segment_data})
|
|
942
|
+
|
|
943
|
+
# Print segments
|
|
944
|
+
print(f"Created {len(clip_start_end)} random segments:")
|
|
945
|
+
for i, (start, end) in enumerate(clip_start_end):
|
|
946
|
+
print(f" Segment {i+1}: {start:.2f}s - {end:.2f}s (duration: {end-start:.2f}s)")
|
|
947
|
+
|
|
948
|
+
# Calculate total frames to transcode
|
|
949
|
+
segment_frames = sum((end - start) * args.fps for start, end in clip_start_end)
|
|
950
|
+
|
|
951
|
+
# Store results for comparison
|
|
952
|
+
results = {}
|
|
953
|
+
|
|
954
|
+
# Run transcoding for each usage mode
|
|
955
|
+
for usage in args.usage:
|
|
956
|
+
if usage not in [0, 1, 2, 3]:
|
|
957
|
+
print(f"Warning: Invalid usage mode {usage}. Skipping...")
|
|
958
|
+
log_step("warning", f"Invalid usage mode {usage}, skipping")
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
print("\n" + "=" * 80)
|
|
962
|
+
print(f"STEP 3: TRANSCODING SEGMENTS USING MODE {usage}")
|
|
963
|
+
print("=" * 80)
|
|
964
|
+
|
|
965
|
+
# Get mode description
|
|
966
|
+
mode_desc = {
|
|
967
|
+
0: "PyNVC transcoding",
|
|
968
|
+
1: "FFmpeg without map",
|
|
969
|
+
2: "FFmpeg with map (no audio)",
|
|
970
|
+
3: "FFmpeg with map (with audio)"
|
|
971
|
+
}.get(usage, "Unknown mode")
|
|
972
|
+
|
|
973
|
+
print(f"Running: {mode_desc}")
|
|
974
|
+
|
|
975
|
+
# Log transcoding start
|
|
976
|
+
log_step("transcoding", f"Starting transcoding with mode {usage} ({mode_desc})",
|
|
977
|
+
params={"usage": usage, "mode": mode_desc, "numthreads": args.numthreads,
|
|
978
|
+
"chunk_size": args.chunk_size, "video_path": video_path})
|
|
979
|
+
clip_chunks = split_list(args.numthreads, clip_start_end)
|
|
980
|
+
threads = []
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
starttime = time.time()
|
|
984
|
+
|
|
985
|
+
for m in range(0, args.numthreads):
|
|
986
|
+
t = threading.Thread(target=segmented_transcode, args=([video_path,
|
|
987
|
+
clip_chunks[m],
|
|
988
|
+
config,
|
|
989
|
+
args.fps,
|
|
990
|
+
usage,
|
|
991
|
+
args.chunk_size],))
|
|
992
|
+
t.start()
|
|
993
|
+
#
|
|
994
|
+
threads.append(t)
|
|
995
|
+
|
|
996
|
+
for t in threads:
|
|
997
|
+
t.join()
|
|
998
|
+
|
|
999
|
+
endtime = time.time()
|
|
1000
|
+
total_time = endtime - starttime
|
|
1001
|
+
throughput = segment_frames / total_time if total_time > 0 else 0
|
|
1002
|
+
|
|
1003
|
+
# Store results
|
|
1004
|
+
results[usage] = {
|
|
1005
|
+
'mode': mode_desc,
|
|
1006
|
+
'time': total_time,
|
|
1007
|
+
'throughput': throughput
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
# Log transcoding completion
|
|
1011
|
+
log_step("transcoding_complete", f"Completed transcoding with mode {usage}",
|
|
1012
|
+
params={"usage": usage, "mode": mode_desc},
|
|
1013
|
+
results={"time": total_time, "throughput": throughput})
|
|
1014
|
+
|
|
1015
|
+
print(f"Completed {mode_desc}")
|
|
1016
|
+
print(f"Processing time: {total_time:.2f} seconds")
|
|
1017
|
+
print(f"Throughput: {throughput:.2f} frames/second")
|
|
1018
|
+
|
|
1019
|
+
# Store results in log data
|
|
1020
|
+
log_data["results"] = {str(usage): {"time": data["time"], "throughput": data["throughput"]}
|
|
1021
|
+
for usage, data in results.items()}
|
|
1022
|
+
|
|
1023
|
+
# Print comparison results with additional details
|
|
1024
|
+
print("\n" + "=" * 80)
|
|
1025
|
+
print("COMPARISON RESULTS")
|
|
1026
|
+
print("=" * 80)
|
|
1027
|
+
|
|
1028
|
+
# Add video details above the table
|
|
1029
|
+
print(f"Video Resolution: {args.width}x{args.height}")
|
|
1030
|
+
print(f"Codec: {args.codec}")
|
|
1031
|
+
print(f"Duration: {args.duration} seconds")
|
|
1032
|
+
print(f"Number of Segments: {len(clip_start_end)}")
|
|
1033
|
+
print(f"GOP Size: {args.gop_size}")
|
|
1034
|
+
print()
|
|
1035
|
+
|
|
1036
|
+
print(f"{'Mode':<30} {'Time (s)':<15} {'Throughput (fps)':<20}")
|
|
1037
|
+
print("-" * 65)
|
|
1038
|
+
|
|
1039
|
+
for usage, data in sorted(results.items()):
|
|
1040
|
+
print(f"{data['mode']:<30} {data['time']:<15.2f} {data['throughput']:<20.2f}")
|
|
1041
|
+
|
|
1042
|
+
# Find fastest method
|
|
1043
|
+
if results:
|
|
1044
|
+
fastest = min(results.items(), key=lambda x: x[1]['time'])
|
|
1045
|
+
highest_throughput = max(results.items(), key=lambda x: x[1]['throughput'])
|
|
1046
|
+
|
|
1047
|
+
print("\nResults Summary:")
|
|
1048
|
+
print(f"Total frames transcoded: {segment_frames:.0f}")
|
|
1049
|
+
print(f"Fastest method: {results[fastest[0]]['mode']} ({fastest[1]['time']:.2f}s)")
|
|
1050
|
+
print(f"Highest throughput: {results[highest_throughput[0]]['mode']} ({highest_throughput[1]['throughput']:.2f} fps)")
|
|
1051
|
+
|
|
1052
|
+
# Log summary with additional details
|
|
1053
|
+
log_step("summary", "Transcoding summary",
|
|
1054
|
+
results={
|
|
1055
|
+
"resolution": f"{args.width}x{args.height}",
|
|
1056
|
+
"codec": args.codec,
|
|
1057
|
+
"duration": args.duration,
|
|
1058
|
+
"num_segments": len(clip_start_end),
|
|
1059
|
+
"gop_size": args.gop_size,
|
|
1060
|
+
"total_frames": segment_frames,
|
|
1061
|
+
"fastest_method": results[fastest[0]]['mode'],
|
|
1062
|
+
"fastest_time": fastest[1]['time'],
|
|
1063
|
+
"highest_throughput_method": results[highest_throughput[0]]['mode'],
|
|
1064
|
+
"highest_throughput": highest_throughput[1]['throughput']
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
print("=" * 80)
|
|
1068
|
+
|
|
1069
|
+
# Close log file
|
|
1070
|
+
if log_file:
|
|
1071
|
+
log_file.close()
|
|
1072
|
+
print(f"Log saved to: {args.log}")
|