video_benchmark.py 18.4 KB
Newer Older
zhyh2010's avatar
zhyh2010 committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
#!/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()