PyNvVideoCodec 2.2.1__cp314-cp314-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyNvVideoCodec/PyNvVideoCodec_121.cp314-win_arm64.pyd +0 -0
- PyNvVideoCodec/PyNvVideoCodec_130.cp314-win_arm64.pyd +0 -0
- PyNvVideoCodec/VersionCheck.cp314-win_arm64.pyd +0 -0
- PyNvVideoCodec/__init__.py +251 -0
- PyNvVideoCodec/avformat-62.dll +0 -0
- PyNvVideoCodec/avutil-60.dll +0 -0
- PyNvVideoCodec/decoders/SimpleDecoder.py +273 -0
- PyNvVideoCodec/decoders/ThreadedDecoder.py +129 -0
- PyNvVideoCodec/decoders/__init__.py +22 -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 +52 -0
- benchmarks/segmented_transcode_benchmark.py +1546 -0
- benchmarks/setup_pycuda_win_arm64.bat +531 -0
- pynvvideocodec-2.2.1.data/data/external/ffmpeg/readme.txt +39 -0
- pynvvideocodec-2.2.1.data/data/external/ffmpeg/src/ffmpeg-8.1.2.tar.xz +0 -0
- pynvvideocodec-2.2.1.dist-info/METADATA +97 -0
- pynvvideocodec-2.2.1.dist-info/RECORD +63 -0
- pynvvideocodec-2.2.1.dist-info/WHEEL +5 -0
- pynvvideocodec-2.2.1.dist-info/licenses/LICENSE.txt +22 -0
- pynvvideocodec-2.2.1.dist-info/licenses/NOTICES.txt +167 -0
- pynvvideocodec-2.2.1.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 +12 -0
- samples/advanced/encode_config_lowlatency.json +13 -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_from_memory.py +151 -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 +574 -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/face_protect_tutorial.ipynb +881 -0
- samples/jupyter/object_detection_tutorial.ipynb +781 -0
- samples/jupyter/simple_decode_tutorial.ipynb +674 -0
- samples/requirements.txt +84 -0
- samples/setup_pycuda_win_arm64.bat +531 -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 +379 -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,1546 @@
|
|
|
1
|
+
# This copyright notice applies to this file only
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
8
|
+
# to deal in the Software without restriction, including without limitation
|
|
9
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
10
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
11
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
|
14
|
+
# all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
19
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
22
|
+
# DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import os
|
|
26
|
+
import platform
|
|
27
|
+
import subprocess
|
|
28
|
+
import time
|
|
29
|
+
import random
|
|
30
|
+
import json
|
|
31
|
+
import datetime
|
|
32
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
33
|
+
from concurrent.futures import wait
|
|
34
|
+
import PyNvVideoCodec as nvc
|
|
35
|
+
from typing import Optional, Dict
|
|
36
|
+
|
|
37
|
+
import pycuda.driver as cuda
|
|
38
|
+
import threading
|
|
39
|
+
from tqdm import tqdm
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
Benchmark script for comparing segmented video transcoding performance.
|
|
44
|
+
|
|
45
|
+
This script generates (or reuses) a Mandelbrot set video with FFmpeg and benchmarks
|
|
46
|
+
transcoding performance using multiple approaches: PyNvVideoCodec and various FFmpeg
|
|
47
|
+
configurations. It supports parallel processing with multiple threads.
|
|
48
|
+
|
|
49
|
+
Features:
|
|
50
|
+
- Supports multiple video codecs (H.264, HEVC, AV1)
|
|
51
|
+
- Tests different thread counts for parallel processing
|
|
52
|
+
- Generates performance comparison results
|
|
53
|
+
- Saves results in JSON format with execution logs
|
|
54
|
+
- Automatically reuses previously generated videos
|
|
55
|
+
|
|
56
|
+
NOTE 1: Videos are generated using FFmpeg only for the first run. Subsequent runs will
|
|
57
|
+
reuse existing videos if they match the parameters (codec, resolution, duration, etc.).
|
|
58
|
+
|
|
59
|
+
NOTE 2: Generated videos are stored in the source_videos/ directory with names that
|
|
60
|
+
include codec and resolution information.
|
|
61
|
+
|
|
62
|
+
NOTE 3: The script will run transcoding in multiple threads if specified. Number of
|
|
63
|
+
threads is controlled by the --numthreads argument.
|
|
64
|
+
|
|
65
|
+
NOTE 4: Execution logs are automatically saved to track all steps including video
|
|
66
|
+
generation/reuse, segment creation, and transcoding results. Default location is
|
|
67
|
+
logs/run_{timestamp}.json (e.g., logs/run_20240615_123045.json). These logs can be
|
|
68
|
+
replayed using the --replay flag to re-run transcoding with the same parameters.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
-g, --gpuid (int): GPU device ID to use (default: 0)
|
|
72
|
+
-W, --width (int): Width of the video in pixels (default: 1920)
|
|
73
|
+
-H, --height (int): Height of the video in pixels (default: 1080)
|
|
74
|
+
-d, --duration (int): Duration of the video in seconds (default: 10)
|
|
75
|
+
-fps, --fps (int): Frames per second (default: 30)
|
|
76
|
+
-p, --preset (str): Encoder preset P1-P7 (default: P4)
|
|
77
|
+
-ic, --input-codec (str): Input codec for video generation: h264, hevc, av1 (default: h264)
|
|
78
|
+
-c, --codec (str): Output codec for transcoding: h264, hevc, av1 (default: h264)
|
|
79
|
+
-ibf, --input-bframes (int): Number of B-frames for input video generation (default: 2)
|
|
80
|
+
-bf, --bframes (int): Number of B-frames for output transcoding (default: 0)
|
|
81
|
+
-n, --numthreads (int): Number of concurrent threads for parallel processing (default: 1)
|
|
82
|
+
-s, --segments (int): Number of random segments to create (default: 3)
|
|
83
|
+
-u, --usage (list[int]): Transcoding modes to benchmark (default: [0, 1, 2, 3])
|
|
84
|
+
See "Transcoding Modes" section below for detailed explanation.
|
|
85
|
+
--chunk-size (int): Number of segments per FFmpeg batch command (default: 5)
|
|
86
|
+
--segment-duration (int): Minimum duration of each segment in seconds (default: 1)
|
|
87
|
+
--gop-size (int): GOP size for video encoding (default: 250)
|
|
88
|
+
--log (str): Path to save execution log (default: logs/run_{timestamp}.json)
|
|
89
|
+
--random-seed (int): Random seed for reproducible segment generation (optional)
|
|
90
|
+
-i, --input (str): Use existing video file instead of generating new one (optional)
|
|
91
|
+
--ffmpeg-dir (str): Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows). Required.
|
|
92
|
+
--replay (str): Replay transcoding from a previous log file (optional)
|
|
93
|
+
|
|
94
|
+
Transcoding Modes:
|
|
95
|
+
The script benchmarks 4 different approaches to segmented video transcoding:
|
|
96
|
+
|
|
97
|
+
Mode 0 - PyNVC Transcoding:
|
|
98
|
+
Uses PyNvVideoCodec's Transcoder class with segmented_transcode() method.
|
|
99
|
+
- One output file per segment
|
|
100
|
+
- GPU-accelerated decode and encode pipeline
|
|
101
|
+
- Output: pynvc_out/{base}_{codec}_{resolution}_bf{bframes}_transcoded_{start}_{end}.mp4
|
|
102
|
+
Example: pynvc_out/mandelbrot_h264_1920x1080_bf0_transcoded_1.75_5.74.mp4
|
|
103
|
+
|
|
104
|
+
Mode 1 - FFmpeg Without Map (Sequential Processing):
|
|
105
|
+
Uses FFmpeg with separate commands for each segment.
|
|
106
|
+
- Individual FFmpeg process per segment (-ss/-to for time ranges)
|
|
107
|
+
- Hardware-accelerated decode (cuvid) and encode (nvenc)
|
|
108
|
+
- Simple approach but spawns multiple processes
|
|
109
|
+
- Output: ffmpeg_out/{base}_{codec}_{resolution}_bf{bframes}_{start}_{end}.mp4
|
|
110
|
+
Example: ffmpeg_out/mandelbrot_h264_1920x1080_bf0_1.75_5.74.mp4
|
|
111
|
+
Command example:
|
|
112
|
+
ffmpeg -ss START -to END -c:v h264_cuvid -i INPUT \
|
|
113
|
+
-c:v h264_nvenc -c:a copy OUTPUT
|
|
114
|
+
|
|
115
|
+
Mode 2 - FFmpeg With Map (No Audio, Batch Processing):
|
|
116
|
+
Uses FFmpeg's filter_complex to process multiple segments in one command.
|
|
117
|
+
- Single FFmpeg process handles multiple segments simultaneously
|
|
118
|
+
- Hardware-accelerated with CUDA
|
|
119
|
+
- Uses trim filters for precise segment extraction
|
|
120
|
+
- Video only (no audio processing)
|
|
121
|
+
- Output: ffmpeg_fc_out/{base}_{codec}_{resolution}_bf{bframes}_{start}_{end}.mp4
|
|
122
|
+
Example: ffmpeg_fc_out/mandelbrot_h264_1920x1080_bf0_1.75_5.74.mp4
|
|
123
|
+
Command example:
|
|
124
|
+
ffmpeg -hwaccel cuda -c:v h264_cuvid -i INPUT \
|
|
125
|
+
-filter_complex "[0:v]trim=start=S1:end=E1[v0];..." \
|
|
126
|
+
-map [v0] -c:v h264_nvenc -an OUT1 \
|
|
127
|
+
-map [v1] -c:v h264_nvenc -an OUT2
|
|
128
|
+
|
|
129
|
+
Mode 3 - FFmpeg With Map (With Audio, Batch Processing):
|
|
130
|
+
Enhanced version of Mode 2 with audio support.
|
|
131
|
+
- Single FFmpeg process for multiple segments
|
|
132
|
+
- Processes both video (trim) and audio (atrim) streams
|
|
133
|
+
- Hardware-accelerated video processing
|
|
134
|
+
- Full quality audio transcoding (AAC)
|
|
135
|
+
- Most complete but potentially slower due to audio processing
|
|
136
|
+
- Output: ffmpeg_fc_out/{base}_{codec}_{resolution}_bf{bframes}_{start}_{end}.mp4
|
|
137
|
+
Example: ffmpeg_fc_out/mandelbrot_h264_1920x1080_bf0_1.75_5.74.mp4
|
|
138
|
+
Command example:
|
|
139
|
+
ffmpeg -hwaccel cuda -c:v h264_cuvid -i INPUT \
|
|
140
|
+
-filter_complex "[0:v]trim=start=S1:end=E1[v0]; \
|
|
141
|
+
[0:a]atrim=start=S1:end=E1[a0];..." \
|
|
142
|
+
-map [v0] -map [a0] -c:v h264_nvenc -c:a aac OUT1 \
|
|
143
|
+
-map [v1] -map [a1] -c:v h264_nvenc -c:a aac OUT2
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
None. Results are printed to console and saved to log file.
|
|
147
|
+
|
|
148
|
+
Logging:
|
|
149
|
+
The script automatically logs all execution steps to a JSON file for tracking and replay:
|
|
150
|
+
|
|
151
|
+
Log Structure:
|
|
152
|
+
- timestamp: ISO format timestamp of when the script was run
|
|
153
|
+
- args: All command-line arguments used
|
|
154
|
+
- steps: Detailed list of execution steps:
|
|
155
|
+
* video_generation: When a new video is generated
|
|
156
|
+
* video_reuse: When an existing video is reused
|
|
157
|
+
* video_input: When user provides input video with -i flag
|
|
158
|
+
* segment_creation: Random segments generated for transcoding
|
|
159
|
+
* transcoding: Each transcoding operation with its parameters
|
|
160
|
+
* summary: Final results with performance metrics
|
|
161
|
+
- results: Performance comparison data (time, throughput, FPS)
|
|
162
|
+
|
|
163
|
+
Default Log Location:
|
|
164
|
+
- Normal runs: logs/run_{timestamp}.json
|
|
165
|
+
- Replay runs: logs/replay_{timestamp}.json
|
|
166
|
+
- Custom path: Specify with --log argument
|
|
167
|
+
|
|
168
|
+
All logs are stored in the logs/ directory (automatically created if not present).
|
|
169
|
+
|
|
170
|
+
Output Files:
|
|
171
|
+
- Transcoded video segments (one file per segment):
|
|
172
|
+
* pynvc_out/{base}_{codec}_{resolution}_bf{bframes}_transcoded.mp4
|
|
173
|
+
* ffmpeg_out/{base}_{codec}_{resolution}_bf{bframes}_{start}_{end}.mp4
|
|
174
|
+
* ffmpeg_fc_out/{base}_{codec}_{resolution}_bf{bframes}_{start}_{end}.mp4
|
|
175
|
+
- Source videos: source_videos/mandelbrot_{codec}_{resolution}.mp4
|
|
176
|
+
|
|
177
|
+
Where:
|
|
178
|
+
{base} = base filename (e.g., mandelbrot)
|
|
179
|
+
{codec} = output codec (h264, hevc, av1)
|
|
180
|
+
{resolution} = video resolution (e.g., 1920x1080)
|
|
181
|
+
{bframes} = number of B-frames
|
|
182
|
+
{start}_{end} = segment timestamps (e.g., 1.75_5.74)
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
Basic usage with default parameters (runs all 4 modes):
|
|
186
|
+
>>> python segmented_transcode_benchmark.py --ffmpeg-dir /path/to/ffmpeg
|
|
187
|
+
|
|
188
|
+
Compare only PyNVC vs FFmpeg with audio:
|
|
189
|
+
>>> python segmented_transcode_benchmark.py -u 0 3
|
|
190
|
+
|
|
191
|
+
Test only PyNVC transcoding:
|
|
192
|
+
>>> python segmented_transcode_benchmark.py -u 0
|
|
193
|
+
|
|
194
|
+
Compare all FFmpeg modes (without PyNVC):
|
|
195
|
+
>>> python segmented_transcode_benchmark.py -u 1 2 3
|
|
196
|
+
|
|
197
|
+
Custom video parameters with specific mode:
|
|
198
|
+
>>> python segmented_transcode_benchmark.py -W 1920 -H 1080 -d 30 -fps 30 -p P4 -c h264 -n 4 -s 10 -u 0
|
|
199
|
+
|
|
200
|
+
Using HEVC codec with B-frames:
|
|
201
|
+
>>> python segmented_transcode_benchmark.py -ic hevc -c hevc -ibf 2 -bf 2
|
|
202
|
+
|
|
203
|
+
Save log to custom location:
|
|
204
|
+
>>> python segmented_transcode_benchmark.py --log my_benchmark_results.json
|
|
205
|
+
|
|
206
|
+
Replay from previous log:
|
|
207
|
+
>>> python segmented_transcode_benchmark.py --ffmpeg-dir /path/to/ffmpeg --replay logs/run_20240615_123045.json
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
# Global log file handle
|
|
211
|
+
log_file = None
|
|
212
|
+
log_data = {
|
|
213
|
+
"timestamp": "",
|
|
214
|
+
"args": {},
|
|
215
|
+
"steps": [],
|
|
216
|
+
"results": {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
def get_ffmpeg_env(ffmpeg_path):
|
|
220
|
+
"""
|
|
221
|
+
Build environment so FFmpeg/FFprobe can find shared libs when run by path.
|
|
222
|
+
- Windows: prepend bin dir to PATH.
|
|
223
|
+
- Unix: if path is .../bin/ffmpeg, add install_dir/lib to LD_LIBRARY_PATH.
|
|
224
|
+
"""
|
|
225
|
+
env = os.environ.copy()
|
|
226
|
+
if not os.path.isfile(ffmpeg_path):
|
|
227
|
+
return env
|
|
228
|
+
ffmpeg_dir = os.path.dirname(ffmpeg_path)
|
|
229
|
+
if platform.system() == "Windows":
|
|
230
|
+
env["PATH"] = ffmpeg_dir + os.pathsep + env.get("PATH", "")
|
|
231
|
+
else:
|
|
232
|
+
if os.path.sep + "bin" + os.path.sep in ffmpeg_path:
|
|
233
|
+
install_dir = os.path.dirname(ffmpeg_dir)
|
|
234
|
+
lib_dir = os.path.join(install_dir, "lib")
|
|
235
|
+
if os.path.isdir(lib_dir):
|
|
236
|
+
env["LD_LIBRARY_PATH"] = lib_dir + (os.pathsep + env.get("LD_LIBRARY_PATH", ""))
|
|
237
|
+
return env
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def parse_ffmpeg_progress(line: str) -> Optional[Dict[str, str]]:
|
|
241
|
+
"""Parse FFmpeg progress line and extract key information"""
|
|
242
|
+
if not line.strip():
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
progress_data = {}
|
|
246
|
+
|
|
247
|
+
if '=' in line:
|
|
248
|
+
for item in line.split():
|
|
249
|
+
if '=' in item:
|
|
250
|
+
key, value = item.split('=', 1)
|
|
251
|
+
progress_data[key] = value
|
|
252
|
+
|
|
253
|
+
return progress_data if progress_data else None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def log_step(step_type, description, params=None, results=None):
|
|
257
|
+
"""
|
|
258
|
+
Log a step to both console and log file
|
|
259
|
+
|
|
260
|
+
Parameters:
|
|
261
|
+
step_type (str): Type of step (video_generation, segment_creation, transcoding)
|
|
262
|
+
description (str): Human-readable description
|
|
263
|
+
params (dict): Parameters used for this step
|
|
264
|
+
results (dict): Results from this step
|
|
265
|
+
"""
|
|
266
|
+
global log_file, log_data
|
|
267
|
+
|
|
268
|
+
step = {
|
|
269
|
+
"type": step_type,
|
|
270
|
+
"description": description,
|
|
271
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
272
|
+
"params": params or {},
|
|
273
|
+
"results": results or {}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Add to log data
|
|
277
|
+
log_data["steps"].append(step)
|
|
278
|
+
|
|
279
|
+
# Print to console
|
|
280
|
+
print(f"[LOG] {description}")
|
|
281
|
+
|
|
282
|
+
# Write to log file if open
|
|
283
|
+
if log_file:
|
|
284
|
+
# Update the file with the latest log data
|
|
285
|
+
log_file.seek(0)
|
|
286
|
+
json.dump(log_data, log_file, indent=2)
|
|
287
|
+
log_file.truncate()
|
|
288
|
+
log_file.flush()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def generate_mandelbrot_video(width, height, duration, fps, output_path, gop_size, input_codec="h264", input_bframes=2, ffmpeg_path="ffmpeg"):
|
|
292
|
+
"""
|
|
293
|
+
Generate a Mandelbrot set zoom video with audio using FFmpeg
|
|
294
|
+
|
|
295
|
+
Uses optimized approach: generates a short base clip and loops it for better
|
|
296
|
+
performance with long duration videos.
|
|
297
|
+
|
|
298
|
+
Parameters:
|
|
299
|
+
width (int): Video width
|
|
300
|
+
height (int): Video height
|
|
301
|
+
duration (int): Video duration in seconds
|
|
302
|
+
fps (int): Frames per second
|
|
303
|
+
output_path (str): Path to save the generated video
|
|
304
|
+
gop_size (int): GOP size for encoding
|
|
305
|
+
input_codec (str): Codec to use for video generation (h264, hevc, av1)
|
|
306
|
+
input_bframes (int): Number of B-frames for video generation
|
|
307
|
+
ffmpeg_path (str): Path to the ffmpeg executable (from --ffmpeg-dir).
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
str: Path to the generated video
|
|
311
|
+
"""
|
|
312
|
+
# Map codec to FFmpeg encoder name
|
|
313
|
+
encoder_map = {
|
|
314
|
+
"h264": "h264_nvenc",
|
|
315
|
+
"hevc": "hevc_nvenc",
|
|
316
|
+
"av1": "av1_nvenc"
|
|
317
|
+
}
|
|
318
|
+
encoder = encoder_map.get(input_codec.lower(), "h264_nvenc")
|
|
319
|
+
|
|
320
|
+
# Update output path to include codec information
|
|
321
|
+
base, ext = os.path.splitext(output_path)
|
|
322
|
+
codec_output_path = f"{base}_{input_codec}_{width}x{height}_{duration}s_{fps}fps_gop{gop_size}_bf{input_bframes}{ext}"
|
|
323
|
+
# Log the step
|
|
324
|
+
log_step("video_generation", f"Generating Mandelbrot video ({width}x{height}, {duration}s, {fps} fps, {input_codec}, B-frames: {input_bframes})",
|
|
325
|
+
params={"width": width, "height": height, "duration": duration, "fps": fps,
|
|
326
|
+
"output_path": codec_output_path, "gop_size": gop_size, "codec": input_codec, "bframes": input_bframes})
|
|
327
|
+
|
|
328
|
+
print(f"Generating Mandelbrot set video with audio at {width}x{height}, {duration}s duration, {fps} fps")
|
|
329
|
+
print(f"Using codec: {input_codec}, encoder: {encoder}, B-frames: {input_bframes}")
|
|
330
|
+
|
|
331
|
+
# Create the directory if it doesn't exist
|
|
332
|
+
os.makedirs(os.path.dirname(codec_output_path) or '.', exist_ok=True)
|
|
333
|
+
|
|
334
|
+
print(f"Using GOP size: {gop_size} and {input_bframes} B-frames")
|
|
335
|
+
|
|
336
|
+
# Determine base clip duration (use min of 10 seconds or full duration)
|
|
337
|
+
base_duration = min(10, duration)
|
|
338
|
+
temp_clip = f"{base}_temp_{input_codec}_{width}x{height}_{duration}s_{fps}fps_gop{gop_size}_bf{input_bframes}{ext}"
|
|
339
|
+
|
|
340
|
+
# Step 1: Generate short Mandelbrot base clip if it doesn't exist
|
|
341
|
+
if not os.path.exists(temp_clip):
|
|
342
|
+
print(f"\nStep 1: Generating {base_duration}s Mandelbrot base clip...")
|
|
343
|
+
print("This base clip will be reused for future runs with same parameters")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# FFmpeg command to generate base Mandelbrot clip
|
|
347
|
+
cmd_base = [
|
|
348
|
+
ffmpeg_path, "-y",
|
|
349
|
+
"-progress", "pipe:2",
|
|
350
|
+
"-v", "warning",
|
|
351
|
+
# Video input from mandelbrot filter
|
|
352
|
+
"-f", "lavfi",
|
|
353
|
+
"-i", f"mandelbrot=size={width}x{height}:rate={fps}:start_scale=3:end_scale=0.1",
|
|
354
|
+
# Audio input - sine wave
|
|
355
|
+
"-f", "lavfi",
|
|
356
|
+
"-i", f"sine=frequency=220:sample_rate=44100:duration={base_duration}",
|
|
357
|
+
# Set duration
|
|
358
|
+
"-t", str(base_duration),
|
|
359
|
+
# Video encoding with specified codec
|
|
360
|
+
"-c:v", encoder,
|
|
361
|
+
"-preset", "medium",
|
|
362
|
+
"-g", str(gop_size),
|
|
363
|
+
"-bf", str(input_bframes),
|
|
364
|
+
# Audio encoding
|
|
365
|
+
"-c:a", "aac",
|
|
366
|
+
"-b:a", "128k",
|
|
367
|
+
# Output
|
|
368
|
+
temp_clip
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
# Calculate estimated total frames for progress tracking
|
|
373
|
+
estimated_frames = base_duration * fps
|
|
374
|
+
|
|
375
|
+
# For custom FFmpeg builds, ensure the subprocess finds shared libs
|
|
376
|
+
env = get_ffmpeg_env(ffmpeg_path)
|
|
377
|
+
# Execute FFmpeg command with progress tracking
|
|
378
|
+
print(f"Generating base clip ({base_duration}s)...")
|
|
379
|
+
process = subprocess.Popen(cmd_base, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
380
|
+
universal_newlines=True, bufsize=1, env=env)
|
|
381
|
+
|
|
382
|
+
# Progress bar setup
|
|
383
|
+
pbar = tqdm(total=estimated_frames, unit='frames',
|
|
384
|
+
desc=f"Base clip generation",
|
|
385
|
+
ncols=80, leave=True)
|
|
386
|
+
|
|
387
|
+
# Read stderr line by line to get progress
|
|
388
|
+
current_frame = 0
|
|
389
|
+
max_frame_seen = 0
|
|
390
|
+
last_update_time = time.time()
|
|
391
|
+
update_interval = 0.5
|
|
392
|
+
|
|
393
|
+
while True:
|
|
394
|
+
line = process.stderr.readline()
|
|
395
|
+
if not line:
|
|
396
|
+
break
|
|
397
|
+
|
|
398
|
+
# Parse progress information
|
|
399
|
+
progress_data = parse_ffmpeg_progress(line.strip())
|
|
400
|
+
if progress_data and 'frame' in progress_data:
|
|
401
|
+
try:
|
|
402
|
+
frame_num = int(progress_data['frame'])
|
|
403
|
+
max_frame_seen = max(max_frame_seen, frame_num)
|
|
404
|
+
|
|
405
|
+
current_time = time.time()
|
|
406
|
+
if (current_time - last_update_time >= update_interval or
|
|
407
|
+
frame_num - current_frame >= 10):
|
|
408
|
+
|
|
409
|
+
if frame_num > estimated_frames:
|
|
410
|
+
pbar.total = frame_num + 50
|
|
411
|
+
|
|
412
|
+
if frame_num > current_frame:
|
|
413
|
+
pbar.update(frame_num - current_frame)
|
|
414
|
+
current_frame = frame_num
|
|
415
|
+
last_update_time = current_time
|
|
416
|
+
|
|
417
|
+
except (ValueError, KeyError):
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
# Wait for process to complete
|
|
421
|
+
process.wait()
|
|
422
|
+
|
|
423
|
+
# Ensure progress bar reaches 100%
|
|
424
|
+
if max_frame_seen > current_frame:
|
|
425
|
+
pbar.update(max_frame_seen - current_frame)
|
|
426
|
+
current_frame = max_frame_seen
|
|
427
|
+
|
|
428
|
+
if current_frame < pbar.total:
|
|
429
|
+
pbar.update(pbar.total - current_frame)
|
|
430
|
+
|
|
431
|
+
pbar.close()
|
|
432
|
+
|
|
433
|
+
if process.returncode != 0:
|
|
434
|
+
remaining_stderr = process.stderr.read()
|
|
435
|
+
print(f"Error generating base clip: {remaining_stderr}")
|
|
436
|
+
raise subprocess.CalledProcessError(process.returncode, cmd_base, remaining_stderr)
|
|
437
|
+
|
|
438
|
+
print(f"✓ Base clip generated: {temp_clip} ({max_frame_seen} frames)")
|
|
439
|
+
|
|
440
|
+
except subprocess.CalledProcessError as e:
|
|
441
|
+
print(f"FFmpeg error: {e}")
|
|
442
|
+
raise
|
|
443
|
+
except Exception as e:
|
|
444
|
+
print(f"Error: {e}")
|
|
445
|
+
raise
|
|
446
|
+
else:
|
|
447
|
+
print(f"✓ Reusing existing base clip: {temp_clip}")
|
|
448
|
+
|
|
449
|
+
# Step 2: Loop the base clip to desired duration (if needed)
|
|
450
|
+
if duration > base_duration:
|
|
451
|
+
print(f"\nStep 2: Looping base clip to {duration}s duration...")
|
|
452
|
+
loop_count = int(duration / base_duration) + 1
|
|
453
|
+
print(f"Looping {loop_count} times to reach target duration")
|
|
454
|
+
|
|
455
|
+
# FFmpeg command to loop the video
|
|
456
|
+
cmd_loop = [
|
|
457
|
+
ffmpeg_path, "-y",
|
|
458
|
+
"-progress", "pipe:2",
|
|
459
|
+
"-v", "warning",
|
|
460
|
+
"-stream_loop", str(loop_count),
|
|
461
|
+
"-i", temp_clip,
|
|
462
|
+
"-t", str(duration),
|
|
463
|
+
# Re-encode with same parameters
|
|
464
|
+
"-c:v", encoder,
|
|
465
|
+
"-preset", "fast", # Use faster preset for looping
|
|
466
|
+
"-g", str(gop_size),
|
|
467
|
+
"-bf", str(input_bframes),
|
|
468
|
+
"-c:a", "aac",
|
|
469
|
+
"-b:a", "128k",
|
|
470
|
+
codec_output_path
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Calculate estimated total frames for progress tracking
|
|
475
|
+
estimated_frames = duration * fps
|
|
476
|
+
|
|
477
|
+
# For custom FFmpeg builds, ensure the subprocess finds shared libs
|
|
478
|
+
env = get_ffmpeg_env(ffmpeg_path)
|
|
479
|
+
# Execute FFmpeg command with progress tracking
|
|
480
|
+
process = subprocess.Popen(cmd_loop, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
481
|
+
universal_newlines=True, bufsize=1, env=env)
|
|
482
|
+
|
|
483
|
+
# Progress bar setup
|
|
484
|
+
video_name = os.path.basename(codec_output_path)
|
|
485
|
+
pbar = tqdm(total=estimated_frames, unit='frames',
|
|
486
|
+
desc=f"Looping to {duration}s",
|
|
487
|
+
ncols=80, leave=True)
|
|
488
|
+
|
|
489
|
+
# Read stderr line by line to get progress
|
|
490
|
+
current_frame = 0
|
|
491
|
+
max_frame_seen = 0
|
|
492
|
+
last_update_time = time.time()
|
|
493
|
+
update_interval = 0.5
|
|
494
|
+
|
|
495
|
+
while True:
|
|
496
|
+
line = process.stderr.readline()
|
|
497
|
+
if not line:
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
# Parse progress information
|
|
501
|
+
progress_data = parse_ffmpeg_progress(line.strip())
|
|
502
|
+
if progress_data and 'frame' in progress_data:
|
|
503
|
+
try:
|
|
504
|
+
frame_num = int(progress_data['frame'])
|
|
505
|
+
max_frame_seen = max(max_frame_seen, frame_num)
|
|
506
|
+
|
|
507
|
+
current_time = time.time()
|
|
508
|
+
if (current_time - last_update_time >= update_interval or
|
|
509
|
+
frame_num - current_frame >= 10):
|
|
510
|
+
|
|
511
|
+
if frame_num > estimated_frames:
|
|
512
|
+
pbar.total = frame_num + 50
|
|
513
|
+
|
|
514
|
+
if frame_num > current_frame:
|
|
515
|
+
pbar.update(frame_num - current_frame)
|
|
516
|
+
current_frame = frame_num
|
|
517
|
+
last_update_time = current_time
|
|
518
|
+
|
|
519
|
+
except (ValueError, KeyError):
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
# Wait for process to complete
|
|
523
|
+
process.wait()
|
|
524
|
+
|
|
525
|
+
# Ensure progress bar reaches 100%
|
|
526
|
+
if max_frame_seen > current_frame:
|
|
527
|
+
pbar.update(max_frame_seen - current_frame)
|
|
528
|
+
current_frame = max_frame_seen
|
|
529
|
+
|
|
530
|
+
if current_frame < pbar.total:
|
|
531
|
+
pbar.update(pbar.total - current_frame)
|
|
532
|
+
|
|
533
|
+
pbar.close()
|
|
534
|
+
|
|
535
|
+
if process.returncode != 0:
|
|
536
|
+
remaining_stderr = process.stderr.read()
|
|
537
|
+
print(f"Error looping video: {remaining_stderr}")
|
|
538
|
+
raise subprocess.CalledProcessError(process.returncode, cmd_loop, remaining_stderr)
|
|
539
|
+
|
|
540
|
+
print(f"✓ Video generation with audio complete: {codec_output_path}")
|
|
541
|
+
print(f" Video codec: {encoder} with {gop_size} GOP size and {input_bframes} B-frames")
|
|
542
|
+
print(f" Frames encoded: {max_frame_seen}")
|
|
543
|
+
print(f" Performance: Generated {duration}s video by looping {base_duration}s base clip")
|
|
544
|
+
|
|
545
|
+
# Clean up temp clip since we have the looped video
|
|
546
|
+
if os.path.exists(temp_clip):
|
|
547
|
+
os.remove(temp_clip)
|
|
548
|
+
print(f" Cleaned up temp clip: {temp_clip}")
|
|
549
|
+
|
|
550
|
+
return codec_output_path
|
|
551
|
+
|
|
552
|
+
except subprocess.CalledProcessError as e:
|
|
553
|
+
print(f"FFmpeg error: {e}")
|
|
554
|
+
raise
|
|
555
|
+
except Exception as e:
|
|
556
|
+
print(f"Error: {e}")
|
|
557
|
+
raise
|
|
558
|
+
else:
|
|
559
|
+
# Duration is <= base_duration, just rename/copy the temp clip
|
|
560
|
+
print(f"\nUsing base clip as final output (duration <= {base_duration}s)")
|
|
561
|
+
if os.path.exists(codec_output_path):
|
|
562
|
+
os.remove(codec_output_path)
|
|
563
|
+
os.rename(temp_clip, codec_output_path)
|
|
564
|
+
print(f"✓ Video generation complete: {codec_output_path}")
|
|
565
|
+
return codec_output_path
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def generate_random_segments(duration, num_segments, min_segment_duration=1.0):
|
|
569
|
+
"""
|
|
570
|
+
Generate random non-overlapping start and end timestamps for video segments
|
|
571
|
+
|
|
572
|
+
Parameters:
|
|
573
|
+
duration (float): Total video duration in seconds
|
|
574
|
+
num_segments (int): Number of segments to generate
|
|
575
|
+
min_segment_duration (float): Minimum duration of each segment in seconds
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
list: List of tuples containing (start_time, end_time) for each segment
|
|
579
|
+
"""
|
|
580
|
+
segments = []
|
|
581
|
+
|
|
582
|
+
# Make sure we can fit all segments with minimum duration
|
|
583
|
+
total_segment_time = num_segments * min_segment_duration
|
|
584
|
+
if total_segment_time > duration:
|
|
585
|
+
num_segments = int(duration / min_segment_duration)
|
|
586
|
+
print(f"Warning: Reduced number of segments to {num_segments} to ensure minimum segment duration")
|
|
587
|
+
total_segment_time = num_segments * min_segment_duration
|
|
588
|
+
|
|
589
|
+
if num_segments == 0:
|
|
590
|
+
print("Warning: Cannot create any segments with the given parameters")
|
|
591
|
+
return []
|
|
592
|
+
|
|
593
|
+
# Calculate the total available gap time (time not occupied by segments)
|
|
594
|
+
total_gap_time = duration - total_segment_time
|
|
595
|
+
|
|
596
|
+
# Divide video into num_segments equal slots and place one segment randomly in each
|
|
597
|
+
# This guarantees we always get exactly num_segments segments
|
|
598
|
+
slot_duration = duration / num_segments
|
|
599
|
+
|
|
600
|
+
# Calculate max segment duration (allow some variation but cap it)
|
|
601
|
+
max_segment_duration = min(slot_duration * 0.9, min_segment_duration * 2)
|
|
602
|
+
max_segment_duration = max(max_segment_duration, min_segment_duration) # Ensure max >= min
|
|
603
|
+
|
|
604
|
+
for i in range(num_segments):
|
|
605
|
+
slot_start = i * slot_duration
|
|
606
|
+
slot_end = (i + 1) * slot_duration
|
|
607
|
+
|
|
608
|
+
# Randomize segment duration between min and max
|
|
609
|
+
segment_duration = random.uniform(min_segment_duration, max_segment_duration)
|
|
610
|
+
|
|
611
|
+
# Ensure segment fits within the slot
|
|
612
|
+
if segment_duration > slot_duration:
|
|
613
|
+
segment_duration = slot_duration
|
|
614
|
+
|
|
615
|
+
# Calculate the range where segment can start within this slot
|
|
616
|
+
max_start = slot_end - segment_duration
|
|
617
|
+
if max_start < slot_start:
|
|
618
|
+
max_start = slot_start
|
|
619
|
+
segment_duration = slot_end - slot_start # Adjust duration to fit
|
|
620
|
+
|
|
621
|
+
# Random start position within the valid range
|
|
622
|
+
start_time = random.uniform(slot_start, max_start)
|
|
623
|
+
end_time = start_time + segment_duration
|
|
624
|
+
|
|
625
|
+
# Ensure we don't exceed video duration
|
|
626
|
+
if end_time > duration:
|
|
627
|
+
end_time = duration
|
|
628
|
+
start_time = max(0, end_time - segment_duration)
|
|
629
|
+
|
|
630
|
+
segments.append((start_time, end_time))
|
|
631
|
+
|
|
632
|
+
# Sort segments by start time
|
|
633
|
+
segments.sort()
|
|
634
|
+
|
|
635
|
+
return segments
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def run_subprocess(cmd, shell=False, description="", env=None):
|
|
639
|
+
"""
|
|
640
|
+
Run a subprocess and wait for completion, with proper error handling
|
|
641
|
+
|
|
642
|
+
Parameters:
|
|
643
|
+
cmd: Command to run (list or string)
|
|
644
|
+
shell: Whether to use shell execution
|
|
645
|
+
description: Description of the process for logging
|
|
646
|
+
env: Optional environment dict (e.g. from get_ffmpeg_env) for FFmpeg shared libs
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
tuple: (stdout, stderr) from the process
|
|
650
|
+
"""
|
|
651
|
+
try:
|
|
652
|
+
print(f"Starting {description}...")
|
|
653
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
654
|
+
text=True, shell=shell, env=env)
|
|
655
|
+
|
|
656
|
+
# Wait for the process to complete
|
|
657
|
+
stdout, stderr = process.communicate()
|
|
658
|
+
|
|
659
|
+
# Check if process completed successfully
|
|
660
|
+
if process.returncode != 0:
|
|
661
|
+
print(f"Error in {description}: {stderr}")
|
|
662
|
+
print(f"Command was: {cmd}")
|
|
663
|
+
else:
|
|
664
|
+
print(f"{description} completed successfully")
|
|
665
|
+
|
|
666
|
+
return stdout, stderr
|
|
667
|
+
except Exception as e:
|
|
668
|
+
print(f"Exception in {description}: {e}")
|
|
669
|
+
raise
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def segmented_transcode(args):
|
|
673
|
+
"""
|
|
674
|
+
Transcode the input video using the specified method
|
|
675
|
+
|
|
676
|
+
Parameters:
|
|
677
|
+
args (list): List of parameters including video path, clip_start_end, config, usage, ffmpeg_path, ffprobe_path, etc.
|
|
678
|
+
"""
|
|
679
|
+
video_path, clip_start_end, config, fps, usage, chunk_size, ffmpeg_path, ffprobe_path = args
|
|
680
|
+
print(f"Segmented transcode for {video_path}")
|
|
681
|
+
print(f"Number of segments: {len(clip_start_end)}")
|
|
682
|
+
print(f"Using mode: {usage}")
|
|
683
|
+
|
|
684
|
+
# So custom FFmpeg builds find shared libs (LD_LIBRARY_PATH on Unix, PATH on Windows)
|
|
685
|
+
ffmpeg_env = get_ffmpeg_env(ffmpeg_path)
|
|
686
|
+
|
|
687
|
+
# Get B-frames setting from config
|
|
688
|
+
bframes = config.get("bf", 0) # Default to 0 if not specified
|
|
689
|
+
print(f"Using {bframes} B-frames for transcoding")
|
|
690
|
+
|
|
691
|
+
# Properly extract base filename without extension
|
|
692
|
+
base, _ = os.path.splitext(os.path.basename(video_path))
|
|
693
|
+
|
|
694
|
+
# Create output directory if it doesn't exist
|
|
695
|
+
os.makedirs(os.path.dirname(os.path.abspath(video_path)), exist_ok=True)
|
|
696
|
+
|
|
697
|
+
# Start timing
|
|
698
|
+
start_time = time.time()
|
|
699
|
+
|
|
700
|
+
# Get codec and resolution from config
|
|
701
|
+
codec = config.get("codec", "h264")
|
|
702
|
+
width = config.get("width", 1920)
|
|
703
|
+
height = config.get("height", 1080)
|
|
704
|
+
resolution = f"{width}x{height}"
|
|
705
|
+
|
|
706
|
+
# Create a resolution-codec-bframes suffix for file names
|
|
707
|
+
suffix = f"_{codec}_{resolution}_bf{bframes}"
|
|
708
|
+
|
|
709
|
+
# Determine FFmpeg encoder based on output codec
|
|
710
|
+
encoder_map = {
|
|
711
|
+
"h264": "h264_nvenc",
|
|
712
|
+
"hevc": "hevc_nvenc",
|
|
713
|
+
"av1": "av1_nvenc"
|
|
714
|
+
}
|
|
715
|
+
encoder = encoder_map.get(codec.lower(), "h264_nvenc")
|
|
716
|
+
|
|
717
|
+
# Determine input video codec and decoder to use
|
|
718
|
+
# First try to get from input file using ffprobe
|
|
719
|
+
input_codec = "h264" # Default assumption
|
|
720
|
+
try:
|
|
721
|
+
probe_cmd = [ffprobe_path, "-v", "error", "-select_streams", "v:0",
|
|
722
|
+
"-show_entries", "stream=codec_name", "-of", "csv=p=0", video_path]
|
|
723
|
+
probe_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=ffmpeg_env)
|
|
724
|
+
if probe_result.returncode == 0 and probe_result.stdout.strip():
|
|
725
|
+
input_codec = probe_result.stdout.strip().lower()
|
|
726
|
+
print(f"Detected input codec: {input_codec}")
|
|
727
|
+
except Exception as e:
|
|
728
|
+
print(f"Error detecting input codec: {str(e)}. Using default h264 decoder.")
|
|
729
|
+
|
|
730
|
+
# Map to cuvid decoder
|
|
731
|
+
decoder_map = {
|
|
732
|
+
"h264": "h264_cuvid",
|
|
733
|
+
"avc": "h264_cuvid",
|
|
734
|
+
"hevc": "hevc_cuvid",
|
|
735
|
+
"av1": "av1_cuvid"
|
|
736
|
+
}
|
|
737
|
+
decoder = decoder_map.get(input_codec, "h264_cuvid")
|
|
738
|
+
print(f"Using decoder: {decoder}")
|
|
739
|
+
|
|
740
|
+
# Create output directories if they don't exist
|
|
741
|
+
os.makedirs("pynvc_out", exist_ok=True)
|
|
742
|
+
os.makedirs("ffmpeg_out", exist_ok=True)
|
|
743
|
+
os.makedirs("ffmpeg_fc_out", exist_ok=True)
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
if usage == 0:
|
|
747
|
+
# PyNVC Usage
|
|
748
|
+
out_file_path = os.path.join("pynvc_out", f"{base}{suffix}_transcoded.mp4")
|
|
749
|
+
print(f"Output will be saved to: ./pynvc_out/")
|
|
750
|
+
|
|
751
|
+
# Make a copy of the config and remove width and height
|
|
752
|
+
transcoder_config = config.copy()
|
|
753
|
+
if 'width' in transcoder_config:
|
|
754
|
+
del transcoder_config['width']
|
|
755
|
+
if 'height' in transcoder_config:
|
|
756
|
+
del transcoder_config['height']
|
|
757
|
+
|
|
758
|
+
# Pass the modified config to the Transcoder
|
|
759
|
+
tr = nvc.Transcoder(video_path, out_file_path, 0, 0, 0, **transcoder_config)
|
|
760
|
+
for start, end in clip_start_end:
|
|
761
|
+
tr.segmented_transcode(start, end)
|
|
762
|
+
|
|
763
|
+
elif usage == 1:
|
|
764
|
+
print(f"FFmpeg without map")
|
|
765
|
+
print(f"Output files will be saved to: ./ffmpeg_out/")
|
|
766
|
+
for i in range(0, len(clip_start_end), chunk_size):
|
|
767
|
+
chunk = clip_start_end[i:i+chunk_size]
|
|
768
|
+
for start, end in chunk:
|
|
769
|
+
# Include codec and resolution in output file path
|
|
770
|
+
out_file_path = os.path.join("ffmpeg_out", f"{base}{suffix}_{start:.2f}_{end:.2f}.mp4")
|
|
771
|
+
segment_cmd = [
|
|
772
|
+
ffmpeg_path, "-loglevel", "warning", "-hide_banner", "-y",
|
|
773
|
+
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
|
|
774
|
+
"-c:v", decoder,
|
|
775
|
+
"-ss", str(start),
|
|
776
|
+
"-to", str(end),
|
|
777
|
+
"-i", video_path,
|
|
778
|
+
"-bf", str(bframes),
|
|
779
|
+
"-c:v", encoder,
|
|
780
|
+
"-c:a", "copy",
|
|
781
|
+
out_file_path
|
|
782
|
+
]
|
|
783
|
+
desc = f"FFmpeg transcode segment {start:.2f}s - {end:.2f}s"
|
|
784
|
+
run_subprocess(segment_cmd, shell=False, description=desc, env=ffmpeg_env)
|
|
785
|
+
|
|
786
|
+
elif usage == 2:
|
|
787
|
+
print(f"FFmpeg with map but no audio")
|
|
788
|
+
print(f"Output files will be saved to: ./ffmpeg_fc_out/")
|
|
789
|
+
for i in range(0, len(clip_start_end), chunk_size):
|
|
790
|
+
chunk = clip_start_end[i:i+chunk_size]
|
|
791
|
+
# Only decode the range spanning this chunk (same as u=1 behavior per chunk)
|
|
792
|
+
chunk_min_start = min(s for s, e in chunk)
|
|
793
|
+
chunk_max_end = max(e for s, e in chunk)
|
|
794
|
+
|
|
795
|
+
segment_cmd = [ffmpeg_path, "-loglevel", "warning", "-hide_banner", "-y",
|
|
796
|
+
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
|
|
797
|
+
"-c:v", decoder,
|
|
798
|
+
"-ss", str(chunk_min_start),
|
|
799
|
+
"-to", str(chunk_max_end),
|
|
800
|
+
"-i", video_path]
|
|
801
|
+
|
|
802
|
+
filter_complex = []
|
|
803
|
+
map_cmds = []
|
|
804
|
+
output_files = []
|
|
805
|
+
|
|
806
|
+
for j, (start, end) in enumerate(chunk):
|
|
807
|
+
# Include codec and resolution in output file path
|
|
808
|
+
out_file_path = f"./ffmpeg_fc_out/{base}{suffix}_{start:.2f}_{end:.2f}.mp4"
|
|
809
|
+
output_files.append(out_file_path)
|
|
810
|
+
|
|
811
|
+
# Trim relative to chunk start (decoded stream starts at chunk_min_start)
|
|
812
|
+
rel_start = start - chunk_min_start
|
|
813
|
+
rel_end = end - chunk_min_start
|
|
814
|
+
filter_complex.append(f"[0:v]trim=start={rel_start}:end={rel_end},setpts=PTS-STARTPTS[v{j}]")
|
|
815
|
+
map_cmds.extend(["-map", f"[v{j}]", "-c:v", encoder, "-an", out_file_path])
|
|
816
|
+
|
|
817
|
+
# Combine all filters
|
|
818
|
+
segment_cmd.extend(["-filter_complex", ";".join(filter_complex)])
|
|
819
|
+
segment_cmd.extend(map_cmds)
|
|
820
|
+
|
|
821
|
+
# Convert the list to a shell command string
|
|
822
|
+
cmd_str = " ".join([
|
|
823
|
+
f'"{arg}"' if (" " in arg or ";" in arg or "[" in arg) else arg
|
|
824
|
+
for arg in segment_cmd
|
|
825
|
+
])
|
|
826
|
+
|
|
827
|
+
# Execute FFmpeg command
|
|
828
|
+
desc = f"FFmpeg batch transcode {len(chunk)} segments (no audio)"
|
|
829
|
+
run_subprocess(cmd_str, shell=True, description=desc, env=ffmpeg_env)
|
|
830
|
+
|
|
831
|
+
elif usage == 3:
|
|
832
|
+
print(f"FFmpeg with map and audio")
|
|
833
|
+
print(f"Output files will be saved to: ./ffmpeg_fc_out/")
|
|
834
|
+
# Probe for audio once per video (not per segment)
|
|
835
|
+
has_audio = False
|
|
836
|
+
if os.path.exists(video_path):
|
|
837
|
+
probe_cmd = [ffprobe_path, "-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "csv=p=0", video_path]
|
|
838
|
+
probe_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=ffmpeg_env)
|
|
839
|
+
has_audio = "audio" in probe_result.stdout
|
|
840
|
+
|
|
841
|
+
for i in range(0, len(clip_start_end), chunk_size):
|
|
842
|
+
chunk = clip_start_end[i:i+chunk_size]
|
|
843
|
+
# Only decode the range spanning this chunk (same as u=1 behavior per chunk)
|
|
844
|
+
chunk_min_start = min(s for s, e in chunk)
|
|
845
|
+
chunk_max_end = max(e for s, e in chunk)
|
|
846
|
+
|
|
847
|
+
segment_cmd = [ffmpeg_path, "-y", "-loglevel", "warning",
|
|
848
|
+
"-hwaccel", "cuda", "-hwaccel_output_format", "cuda",
|
|
849
|
+
"-c:v", decoder,
|
|
850
|
+
"-ss", str(chunk_min_start),
|
|
851
|
+
"-to", str(chunk_max_end),
|
|
852
|
+
"-i", video_path]
|
|
853
|
+
|
|
854
|
+
filter_complex = []
|
|
855
|
+
map_cmds = []
|
|
856
|
+
output_files = []
|
|
857
|
+
|
|
858
|
+
for j, (start, end) in enumerate(chunk):
|
|
859
|
+
# Include codec and resolution in output file path
|
|
860
|
+
out_file_path = f"./ffmpeg_fc_out/{base}{suffix}_{start:.2f}_{end:.2f}.mp4"
|
|
861
|
+
output_files.append(out_file_path)
|
|
862
|
+
|
|
863
|
+
# Trim relative to chunk start (decoded stream starts at chunk_min_start)
|
|
864
|
+
rel_start = start - chunk_min_start
|
|
865
|
+
rel_end = end - chunk_min_start
|
|
866
|
+
filter_complex.append(f"[0:v]trim=start={rel_start}:end={rel_end},setpts=PTS-STARTPTS[v{j}]")
|
|
867
|
+
|
|
868
|
+
if has_audio:
|
|
869
|
+
filter_complex.append(f"[0:a]atrim=start={rel_start}:end={rel_end},asetpts=PTS-STARTPTS[a{j}]")
|
|
870
|
+
map_cmds.extend(["-map", f"[v{j}]", "-map", f"[a{j}]"])
|
|
871
|
+
else:
|
|
872
|
+
map_cmds.extend(["-map", f"[v{j}]"])
|
|
873
|
+
|
|
874
|
+
# Convert P4 to p4 (lowercase) for FFmpeg preset
|
|
875
|
+
ffmpeg_preset = config.get("preset", "p4").lower()
|
|
876
|
+
|
|
877
|
+
# Add encoding parameters
|
|
878
|
+
map_cmds.extend([
|
|
879
|
+
"-c:v", encoder, # Use appropriate encoder
|
|
880
|
+
"-preset", ffmpeg_preset,
|
|
881
|
+
"-rc", "vbr",
|
|
882
|
+
"-b:v", "5M",
|
|
883
|
+
"-maxrate", "10M",
|
|
884
|
+
"-bufsize", "10M",
|
|
885
|
+
])
|
|
886
|
+
|
|
887
|
+
# Add audio encoding if we have audio
|
|
888
|
+
audio_filter = f"a{j}" in "".join(filter_complex)
|
|
889
|
+
if audio_filter:
|
|
890
|
+
map_cmds.extend(["-c:a", "aac", "-b:a", "192k"])
|
|
891
|
+
|
|
892
|
+
map_cmds.append(out_file_path)
|
|
893
|
+
|
|
894
|
+
# Combine all filters
|
|
895
|
+
segment_cmd.extend(["-filter_complex", ";".join(filter_complex)])
|
|
896
|
+
segment_cmd.extend(map_cmds)
|
|
897
|
+
|
|
898
|
+
# Convert the list to a shell command string
|
|
899
|
+
cmd_str = " ".join([
|
|
900
|
+
f'"{arg}"' if (" " in arg or ";" in arg or "[" in arg) else arg
|
|
901
|
+
for arg in segment_cmd
|
|
902
|
+
])
|
|
903
|
+
|
|
904
|
+
# Execute FFmpeg command
|
|
905
|
+
desc = f"FFmpeg batch transcode {len(chunk)} segments (with audio)"
|
|
906
|
+
run_subprocess(cmd_str, shell=True, description=desc, env=ffmpeg_env)
|
|
907
|
+
|
|
908
|
+
else:
|
|
909
|
+
print(f"Error: Invalid usage mode {usage}. Must be 0, 1, 2, or 3")
|
|
910
|
+
|
|
911
|
+
except Exception as e:
|
|
912
|
+
print(f"Error processing {video_path}: {str(e)}")
|
|
913
|
+
raise
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def extract_segments_from_log(log_data):
|
|
917
|
+
"""
|
|
918
|
+
Extract segment information from log data
|
|
919
|
+
|
|
920
|
+
Parameters:
|
|
921
|
+
log_data (dict): Log data from a previous run
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
list: List of (start_time, end_time) tuples
|
|
925
|
+
"""
|
|
926
|
+
try:
|
|
927
|
+
# Find segment creation step
|
|
928
|
+
for step in log_data.get("steps", []):
|
|
929
|
+
if step.get("type") == "segment_creation":
|
|
930
|
+
if "results" in step and "segments" in step["results"]:
|
|
931
|
+
segments = step["results"]["segments"]
|
|
932
|
+
return [(segment["start"], segment["end"]) for segment in segments]
|
|
933
|
+
|
|
934
|
+
return []
|
|
935
|
+
|
|
936
|
+
except Exception as e:
|
|
937
|
+
print(f"Error parsing segments from log: {e}")
|
|
938
|
+
return []
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def replay_from_log(log_path, ffmpeg_path, ffprobe_path):
|
|
942
|
+
"""
|
|
943
|
+
Replay a previous run from a log file
|
|
944
|
+
|
|
945
|
+
Parameters:
|
|
946
|
+
log_path (str): Path to the log file
|
|
947
|
+
ffmpeg_path (str): Path to the ffmpeg executable (from --ffmpeg-dir).
|
|
948
|
+
ffprobe_path (str): Path to the ffprobe executable (from --ffmpeg-dir).
|
|
949
|
+
"""
|
|
950
|
+
import re
|
|
951
|
+
global log_data
|
|
952
|
+
|
|
953
|
+
print(f"Replaying from log: {log_path}")
|
|
954
|
+
|
|
955
|
+
try:
|
|
956
|
+
with open(log_path, 'r') as f:
|
|
957
|
+
log_data = json.load(f)
|
|
958
|
+
|
|
959
|
+
print(f"Log timestamp: {log_data['timestamp']}")
|
|
960
|
+
print(f"Original arguments: {json.dumps(log_data['args'], indent=2)}")
|
|
961
|
+
|
|
962
|
+
# Extract video path from video generation, video reuse, or video input step
|
|
963
|
+
video_path = None
|
|
964
|
+
video_params = None
|
|
965
|
+
for step in log_data['steps']:
|
|
966
|
+
if step['type'] == 'video_generation':
|
|
967
|
+
video_path = step['params']['output_path']
|
|
968
|
+
video_params = step['params']
|
|
969
|
+
break
|
|
970
|
+
elif step['type'] == 'video_reuse':
|
|
971
|
+
# For video_reuse, the path is in the description or params
|
|
972
|
+
if 'output_path' in step['params']:
|
|
973
|
+
video_path = step['params']['output_path']
|
|
974
|
+
video_params = step['params']
|
|
975
|
+
elif 'params' in step and step['params']:
|
|
976
|
+
# Construct the path from parameters
|
|
977
|
+
width = step['params'].get('width')
|
|
978
|
+
height = step['params'].get('height')
|
|
979
|
+
codec = step['params'].get('codec', 'h264')
|
|
980
|
+
video_path = f"source_videos/mandelbrot_{codec}_{width}x{height}.mp4"
|
|
981
|
+
video_params = step['params']
|
|
982
|
+
# Try to extract from description as fallback
|
|
983
|
+
if not video_path and 'description' in step:
|
|
984
|
+
match = re.search(r'Reusing existing video: (.+)', step['description'])
|
|
985
|
+
if match:
|
|
986
|
+
video_path = match.group(1)
|
|
987
|
+
video_params = step['params']
|
|
988
|
+
break
|
|
989
|
+
elif step['type'] == 'video_input':
|
|
990
|
+
# User provided an input video
|
|
991
|
+
if 'input_path' in step['params']:
|
|
992
|
+
video_path = step['params']['input_path']
|
|
993
|
+
video_params = step['params']
|
|
994
|
+
# Try to extract from description as fallback
|
|
995
|
+
if not video_path and 'description' in step:
|
|
996
|
+
match = re.search(r'Using user-provided input video: (.+)', step['description'])
|
|
997
|
+
if match:
|
|
998
|
+
video_path = match.group(1)
|
|
999
|
+
video_params = step['params']
|
|
1000
|
+
break
|
|
1001
|
+
|
|
1002
|
+
if not video_path:
|
|
1003
|
+
print("Error: No video generation, video reuse, or video input step found in log")
|
|
1004
|
+
print("Available step types:", [step.get('type') for step in log_data.get('steps', [])])
|
|
1005
|
+
return
|
|
1006
|
+
|
|
1007
|
+
# Check if the video exists
|
|
1008
|
+
if not os.path.exists(video_path):
|
|
1009
|
+
print(f"Video file not found: {video_path}")
|
|
1010
|
+
print("Regenerating video...")
|
|
1011
|
+
|
|
1012
|
+
# Use video_params if available, otherwise try to get from log_data args
|
|
1013
|
+
if video_params:
|
|
1014
|
+
video_path = generate_mandelbrot_video(
|
|
1015
|
+
video_params.get('width'),
|
|
1016
|
+
video_params.get('height'),
|
|
1017
|
+
video_params.get('duration'),
|
|
1018
|
+
video_params.get('fps'),
|
|
1019
|
+
video_path, # Use the constructed path
|
|
1020
|
+
video_params.get('gop_size'),
|
|
1021
|
+
video_params.get('codec', 'h264'),
|
|
1022
|
+
video_params.get('bframes', 2),
|
|
1023
|
+
ffmpeg_path=ffmpeg_path
|
|
1024
|
+
)
|
|
1025
|
+
else:
|
|
1026
|
+
# Fallback to args
|
|
1027
|
+
args_data = log_data.get('args', {})
|
|
1028
|
+
video_path = generate_mandelbrot_video(
|
|
1029
|
+
args_data.get('width', 1920),
|
|
1030
|
+
args_data.get('height', 1080),
|
|
1031
|
+
args_data.get('duration', 10),
|
|
1032
|
+
args_data.get('fps', 30),
|
|
1033
|
+
video_path,
|
|
1034
|
+
args_data.get('gop_size', 250),
|
|
1035
|
+
args_data.get('input_codec', 'h264'),
|
|
1036
|
+
args_data.get('input_bframes', 2),
|
|
1037
|
+
ffmpeg_path=ffmpeg_path
|
|
1038
|
+
)
|
|
1039
|
+
else:
|
|
1040
|
+
print(f"Using existing video: {video_path}")
|
|
1041
|
+
|
|
1042
|
+
# Extract segments
|
|
1043
|
+
clip_start_end = extract_segments_from_log(log_data)
|
|
1044
|
+
if not clip_start_end:
|
|
1045
|
+
print("Error: No segments found in log")
|
|
1046
|
+
return
|
|
1047
|
+
|
|
1048
|
+
# Extract usage modes
|
|
1049
|
+
usage_modes = []
|
|
1050
|
+
for step in log_data['steps']:
|
|
1051
|
+
if step['type'] == 'transcoding' and 'usage' in step['params']:
|
|
1052
|
+
if step['params']['usage'] not in usage_modes:
|
|
1053
|
+
usage_modes.append(step['params']['usage'])
|
|
1054
|
+
|
|
1055
|
+
if not usage_modes:
|
|
1056
|
+
print("Error: No transcoding steps found in log")
|
|
1057
|
+
return
|
|
1058
|
+
|
|
1059
|
+
print(f"Replay will use these usage modes: {usage_modes}")
|
|
1060
|
+
|
|
1061
|
+
# Extract config
|
|
1062
|
+
config = log_data['args'].get('config', {"preset": "P4", "codec": "h264", "tuning_info": "high_quality", "bf": "0"})
|
|
1063
|
+
|
|
1064
|
+
# Calculate total frames
|
|
1065
|
+
fps = log_data['args'].get('fps', 30)
|
|
1066
|
+
segment_frames = sum((end - start) * fps for start, end in clip_start_end)
|
|
1067
|
+
|
|
1068
|
+
# Store results for comparison
|
|
1069
|
+
results = {}
|
|
1070
|
+
|
|
1071
|
+
# Run transcoding for each usage mode
|
|
1072
|
+
for usage in usage_modes:
|
|
1073
|
+
if usage not in [0, 1, 2, 3]:
|
|
1074
|
+
print(f"Warning: Invalid usage mode {usage}. Skipping...")
|
|
1075
|
+
continue
|
|
1076
|
+
|
|
1077
|
+
print("\n" + "=" * 80)
|
|
1078
|
+
print(f"REPLAY: TRANSCODING SEGMENTS USING MODE {usage}")
|
|
1079
|
+
print("=" * 80)
|
|
1080
|
+
|
|
1081
|
+
# Get mode description
|
|
1082
|
+
mode_desc = {
|
|
1083
|
+
0: "PyNVC transcoding",
|
|
1084
|
+
1: "FFmpeg without map",
|
|
1085
|
+
2: "FFmpeg with map (no audio)",
|
|
1086
|
+
3: "FFmpeg with map (with audio)"
|
|
1087
|
+
}.get(usage, "Unknown mode")
|
|
1088
|
+
|
|
1089
|
+
print(f"Running: {mode_desc}")
|
|
1090
|
+
|
|
1091
|
+
futures_list = []
|
|
1092
|
+
starttime = time.time()
|
|
1093
|
+
|
|
1094
|
+
# Extract numthreads and chunk_size
|
|
1095
|
+
numthreads = log_data['args'].get('numthreads', 1)
|
|
1096
|
+
chunk_size = log_data['args'].get('chunk_size', 5)
|
|
1097
|
+
# numthreads = 3
|
|
1098
|
+
|
|
1099
|
+
# Distribute segments among threads (same as normal run)
|
|
1100
|
+
clip_chunks = split_list(numthreads, clip_start_end)
|
|
1101
|
+
|
|
1102
|
+
with ThreadPoolExecutor(max_workers=numthreads) as executor:
|
|
1103
|
+
# Submit one task per thread, each with its subset of segments
|
|
1104
|
+
futures = [
|
|
1105
|
+
executor.submit(segmented_transcode, [
|
|
1106
|
+
video_path,
|
|
1107
|
+
clip_chunks[m],
|
|
1108
|
+
config,
|
|
1109
|
+
fps,
|
|
1110
|
+
usage,
|
|
1111
|
+
chunk_size,
|
|
1112
|
+
ffmpeg_path,
|
|
1113
|
+
ffprobe_path
|
|
1114
|
+
])
|
|
1115
|
+
for m in range(numthreads)
|
|
1116
|
+
]
|
|
1117
|
+
futures_list.append(futures)
|
|
1118
|
+
|
|
1119
|
+
# Wait for all tasks to complete
|
|
1120
|
+
for future in futures_list:
|
|
1121
|
+
wait(future)
|
|
1122
|
+
|
|
1123
|
+
endtime = time.time()
|
|
1124
|
+
total_time = endtime - starttime
|
|
1125
|
+
throughput = segment_frames / total_time if total_time > 0 else 0
|
|
1126
|
+
|
|
1127
|
+
# Store results
|
|
1128
|
+
results[usage] = {
|
|
1129
|
+
'mode': mode_desc,
|
|
1130
|
+
'time': total_time,
|
|
1131
|
+
'throughput': throughput
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
print(f"Completed {mode_desc}")
|
|
1135
|
+
print(f"Processing time: {total_time:.2f} seconds")
|
|
1136
|
+
print(f"Throughput: {throughput:.2f} frames/second")
|
|
1137
|
+
|
|
1138
|
+
# Log the results
|
|
1139
|
+
log_step("replay_results", f"Replay results for mode {usage}",
|
|
1140
|
+
params={"usage": usage, "mode": mode_desc},
|
|
1141
|
+
results={"time": total_time, "throughput": throughput})
|
|
1142
|
+
|
|
1143
|
+
# Print comparison results with additional details
|
|
1144
|
+
print("\n" + "=" * 80)
|
|
1145
|
+
print("REPLAY COMPARISON RESULTS")
|
|
1146
|
+
print("=" * 80)
|
|
1147
|
+
|
|
1148
|
+
# Add video details from the log data
|
|
1149
|
+
width = log_data['args'].get('width', 'unknown')
|
|
1150
|
+
height = log_data['args'].get('height', 'unknown')
|
|
1151
|
+
codec = log_data['args'].get('codec', 'unknown')
|
|
1152
|
+
duration = log_data['args'].get('duration', 'unknown')
|
|
1153
|
+
gop_size = log_data['args'].get('gop_size', 'unknown')
|
|
1154
|
+
num_segments = len(clip_start_end)
|
|
1155
|
+
|
|
1156
|
+
print(f"Video Resolution: {width}x{height}")
|
|
1157
|
+
print(f"Codec: {codec}")
|
|
1158
|
+
print(f"Duration: {duration} seconds")
|
|
1159
|
+
print(f"Number of Segments: {num_segments}")
|
|
1160
|
+
print(f"GOP Size: {gop_size}")
|
|
1161
|
+
print()
|
|
1162
|
+
|
|
1163
|
+
print(f"{'Mode':<30} {'Time (s)':<15} {'Throughput (fps)':<20}")
|
|
1164
|
+
print("-" * 65)
|
|
1165
|
+
|
|
1166
|
+
for usage, data in sorted(results.items()):
|
|
1167
|
+
print(f"{data['mode']:<30} {data['time']:<15.2f} {data['throughput']:<20.2f}")
|
|
1168
|
+
|
|
1169
|
+
# Find fastest method
|
|
1170
|
+
if results:
|
|
1171
|
+
fastest = min(results.items(), key=lambda x: x[1]['time'])
|
|
1172
|
+
highest_throughput = max(results.items(), key=lambda x: x[1]['throughput'])
|
|
1173
|
+
|
|
1174
|
+
print("\nResults Summary:")
|
|
1175
|
+
print(f"Total frames transcoded: {segment_frames:.0f}")
|
|
1176
|
+
print(f"Fastest method: {results[fastest[0]]['mode']} ({fastest[1]['time']:.2f}s)")
|
|
1177
|
+
print(f"Highest throughput: {results[highest_throughput[0]]['mode']} ({highest_throughput[1]['throughput']:.2f} fps)")
|
|
1178
|
+
|
|
1179
|
+
# Log summary with additional details
|
|
1180
|
+
log_step("summary", "Transcoding summary",
|
|
1181
|
+
results={
|
|
1182
|
+
"resolution": f"{width}x{height}",
|
|
1183
|
+
"codec": codec,
|
|
1184
|
+
"duration": duration,
|
|
1185
|
+
"num_segments": num_segments,
|
|
1186
|
+
"gop_size": gop_size,
|
|
1187
|
+
"total_frames": segment_frames,
|
|
1188
|
+
"fastest_method": results[fastest[0]]['mode'],
|
|
1189
|
+
"fastest_time": fastest[1]['time'],
|
|
1190
|
+
"highest_throughput_method": results[highest_throughput[0]]['mode'],
|
|
1191
|
+
"highest_throughput": highest_throughput[1]['throughput']
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
print("=" * 80)
|
|
1195
|
+
|
|
1196
|
+
# Compare with original results if available
|
|
1197
|
+
if 'results' in log_data and log_data['results']:
|
|
1198
|
+
print("\n" + "=" * 80)
|
|
1199
|
+
print("COMPARISON WITH ORIGINAL RUN")
|
|
1200
|
+
print("=" * 80)
|
|
1201
|
+
|
|
1202
|
+
print(f"{'Mode':<30} {'Original Time':<15} {'Replay Time':<15} {'Original FPS':<15} {'Replay FPS':<15}")
|
|
1203
|
+
print("-" * 90)
|
|
1204
|
+
|
|
1205
|
+
for usage, data in sorted(results.items()):
|
|
1206
|
+
if str(usage) in log_data['results']:
|
|
1207
|
+
orig_data = log_data['results'][str(usage)]
|
|
1208
|
+
print(f"{data['mode']:<30} {orig_data['time']:<15.2f} {data['time']:<15.2f} {orig_data['throughput']:<15.2f} {data['throughput']:<15.2f}")
|
|
1209
|
+
|
|
1210
|
+
print("=" * 90)
|
|
1211
|
+
|
|
1212
|
+
except Exception as e:
|
|
1213
|
+
print(f"Error during replay: {e}")
|
|
1214
|
+
raise
|
|
1215
|
+
|
|
1216
|
+
def split_list(n, clip_start_end):
|
|
1217
|
+
k, m = divmod(len(clip_start_end), n)
|
|
1218
|
+
result = []
|
|
1219
|
+
start = 0
|
|
1220
|
+
for i in range(n):
|
|
1221
|
+
end = start + k + (1 if i < m else 0)
|
|
1222
|
+
result.append(clip_start_end[start:end])
|
|
1223
|
+
start = end
|
|
1224
|
+
return result
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def parse_args():
|
|
1228
|
+
"""Parse command-line arguments."""
|
|
1229
|
+
parser = argparse.ArgumentParser(description="Benchmark segmented transcoding approaches")
|
|
1230
|
+
parser.add_argument("-g", "--gpuid", type=int, default=0, help="GPU ID")
|
|
1231
|
+
parser.add_argument("-W", "--width", type=int, default=1920, help="Video width. Default is 1920")
|
|
1232
|
+
parser.add_argument("-H", "--height", type=int, default=1080, help="Video height. Default is 1080")
|
|
1233
|
+
parser.add_argument("-d", "--duration", type=int, default=5400, help="Video duration in seconds. Default is 5400")
|
|
1234
|
+
parser.add_argument("-fps", "--fps", type=int, default=30, help="Frames per second. Default is 30")
|
|
1235
|
+
parser.add_argument("-p", "--preset", type=str, default="P1", help="Encoding preset (P1-P7). Default is P1")
|
|
1236
|
+
parser.add_argument("-ic", "--input-codec", type=str, default="h264",
|
|
1237
|
+
choices=["h264", "hevc", "av1"],
|
|
1238
|
+
help="Input codec to use for generating the video. Default is h264")
|
|
1239
|
+
parser.add_argument("-c", "--codec", type=str, default="h264",
|
|
1240
|
+
choices=["h264", "hevc", "av1"],
|
|
1241
|
+
help="Output codec to use for transcoding. Default is h264")
|
|
1242
|
+
parser.add_argument("-ibf", "--input-bframes", type=int, default=2,
|
|
1243
|
+
help="Number of B-frames for input video generation. Default is 2")
|
|
1244
|
+
parser.add_argument("-bf", "--bframes", type=int, default=0,
|
|
1245
|
+
help="Number of B-frames for output transcoding. Default is 0")
|
|
1246
|
+
parser.add_argument("-n", "--numthreads", type=int, default=1, help="Number of threads. Default is 1")
|
|
1247
|
+
parser.add_argument("-s", "--segments", type=int, default=20, help="Number of random segments. Default is 20")
|
|
1248
|
+
parser.add_argument("-u", "--usage", type=int, nargs='*', default=None,
|
|
1249
|
+
help="Transcoding method (0: PyNVC, 1: FFmpeg without map, 2: FFmpeg stream-aware, 3: FFmpeg frame-by-frame). Default is [0, 1, 2, 3]")
|
|
1250
|
+
parser.add_argument("--chunk-size", type=int, default=5, help="Number of segments to process in a single FFmpeg command. Default is 5")
|
|
1251
|
+
parser.add_argument("--segment-duration", type=int, default=5, help="Duration of each segment in seconds. Default is 5")
|
|
1252
|
+
parser.add_argument("--log", type=str, help="Path to save log file")
|
|
1253
|
+
parser.add_argument("--random-seed", type=int, help="Random seed for reproducible segment generation")
|
|
1254
|
+
parser.add_argument("--gop-size", type=int, default=250, help="GOP size for encoding. Default is 250")
|
|
1255
|
+
parser.add_argument("-i", "--input", type=str, help="Input video file (instead of generating using ffmpeg)")
|
|
1256
|
+
parser.add_argument("--ffmpeg-dir", type=str, required=True,
|
|
1257
|
+
help="Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).")
|
|
1258
|
+
parser.add_argument("--replay", type=str, help="Path to log file for replay")
|
|
1259
|
+
|
|
1260
|
+
args = parser.parse_args()
|
|
1261
|
+
|
|
1262
|
+
# Set default usages if none provided
|
|
1263
|
+
if args.usage is None:
|
|
1264
|
+
args.usage = [0, 1, 2, 3]
|
|
1265
|
+
|
|
1266
|
+
return args
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
if __name__ == "__main__":
|
|
1270
|
+
script_start_time = time.perf_counter()
|
|
1271
|
+
|
|
1272
|
+
args = parse_args()
|
|
1273
|
+
|
|
1274
|
+
# Resolve FFmpeg and FFprobe executables from the required installation directory
|
|
1275
|
+
bin_dir = os.path.join(args.ffmpeg_dir, "bin")
|
|
1276
|
+
if platform.system() == "Windows":
|
|
1277
|
+
ffmpeg_exe = os.path.join(bin_dir, "ffmpeg.exe")
|
|
1278
|
+
if not os.path.isfile(ffmpeg_exe):
|
|
1279
|
+
ffmpeg_exe = os.path.join(bin_dir, "ffmpeg")
|
|
1280
|
+
ffprobe_exe = os.path.join(bin_dir, "ffprobe.exe")
|
|
1281
|
+
if not os.path.isfile(ffprobe_exe):
|
|
1282
|
+
ffprobe_exe = os.path.join(bin_dir, "ffprobe")
|
|
1283
|
+
else:
|
|
1284
|
+
ffmpeg_exe = os.path.join(bin_dir, "ffmpeg")
|
|
1285
|
+
ffprobe_exe = os.path.join(bin_dir, "ffprobe")
|
|
1286
|
+
if not os.path.isfile(ffmpeg_exe):
|
|
1287
|
+
expected = "bin/ffmpeg.exe or bin/ffmpeg" if platform.system() == "Windows" else "bin/ffmpeg"
|
|
1288
|
+
raise FileNotFoundError(
|
|
1289
|
+
f"FFmpeg executable not found at {ffmpeg_exe}. "
|
|
1290
|
+
f"The given FFmpeg installation directory must contain {expected}."
|
|
1291
|
+
)
|
|
1292
|
+
if not os.path.isfile(ffprobe_exe):
|
|
1293
|
+
expected = "bin/ffprobe.exe or bin/ffprobe" if platform.system() == "Windows" else "bin/ffprobe"
|
|
1294
|
+
raise FileNotFoundError(
|
|
1295
|
+
f"FFprobe executable not found at {ffprobe_exe}. "
|
|
1296
|
+
f"The given FFmpeg installation directory must contain {expected}."
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
# Create logs directory if it doesn't exist
|
|
1300
|
+
os.makedirs("logs", exist_ok=True)
|
|
1301
|
+
|
|
1302
|
+
# Generate default log filename if not specified
|
|
1303
|
+
if not args.log and not args.replay:
|
|
1304
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1305
|
+
args.log = f"logs/run_{timestamp}.json"
|
|
1306
|
+
|
|
1307
|
+
# Initialize log data
|
|
1308
|
+
log_data["timestamp"] = datetime.datetime.now().isoformat()
|
|
1309
|
+
log_data["args"] = vars(args)
|
|
1310
|
+
|
|
1311
|
+
# Replay mode
|
|
1312
|
+
if args.replay:
|
|
1313
|
+
# Open log file in append mode to add replay results
|
|
1314
|
+
replay_log_path = f"logs/replay_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
1315
|
+
with open(replay_log_path, 'w') as log_file:
|
|
1316
|
+
log_data = {
|
|
1317
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
|
1318
|
+
"replay_source": args.replay,
|
|
1319
|
+
"steps": [],
|
|
1320
|
+
"results": {}
|
|
1321
|
+
}
|
|
1322
|
+
json.dump(log_data, log_file, indent=2)
|
|
1323
|
+
replay_from_log(args.replay, ffmpeg_exe, ffprobe_exe)
|
|
1324
|
+
else:
|
|
1325
|
+
# Normal execution mode
|
|
1326
|
+
# Open log file
|
|
1327
|
+
log_file = open(args.log, 'w')
|
|
1328
|
+
json.dump(log_data, log_file, indent=2)
|
|
1329
|
+
|
|
1330
|
+
print(f"Logging to: {args.log}")
|
|
1331
|
+
|
|
1332
|
+
# In the video generation part
|
|
1333
|
+
if not args.input:
|
|
1334
|
+
# Generate the Mandelbrot video with the specified input codec
|
|
1335
|
+
# Use a simple base name - generate_mandelbrot_video will add codec/resolution suffix
|
|
1336
|
+
base_video_path = "source_videos/mandelbrot.mp4"
|
|
1337
|
+
os.makedirs("source_videos", exist_ok=True)
|
|
1338
|
+
|
|
1339
|
+
# Construct the expected filename (matches the naming in generate_mandelbrot_video)
|
|
1340
|
+
expected_video_path = f"source_videos/mandelbrot_{args.input_codec}_{args.width}x{args.height}_{args.duration}s_{args.fps}fps_gop{args.gop_size}_bf{args.input_bframes}.mp4"
|
|
1341
|
+
|
|
1342
|
+
# Check if video already exists
|
|
1343
|
+
if os.path.exists(expected_video_path):
|
|
1344
|
+
print("=" * 80)
|
|
1345
|
+
print(f"STEP 1: USING EXISTING MANDELBROT VIDEO")
|
|
1346
|
+
print(f"Found existing video: {expected_video_path}")
|
|
1347
|
+
print("=" * 80)
|
|
1348
|
+
video_path = expected_video_path
|
|
1349
|
+
log_step("video_reuse", f"Reusing existing video: {expected_video_path}",
|
|
1350
|
+
params={"width": args.width, "height": args.height, "duration": args.duration,
|
|
1351
|
+
"fps": args.fps, "codec": args.input_codec, "bframes": args.input_bframes,
|
|
1352
|
+
"output_path": expected_video_path, "gop_size": args.gop_size})
|
|
1353
|
+
else:
|
|
1354
|
+
print("=" * 80)
|
|
1355
|
+
print(f"STEP 1: GENERATING MANDELBROT VIDEO WITH {args.input_codec.upper()} CODEC")
|
|
1356
|
+
print(f"Using {args.input_bframes} B-frames for input video generation")
|
|
1357
|
+
print("=" * 80)
|
|
1358
|
+
|
|
1359
|
+
video_path = generate_mandelbrot_video(
|
|
1360
|
+
args.width, args.height, args.duration, args.fps,
|
|
1361
|
+
base_video_path, args.gop_size, args.input_codec, args.input_bframes,
|
|
1362
|
+
ffmpeg_path=ffmpeg_exe
|
|
1363
|
+
)
|
|
1364
|
+
else:
|
|
1365
|
+
# User provided an input video
|
|
1366
|
+
video_path = args.input
|
|
1367
|
+
print("=" * 80)
|
|
1368
|
+
print(f"STEP 1: USING USER-PROVIDED INPUT VIDEO")
|
|
1369
|
+
print(f"Input video: {video_path}")
|
|
1370
|
+
print("=" * 80)
|
|
1371
|
+
log_step("video_input", f"Using user-provided input video: {video_path}",
|
|
1372
|
+
params={"input_path": video_path})
|
|
1373
|
+
|
|
1374
|
+
# Configure encoding parameters based on output codec
|
|
1375
|
+
config = {
|
|
1376
|
+
"preset": args.preset.upper(),
|
|
1377
|
+
"width": args.width,
|
|
1378
|
+
"height": args.height,
|
|
1379
|
+
"codec": args.codec.lower(), # This is the output codec
|
|
1380
|
+
"tuning_info": "high_quality",
|
|
1381
|
+
"bf": str(args.bframes), # Use the bframes parameter from args
|
|
1382
|
+
"gop": str(args.gop_size)
|
|
1383
|
+
}
|
|
1384
|
+
log_data["args"]["config"] = config
|
|
1385
|
+
|
|
1386
|
+
# Set random seed if provided for reproducible segment generation
|
|
1387
|
+
if args.random_seed is not None:
|
|
1388
|
+
random.seed(args.random_seed)
|
|
1389
|
+
log_step("initialization", f"Set random seed to {args.random_seed}")
|
|
1390
|
+
|
|
1391
|
+
# Initialize CUDA
|
|
1392
|
+
cuda.init()
|
|
1393
|
+
log_step("initialization", "Initialized CUDA")
|
|
1394
|
+
|
|
1395
|
+
# Create random segments
|
|
1396
|
+
print("\n" + "=" * 80)
|
|
1397
|
+
print("STEP 2: CREATING RANDOM SEGMENTS")
|
|
1398
|
+
print("=" * 80)
|
|
1399
|
+
|
|
1400
|
+
clip_start_end = generate_random_segments(args.duration, args.segments, min_segment_duration=args.segment_duration)
|
|
1401
|
+
|
|
1402
|
+
# Log segment creation
|
|
1403
|
+
segment_data = [{"start": start, "end": end} for start, end in clip_start_end]
|
|
1404
|
+
log_step("segment_creation", f"Created {len(clip_start_end)} random segments",
|
|
1405
|
+
params={"num_segments": args.segments, "min_duration": 1.0, "duration": args.duration},
|
|
1406
|
+
results={"segments": segment_data})
|
|
1407
|
+
|
|
1408
|
+
# Print segments
|
|
1409
|
+
print(f"Created {len(clip_start_end)} random segments:")
|
|
1410
|
+
for i, (start, end) in enumerate(clip_start_end):
|
|
1411
|
+
print(f" Segment {i+1}: {start:.2f}s - {end:.2f}s (duration: {end-start:.2f}s)")
|
|
1412
|
+
|
|
1413
|
+
# Calculate total frames to transcode
|
|
1414
|
+
segment_frames = sum((end - start) * args.fps for start, end in clip_start_end)
|
|
1415
|
+
|
|
1416
|
+
# Store results for comparison
|
|
1417
|
+
results = {}
|
|
1418
|
+
|
|
1419
|
+
# Run transcoding for each usage mode
|
|
1420
|
+
for usage in args.usage:
|
|
1421
|
+
if usage not in [0, 1, 2, 3]:
|
|
1422
|
+
print(f"Warning: Invalid usage mode {usage}. Skipping...")
|
|
1423
|
+
log_step("warning", f"Invalid usage mode {usage}, skipping")
|
|
1424
|
+
continue
|
|
1425
|
+
|
|
1426
|
+
print("\n" + "=" * 80)
|
|
1427
|
+
print(f"STEP 3: TRANSCODING SEGMENTS USING MODE {usage}")
|
|
1428
|
+
print("=" * 80)
|
|
1429
|
+
|
|
1430
|
+
# Get mode description
|
|
1431
|
+
mode_desc = {
|
|
1432
|
+
0: "PyNVC transcoding",
|
|
1433
|
+
1: "FFmpeg without map",
|
|
1434
|
+
2: "FFmpeg with map (no audio)",
|
|
1435
|
+
3: "FFmpeg with map (with audio)"
|
|
1436
|
+
}.get(usage, "Unknown mode")
|
|
1437
|
+
|
|
1438
|
+
print(f"Running: {mode_desc}")
|
|
1439
|
+
|
|
1440
|
+
# Log transcoding start
|
|
1441
|
+
log_step("transcoding", f"Starting transcoding with mode {usage} ({mode_desc})",
|
|
1442
|
+
params={"usage": usage, "mode": mode_desc, "numthreads": args.numthreads,
|
|
1443
|
+
"chunk_size": args.chunk_size, "video_path": video_path})
|
|
1444
|
+
clip_chunks = split_list(args.numthreads, clip_start_end)
|
|
1445
|
+
threads = []
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
starttime = time.time()
|
|
1449
|
+
|
|
1450
|
+
for m in range(0, args.numthreads):
|
|
1451
|
+
t = threading.Thread(target=segmented_transcode, args=([video_path,
|
|
1452
|
+
clip_chunks[m],
|
|
1453
|
+
config,
|
|
1454
|
+
args.fps,
|
|
1455
|
+
usage,
|
|
1456
|
+
args.chunk_size,
|
|
1457
|
+
ffmpeg_exe,
|
|
1458
|
+
ffprobe_exe],))
|
|
1459
|
+
t.start()
|
|
1460
|
+
#
|
|
1461
|
+
threads.append(t)
|
|
1462
|
+
|
|
1463
|
+
for t in threads:
|
|
1464
|
+
t.join()
|
|
1465
|
+
|
|
1466
|
+
endtime = time.time()
|
|
1467
|
+
total_time = endtime - starttime
|
|
1468
|
+
throughput = segment_frames / total_time if total_time > 0 else 0
|
|
1469
|
+
|
|
1470
|
+
# Store results
|
|
1471
|
+
results[usage] = {
|
|
1472
|
+
'mode': mode_desc,
|
|
1473
|
+
'time': total_time,
|
|
1474
|
+
'throughput': throughput
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
# Log transcoding completion
|
|
1478
|
+
log_step("transcoding_complete", f"Completed transcoding with mode {usage}",
|
|
1479
|
+
params={"usage": usage, "mode": mode_desc},
|
|
1480
|
+
results={"time": total_time, "throughput": throughput})
|
|
1481
|
+
|
|
1482
|
+
print(f"Completed {mode_desc}")
|
|
1483
|
+
print(f"Processing time: {total_time:.2f} seconds")
|
|
1484
|
+
print(f"Throughput: {throughput:.2f} frames/second")
|
|
1485
|
+
|
|
1486
|
+
# Store results in log data
|
|
1487
|
+
log_data["results"] = {str(usage): {"time": data["time"], "throughput": data["throughput"]}
|
|
1488
|
+
for usage, data in results.items()}
|
|
1489
|
+
|
|
1490
|
+
# Print comparison results with additional details
|
|
1491
|
+
print("\n" + "=" * 80)
|
|
1492
|
+
print("COMPARISON RESULTS")
|
|
1493
|
+
print("=" * 80)
|
|
1494
|
+
|
|
1495
|
+
# Add video details above the table
|
|
1496
|
+
print(f"Video Resolution: {args.width}x{args.height}")
|
|
1497
|
+
print(f"Codec: {args.codec}")
|
|
1498
|
+
print(f"Duration: {args.duration} seconds")
|
|
1499
|
+
print(f"Number of Segments: {len(clip_start_end)}")
|
|
1500
|
+
print(f"GOP Size: {args.gop_size}")
|
|
1501
|
+
print()
|
|
1502
|
+
|
|
1503
|
+
print(f"{'Mode':<30} {'Time (s)':<15} {'Throughput (fps)':<20}")
|
|
1504
|
+
print("-" * 65)
|
|
1505
|
+
|
|
1506
|
+
for usage, data in sorted(results.items()):
|
|
1507
|
+
print(f"{data['mode']:<30} {data['time']:<15.2f} {data['throughput']:<20.2f}")
|
|
1508
|
+
|
|
1509
|
+
# Find fastest method
|
|
1510
|
+
if results:
|
|
1511
|
+
fastest = min(results.items(), key=lambda x: x[1]['time'])
|
|
1512
|
+
highest_throughput = max(results.items(), key=lambda x: x[1]['throughput'])
|
|
1513
|
+
|
|
1514
|
+
print("\nResults Summary:")
|
|
1515
|
+
print(f"Total frames transcoded: {segment_frames:.0f}")
|
|
1516
|
+
print(f"Fastest method: {results[fastest[0]]['mode']} ({fastest[1]['time']:.2f}s)")
|
|
1517
|
+
print(f"Highest throughput: {results[highest_throughput[0]]['mode']} ({highest_throughput[1]['throughput']:.2f} fps)")
|
|
1518
|
+
|
|
1519
|
+
# Log summary with additional details
|
|
1520
|
+
log_step("summary", "Transcoding summary",
|
|
1521
|
+
results={
|
|
1522
|
+
"resolution": f"{args.width}x{args.height}",
|
|
1523
|
+
"codec": args.codec,
|
|
1524
|
+
"duration": args.duration,
|
|
1525
|
+
"num_segments": len(clip_start_end),
|
|
1526
|
+
"gop_size": args.gop_size,
|
|
1527
|
+
"total_frames": segment_frames,
|
|
1528
|
+
"fastest_method": results[fastest[0]]['mode'],
|
|
1529
|
+
"fastest_time": fastest[1]['time'],
|
|
1530
|
+
"highest_throughput_method": results[highest_throughput[0]]['mode'],
|
|
1531
|
+
"highest_throughput": highest_throughput[1]['throughput']
|
|
1532
|
+
})
|
|
1533
|
+
|
|
1534
|
+
print("=" * 80)
|
|
1535
|
+
|
|
1536
|
+
# Close log file
|
|
1537
|
+
if log_file:
|
|
1538
|
+
log_file.close()
|
|
1539
|
+
print(f"Log saved to: {args.log}")
|
|
1540
|
+
|
|
1541
|
+
# Display total execution time
|
|
1542
|
+
total_time = time.perf_counter() - script_start_time
|
|
1543
|
+
hours = int(total_time // 3600)
|
|
1544
|
+
minutes = int((total_time % 3600) // 60)
|
|
1545
|
+
seconds = total_time % 60
|
|
1546
|
+
print(f"\nTotal execution time: {hours:02d}:{minutes:02d}:{seconds:06.3f} ({total_time:.3f} seconds)")
|