PyNvVideoCodec 2.2.1__cp312-cp312-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. PyNvVideoCodec/PyNvVideoCodec_121.cp312-win_arm64.pyd +0 -0
  2. PyNvVideoCodec/PyNvVideoCodec_130.cp312-win_arm64.pyd +0 -0
  3. PyNvVideoCodec/VersionCheck.cp312-win_arm64.pyd +0 -0
  4. PyNvVideoCodec/__init__.py +251 -0
  5. PyNvVideoCodec/avformat-62.dll +0 -0
  6. PyNvVideoCodec/avutil-60.dll +0 -0
  7. PyNvVideoCodec/decoders/SimpleDecoder.py +273 -0
  8. PyNvVideoCodec/decoders/ThreadedDecoder.py +129 -0
  9. PyNvVideoCodec/decoders/__init__.py +22 -0
  10. PyNvVideoCodec/transcoder/Transcoder.py +55 -0
  11. PyNvVideoCodec/transcoder/__init__.py +22 -0
  12. benchmarks/cached_decoder_benchmark.py +813 -0
  13. benchmarks/frame_sampling_benchmark.py +765 -0
  14. benchmarks/requirements.txt +52 -0
  15. benchmarks/segmented_transcode_benchmark.py +1546 -0
  16. benchmarks/setup_pycuda_win_arm64.bat +531 -0
  17. pynvvideocodec-2.2.1.data/data/external/ffmpeg/readme.txt +39 -0
  18. pynvvideocodec-2.2.1.data/data/external/ffmpeg/src/ffmpeg-8.1.2.tar.xz +0 -0
  19. pynvvideocodec-2.2.1.dist-info/METADATA +97 -0
  20. pynvvideocodec-2.2.1.dist-info/RECORD +63 -0
  21. pynvvideocodec-2.2.1.dist-info/WHEEL +5 -0
  22. pynvvideocodec-2.2.1.dist-info/licenses/LICENSE.txt +22 -0
  23. pynvvideocodec-2.2.1.dist-info/licenses/NOTICES.txt +167 -0
  24. pynvvideocodec-2.2.1.dist-info/top_level.txt +3 -0
  25. samples/advanced/decode.py +173 -0
  26. samples/advanced/decode_from_memory_buffer.py +204 -0
  27. samples/advanced/decode_perf.py +299 -0
  28. samples/advanced/decode_reconfigure.py +184 -0
  29. samples/advanced/decode_sei_msg.py +193 -0
  30. samples/advanced/decode_with_cuda_control.py +214 -0
  31. samples/advanced/decode_with_low_latency.py +183 -0
  32. samples/advanced/encode_config.json +12 -0
  33. samples/advanced/encode_config_lowlatency.json +13 -0
  34. samples/advanced/encode_perf.py +338 -0
  35. samples/advanced/encode_reconfigure.py +211 -0
  36. samples/advanced/encode_sei_msg.py +188 -0
  37. samples/advanced/simple_decode_from_memory.py +151 -0
  38. samples/advanced/simple_decode_stats.py +179 -0
  39. samples/advanced/transcode.py +288 -0
  40. samples/basic/create_video_segments.py +187 -0
  41. samples/basic/encode.py +574 -0
  42. samples/basic/segments.txt +3 -0
  43. samples/basic/simple_decode_sampling.py +182 -0
  44. samples/basic/transcode_config.json +6 -0
  45. samples/jupyter/ReconfigureDecoder.png +0 -0
  46. samples/jupyter/SimpleDecoderAPIs.png +0 -0
  47. samples/jupyter/ThreadedDecoder.png +0 -0
  48. samples/jupyter/coco_classes.pickle +0 -0
  49. samples/jupyter/face_protect_tutorial.ipynb +881 -0
  50. samples/jupyter/object_detection_tutorial.ipynb +781 -0
  51. samples/jupyter/simple_decode_tutorial.ipynb +674 -0
  52. samples/requirements.txt +84 -0
  53. samples/setup_pycuda_win_arm64.bat +531 -0
  54. samples/utils/Utils.py +405 -0
  55. samples/utils/__init__.py +36 -0
  56. samples/utils/decode_parser.py +642 -0
  57. samples/utils/decode_stats_writer.py +177 -0
  58. samples/utils/encode_parallel_utils.py +174 -0
  59. samples/utils/encode_parser.py +379 -0
  60. samples/utils/frame_utils.py +89 -0
  61. samples/utils/sei_message_parser.py +223 -0
  62. samples/utils/sei_parser.py +223 -0
  63. samples/utils/transcode_parser.py +218 -0
@@ -0,0 +1,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)")