PyNvVideoCodec 2.0.0__cp313-cp313-win_amd64.whl

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