#!/usr/bin/env python3 import argparse import subprocess import ffmpeg import os import re import csv import zipfile import sys from collections import defaultdict from benchmark_multi_inst import run_multi_instance_benchmark # --- 配置数据 --- DECODE_FILE_ARR = [ ["sample_1920x1080_30fps_8bit.h264", "1", "812", "H264", "8", "1920x1080"], ] ENCODE_FILE_ARR = [ ["1280x720_nv12.yuv", "677", "H264", "8", "1280x720"], ["1920x1080_nv12.yuv", "812", "H264", "8", "1920x1080"], ["3840x2160_nv12.yuv", "812", "H264", "8", "3840x2160"], ["7680x4320_nv12.yuv", "77", "H264", "8", "7680x4320"], ["1280x720_nv12.yuv", "677", "HEVC", "8", "1280x720"], ["1920x1080_nv12.yuv", "812", "HEVC", "8", "1920x1080"], ["3840x2160_nv12.yuv", "812", "HEVC", "8", "3840x2160"], ["7680x4320_nv12.yuv", "77", "HEVC", "8", "7680x4320"], ["1280x720_p010le.yuv", "677", "HEVC", "10", "1280x720"], ["1920x1080_p010le.yuv", "812", "HEVC", "10", "1920x1080"], ["3840x2160_p010le.yuv", "812", "HEVC", "10", "3840x2160"], ["1920x1080_nv12.yuv", "1", "JPEG_1", "8", "1920x1080"], ["3840x2160_nv12.yuv", "1", "JPEG_1", "8", "3840x2160"], ["4096x2160_nv12.yuv", "1", "JPEG_1", "8", "4096x2160"], ["8192x4320_nv12.yuv", "1", "JPEG_1", "8", "8192x4320"], ["1920x1080_nv12.yuv", "2174", "JPEG", "8", "1920x1080"], ["1920x1080_nv12.yuv", "1701", "JPEG", "8", "1920x1080"], ["1920x1080_nv12.yuv", "2076", "JPEG", "8", "1920x1080"], ["1920x1080_nv12.yuv", "1581", "JPEG", "8", "1920x1080"], ["1920x1080_nv12.yuv", "2154", "JPEG", "8", "1920x1080"], ] # 编解码器映射(键为小写) NVENC_ENCODER_NAMES = { "h264": "h264_nvenc", "hevc": "hevc_nvenc", "vp9": "vp9_nvenc", "mjpeg": "mjpeg_nvenc", } VAAPI_ENCODER_NAMES = { "h264": "h264_vaapi", "hevc": "hevc_vaapi", "vp9": "vp9_vaapi", "mjpeg": "mjpeg_vaapi", } V4L2_CODEC_NAMES = { "h264": "h264_v4l2m2m", "hevc": "hevc_v4l2m2m", "vp9": "vp9_v4l2m2m", "avs2": "avs2_v4l2m2m", "mjpeg": "mjpeg_v4l2m2m", } CUVID_CODEC_NAMES = { "h264": "h264_cuvid", "hevc": "hevc_cuvid", "vp9": "vp9_cuvid", "avs2": "avs2_cuvid", "mjpeg": "mjpeg_cuvid", } # --- 辅助函数 --- def parse_ffmpeg_benchmark_times(log_content): """解析 ffmpeg 日志获取所有 rtime 值(秒)""" times = [] # 匹配 rtime=0.123456 (benchmark 输出的浮点秒数) time_matches = re.findall(r'rtime=(\d+\.?\d*)', log_content) for time_str in time_matches: try: total_sec = float(time_str) times.append(total_sec) except ValueError: continue # 备选:匹配 time=00:00:00.00 (时间戳格式) if not times: timestamp_matches = re.findall(r'time=(\d+:\d+:\d+\.\d+)', log_content) for time_str in timestamp_matches: parts = time_str.split(':') if len(parts) == 3: h, m, s = parts s_clean = s.rstrip('s') try: total_sec = float(h) * 3600 + float(m) * 60 + float(s_clean) times.append(total_sec) except ValueError: continue return times def parse_ffmpeg_log_frames(log_content): """获取总帧数""" frame_matches = re.findall(r'frame=\s*(\d+)', log_content) if frame_matches: return int(frame_matches[-1]) return 0 def calculate_fps(frames, rtime): if rtime > 0: return round(frames / rtime, 2) return 0.0 def run_ffmpeg_command(cmd, log_file_path): """运行 ffmpeg 命令并将 stderr 保存到日志文件(即使失败也创建文件)""" try: with open(log_file_path, 'w') as log_f: process = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=log_f, text=True) return process.returncode except Exception as e: # 即使出错也创建日志文件 with open(log_file_path, 'w') as log_f: log_f.write(f"Error: {e}\n") return -1 def extract_zip(): video_benchmark_dir = "./video_benchmark" zip_file = "video_benchmark.zip" if not os.path.exists(video_benchmark_dir): if os.path.exists(zip_file): print(f"Extracting {zip_file}...") with zipfile.ZipFile(zip_file, 'r') as zip_ref: zip_ref.extractall(video_benchmark_dir) else: print(f"Warning: {zip_file} not found. Ensure test files exist in {video_benchmark_dir}") else: print("video_benchmark dir exist") def get_decoder_args(codec_type, file_ext): """获取解码器参数""" file_ext_lower = file_ext.lower() if codec_type == "cuda": return {"hwaccel": "cuda"} elif codec_type == "vaapi": return {"hwaccel": "vaapi"} elif codec_type == "v4l2": decoder = V4L2_CODEC_NAMES.get(file_ext_lower, file_ext_lower) return {"vcodec": decoder} elif codec_type == "cuvid": decoder = CUVID_CODEC_NAMES.get(file_ext_lower, file_ext_lower) return {"vcodec": decoder} return {} def get_encoder_name(codec_type, file_ext): """获取编码器名称(修复大小写问题)""" # 转换为小写 file_ext_lower = file_ext.lower() # 特殊处理:H264 -> h264, HEVC -> hevc, JPEG -> mjpeg if file_ext_lower in ["h264", "hevc", "vp9", "avs2"]: codec_key = file_ext_lower elif file_ext_lower in ["jpg", "jpeg", "avi", "mjpeg"]: codec_key = "mjpeg" else: codec_key = file_ext_lower if codec_type == "cuda": return NVENC_ENCODER_NAMES.get(codec_key, "") elif codec_type == "vaapi": return VAAPI_ENCODER_NAMES.get(codec_key, "") elif codec_type == "v4l2": return V4L2_CODEC_NAMES.get(codec_key, "") elif codec_type == "cuvid": return CUVID_CODEC_NAMES.get(codec_key, "") return "" def run_single_decode_benchmark(input_file, codec_type, log_file): """运行单次解码测试""" file_ext = os.path.splitext(input_file)[1].replace('.', '') input_kwargs = get_decoder_args(codec_type, file_ext) try: stream = ffmpeg.input(input_file, **input_kwargs) cmd = ffmpeg.output(stream, 'pipe:', format='null').global_args('-benchmark', '-y').compile() run_ffmpeg_command(cmd, log_file) return True except Exception as e: print(f"FFmpeg command construction failed: {e}") return False def run_encode_benchmark(yuv_file, output_file, resolution, codec_type, file_ext, depth, log_file): """运行编码测试""" w, h = resolution.split('x') pix_fmt = "p010le" if "10" in str(depth) else "nv12" profile = "main10" if "10" in str(depth) else None # 修复:获取编码器名称(处理大小写) encoder_name = get_encoder_name(codec_type, file_ext) if not encoder_name: print(f"Unknown encoder for {file_ext} on {codec_type}") # 即使失败也创建日志文件 with open(log_file, 'w') as f: f.write(f"Error: Unknown encoder for {file_ext} on {codec_type}\n") return False try: input_kwargs = { 'format': 'rawvideo', 's': f"{w}x{h}", 'pix_fmt': pix_fmt } global_args = ['-benchmark', '-y'] output_kwargs = {'vcodec': encoder_name} if codec_type == "vaapi": input_kwargs['hwaccel'] = 'vaapi' global_args.extend(['-vaapi_device', '/dev/dri/renderD128']) output_kwargs['vf'] = 'format=nv12,hwupload' if pix_fmt == 'nv12' else 'format=p010le,hwupload' if profile: output_kwargs['profile'] = profile stream = ffmpeg.input(yuv_file, **input_kwargs) cmd = ffmpeg.output(stream, output_file, **output_kwargs).global_args(*global_args).compile() run_ffmpeg_command(cmd, log_file) return True except Exception as e: print(f"Encode command failed: {e}") # 即使失败也创建日志文件 with open(log_file, 'w') as f: f.write(f"Error: {e}\n") return False def aggregate_results(temp_csv, final_csv, is_encode=False, threads=6): """聚合 CSV 结果""" data = defaultdict(lambda: {"sum_1core": 0.0, "sum_multi": 0.0, "count": 0}) try: with open(temp_csv, 'r', newline='') as f: reader = csv.reader(f) header = next(reader) for row in reader: if len(row) < 5: continue fmt = row[2].strip() depth = row[3].strip() res = row[4].strip() try: fps_1 = float(row[5]) if len(row) > 5 else 0.0 fps_multi = float(row[6]) if (not is_encode and len(row) > 6) else 0.0 except ValueError: fps_1 = 0.0 fps_multi = 0.0 key = f"{fmt},{depth},{res}" data[key]["sum_1core"] += fps_1 data[key]["sum_multi"] += fps_multi data[key]["count"] += 1 except FileNotFoundError: print(f"Warning: {temp_csv} not found.") return with open(final_csv, 'w', newline='') as f: writer = csv.writer(f) if is_encode: writer.writerow(["Encode", "Bit Depth", "Resolution", "Fps/(BMZ 1 Core)"]) for key, val in data.items(): if val["count"] > 0: avg_1 = val["sum_1core"] / val["count"] parts = key.split(',') writer.writerow([parts[0], parts[1], parts[2], f"{avg_1:.2f}"]) else: writer.writerow(["Decode", "Bit Depth", "Resolution", "Fps/(BMZ 1 Core)", f"Fps/(BMZ {threads} Processes)"]) for key, val in data.items(): if val["count"] > 0: avg_1 = val["sum_1core"] / val["count"] avg_multi = val["sum_multi"] / val["count"] parts = key.split(',') writer.writerow([parts[0], parts[1], parts[2], f"{avg_1:.2f}", f"{avg_multi:.2f}"]) def main(): parser = argparse.ArgumentParser(description="Video Benchmark Script") parser.add_argument("-c", "--codec", default="cuda", choices=["cuda", "cuvid", "v4l2", "vaapi"], help="Codec backend") parser.add_argument("-n", "--threads", type=int, default=6, help="Number of concurrent processes") args = parser.parse_args() CODEC = args.codec THREADS = args.threads video_decode_benchmark_temp = f"video_{CODEC}_decode_benchmark_temp.csv" video_decode_benchmark = f"video_{CODEC}_decode_benchmark.csv" video_encode_benchmark_temp = f"video_{CODEC}_encode_benchmark_temp.csv" video_encode_benchmark = f"video_{CODEC}_encode_benchmark.csv" for f in [video_decode_benchmark_temp, video_encode_benchmark_temp]: if os.path.exists(f): os.remove(f) with open(video_decode_benchmark_temp, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(["Test File", "Total Frames", "Decode", "Bit Depth", "Resolution", "Fps/(BMZ 1 Core)", f"Fps/(BMZ {THREADS} Processes)"]) with open(video_encode_benchmark_temp, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(["Test File", "Total Frames", "Encode", "Bit Depth", "Resolution", "Fps/(BMZ 1 Core)"]) extract_zip() base_dir = "./video_benchmark" if not DECODE_FILE_ARR: print("Error: DECODE_FILE_ARR is empty. Please uncomment test files in script.") return for i, file_info in enumerate(DECODE_FILE_ARR): filename, encode_idx, frames, format_name, depth, resolution = file_info filepath = os.path.join(base_dir, filename) if not os.path.exists(filepath): print(f"Skip missing file: {filepath}") continue file_ext = os.path.splitext(filename)[1].replace('.', '') base_name = os.path.splitext(filename)[0] # 中间生成的 YUV 文件路径 yuv_filename = os.path.join(base_dir, base_name + ".yuv") decode_log = os.path.join(base_dir, base_name + ".log") multi_log = os.path.join(base_dir, base_name + "_multi.log") encode_log = os.path.join(base_dir, base_name + "_encode.log") # --- 修改点 1: 编码输出文件改为临时文件,避免覆盖原文件 --- # 原代码:encode_result = os.path.join(base_dir, filename) # 新代码:添加 _encoded_tmp 后缀,确保不与原文件冲突 encode_result = os.path.join(base_dir, f"{base_name}_encoded_tmp.{file_ext}") print(f"Processing: {filename} (Idx: {encode_idx})") # --- 解码测试 --- success = run_single_decode_benchmark(filepath, CODEC, decode_log) fps_single = 0.0 fps_multi = 0.0 error_occurred = False if not success or not os.path.exists(decode_log) or os.path.getsize(decode_log) == 0: error_occurred = True print(f" -> Single instance failed or empty log") else: with open(decode_log, 'r') as f: log_content = f.read() d_times = parse_ffmpeg_benchmark_times(log_content) d_frames = parse_ffmpeg_log_frames(log_content) d_rtime = d_times[0] if d_times else 0.0 if d_rtime > 0: fps_single = calculate_fps(d_frames, d_rtime) else: error_occurred = True print(f" -> Failed to parse rtime from single log") # 多实例 if not error_occurred: run_multi_instance_benchmark(filepath, CODEC, THREADS, multi_log) with open(multi_log, 'r') as f: multi_log_content = f.read() m_times = parse_ffmpeg_benchmark_times(multi_log_content) if m_times: avg_rtime = sum(m_times) / len(m_times) fps_multi = calculate_fps(d_frames, avg_rtime) * THREADS else: fps_multi = 0.0 print(f" -> Failed to parse rtime from multi log") if error_occurred: fps_single = 0.0 fps_multi = 0.0 with open(video_decode_benchmark_temp, 'a', newline='') as f: writer = csv.writer(f) writer.writerow([filename, frames, format_name.strip(), depth.strip(), resolution.strip(), fps_single, fps_multi]) # --- 编码测试 --- if encode_idx != "n/a" and not error_occurred: try: idx = int(encode_idx) if 0 <= idx < len(ENCODE_FILE_ARR): enc_info = ENCODE_FILE_ARR[idx] enc_yuv_name = enc_info[0] enc_frames = enc_info[1] enc_format = enc_info[2] enc_depth = enc_info[3] enc_resolution = enc_info[4] print(f"Encoding: {filename} -> {enc_format}") # 生成 YUV(使用与解码测试相同的解码器参数) if not os.path.exists(yuv_filename): try: input_kwargs = get_decoder_args(CODEC, file_ext) stream = ffmpeg.input(filepath, **input_kwargs) cmd = ffmpeg.output(stream, yuv_filename, pix_fmt='nv12').global_args('-y').compile() subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as e: print(f"YUV conversion failed: {e}") enc_fps = 0.0 with open(video_encode_benchmark_temp, 'a', newline='') as f: csv.writer(f).writerow([enc_yuv_name, enc_frames, enc_format.strip(), enc_depth.strip(), enc_resolution.strip(), enc_fps]) continue # 编码 - 使用 Encode_File_Arr 中的格式 # 注意:输出文件现在是 encode_result (临时文件),不会覆盖 filepath (原文件) run_encode_benchmark(yuv_filename, encode_result, enc_resolution, CODEC, enc_format.strip(), enc_depth, encode_log) # 检查日志文件是否存在 if os.path.exists(encode_log): with open(encode_log, 'r') as f: enc_log_content = f.read() e_frames = parse_ffmpeg_log_frames(enc_log_content) e_times = parse_ffmpeg_benchmark_times(enc_log_content) e_rtime = e_times[0] if e_times else 0.0 enc_fps = calculate_fps(e_frames, e_rtime) else: enc_fps = 0.0 print(f" -> Encode log file not created") with open(video_encode_benchmark_temp, 'a', newline='') as f: writer = csv.writer(f) writer.writerow([enc_yuv_name, enc_frames, enc_format.strip(), enc_depth.strip(), enc_resolution.strip(), enc_fps]) # --- 修改点 2: 清理临时文件 --- # 现在 encode_result 是临时文件,可以安全删除,不会误删原文件 for f_to_rm in [yuv_filename, encode_result, encode_log]: if os.path.exists(f_to_rm): os.remove(f_to_rm) except ValueError: pass # 清理日志 for f_to_rm in [decode_log, multi_log]: if os.path.exists(f_to_rm): os.remove(f_to_rm) print("Aggregating results...") aggregate_results(video_decode_benchmark_temp, video_decode_benchmark, is_encode=False, threads=THREADS) aggregate_results(video_encode_benchmark_temp, video_encode_benchmark, is_encode=True, threads=THREADS) for f in [video_decode_benchmark_temp, video_encode_benchmark_temp]: if os.path.exists(f): os.remove(f) print("Benchmark finished.") if __name__ == "__main__": main()