
批量跑 ffmpeg 要扛住空格中文文件名、中途中断、坏文件和输出校验。脚本都在 macOSbash 3.2、ffmpeg 7.1、18 核上跑过结果原样贴出示例动作是缩到 720p换成 delogo 结构不变。一、测试素材用ffmpeg -f lavfi -i testsrcd20:r25:s1920x1080 -f lavfi -i sinef440:d20 -shortest合成 5 条 20 到 28 s 的片子其中一条叫「片段 05 测试.mp4」再echo not a video in/broken.mp4造一个坏文件演示失败。二、两个坑for f in $(ls in)把「片段 05 测试.mp4」拆成 [片段]、[05]、[测试.mp4]所以遍历用 find -print0 配 read -d 。另一个坑是循环只跑了几个while IFS read -r f; do echo 处理 [$f] ffmpeg -v error -n -i $f -vf scale1920:-2 -preset veryslow out_bad/${f##*/} done list.txtlist.txt 有 6 行却打出「处理 [p4]」这类残名只输出 3 个ffmpeg 默认读 stdin按 q 退出靠它和 read 抢输入。加 -nostdin 后 6 行全读到、输出 5 个broken 本该失败。三、Shell 串行版IN${1:-in}; OUT${2:-out}; mkdir -p $OUT; : failed.txt ok0; skip0; bad0; t0$(date %s) while IFS read -r -d src; do dst$OUT/${src##*/}; tmp${dst%.mp4}.part.mp4 [ -s $dst ] { skip$((skip1)); continue; } if ffmpeg -nostdin -v error -y -i $src -vf scale1280:-2 \ -crf 20 -c:a copy $tmp 2err.log; then mv $tmp $dst; ok$((ok1)); echo ok $src else rm -f $tmp; bad$((bad1)); echo err $src; printf %s\n $src failed.txt fi done (find $IN -type f -name *.mp4 -print0 | sort -z) echo 成功 ${ok}跳过 ${skip}失败 ${bad}耗时 $(( $(date %s) - t0 ))s用 (find …)find | while的循环在子 shell计数器出来就丢。先写 .part 成功才 mv中断只留 .part不会被当成已完成。-n 遇到已存在的输出会退出但退出码是 0会被误判成功所以跳过判断放在 shell 里ffmpeg 只对临时文件用 -y。首次成功 5跳过 0失败 1耗时 5s再跑跳过 5失败 1耗时 0sfailed.txt 里是 in/broken.mp4。四、并行xargs -Psrc$1; dstout_par/${src##*/}; tmp${dst%.mp4}.part.mp4 [ -s $dst ] exit 0 if ffmpeg -nostdin -v error -y -i $src -vf scale1280:-2 \ -threads ${TH:-0} -preset slow -crf 20 -c:a copy $tmp 2err.log then mv $tmp $dst else rm -f $tmp; printf %s\n $src failed.txt; exit 1 fifind in -name *.mp4 -print0 | TH0 xargs -0 -n 1 -P 3 ./one.sh耗时-P-threads耗时1118.0 s616.8 s10自动5.8 s305.7 s346.4 slibx264 单进程已吃满多核再并发几乎不快单线程编码时并发才明显P × threads 取核数左右机器有后台负载数字有波动。任一子进程非 0xargs 退出码为 1。也可用parallel -0 -j 3 ./one.sh。五、Python 版线程池、重试、续跑import subprocess as sp, sys, time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path def dur(p): # 视频流时长没有视频流返回 None o sp.run([ffprobe, -v, error, -of, defaultnw1:nk1, -show_entries, streamduration, -select_streams, v:0, p], capture_outputTrue, textTrue).stdout return float(o) if o.strip() else None def work(src): dst OUT / src.name if dst.exists() and dst.stat().st_size 0: return skip, 0, tmp dst.with_suffix(.part.mp4) cmd [ffmpeg, -nostdin, -v, error, -y, -i, src, -vf, scale1280:-2, -crf, 20, -c:a, copy, tmp] for n in range(1, TRIES 1): r sp.run(cmd, capture_outputTrue, textTrue) a, b dur(src), dur(tmp) if r.returncode 0 and a and b and abs(a - b) 0.1: tmp.replace(dst) return ok, n, tmp.unlink(missing_okTrue) time.sleep(0.5 * n) return fail, TRIES, frc{r.returncode} (r.stderr.strip() or 校验不过).splitlines()[-1] IN, OUT, JOBS, TRIES Path(sys.argv[1]), Path(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4]) OUT.mkdir(parentsTrue, exist_okTrue) files, t0, cnt, bad sorted(IN.rglob(*.mp4)), time.time(), {ok: 0, skip: 0, fail: 0}, [] with ThreadPoolExecutor(JOBS) as pool: futs {pool.submit(work, f): f for f in files} for i, fu in enumerate(as_completed(futs), 1): st, n, err fu.result() cnt[st] 1 print(f[{i}/{len(files)}] {st} {futs[fu].name} 尝试{n}次 {err}, flushTrue) bad [f{futs[fu]}\n] if st fail else [] Path(failed.txt).write_text(.join(bad), encodingutf-8) print(f成功 {cnt[ok]}跳过 {cnt[skip]}失败 {cnt[fail]}耗时 {time.time() - t0:.1f}s) sys.exit(1 if bad else 0)列表传参不经过 shell文件名不用转义干活的是子进程线程只在等GIL 不碍事。returncode 和时长校验一起判重试间隔递增真坏的文件进 failed.txt。python3 batch.py in out_py 3 3最后两行[6/6] fail broken.mp4 尝试3次 rc183 Error opening input files: Invalid data found when processing input 成功 5跳过 0失败 1耗时 6.4s退出码 1。再跑成功 0跳过 5失败 1耗时 3.6s。把 broken.mp4 换成正常的 12 s 片子续跑成功 1跳过 5失败 0耗时 0.4sfailed.txt 清空退出码 0。六、处理后校验dur() 读 v:0 的时长读到视频流、与原片差 ≤0.1 s 才通过24 s 原片-t 5 -c copy截出的 5.08 s 片子被拦下纯音轨文件读不到视频流也拦下。校验在改名前目录里只留合格成品。七、常见问题文件名带空格用 -print0、-d 、-0变量加引号。循环只跑几个ffmpeg 没加 -nostdin。输出覆盖原文件输入输出分目录同名时 ffmpeg 报 same as Input、退出码 234先写 .part 再改名。并行过多机器卡先拿三五条跑张耗时表再定 -P。本文作者字幕清洁工