查看: 77|回复: 3

Python调用FFmpeg批量裁剪视频并生成封面字幕脚本

[复制链接]
发表于 半小时前 | 显示全部楼层 |阅读模式
批量视频裁剪的难点通常不是会不会用剪辑软件,而是素材多、参数重复、时间轴需要可回溯。把区间写进 JSON 配置,让 Python 调用 FFmpeg 执行,改需求只改配置并重跑,比在时间线上反复拖拽更稳。下面按元数据探测、裁剪命令、批量校验、封面与字幕几个环节拆解,代码基于原文示例,FFmpeg 版本请以本机 ffmpeg -version 为准。

环境侧以 Python 3.8 及以上为基础,核心依赖是 FFmpeg、ffprobe 和 Pillow。FFmpeg 不固定版本号,但字幕烧录依赖 libass,需要单独确认;封面文字和字幕渲染中文还需要系统里有可用中文字体。安装后先验证命令:
  1. ffmpeg -version
  2. ffprobe -version
  3. ffmpeg -hide_banner -filters | grep subtitles
  4. fc-list :lang=zh | head -n 20
复制代码
前两条确认主程序和探测器可用,第三条确认是否有 subtitles 滤镜,没有输出通常说明该构建未编译 libass。fc-list 在 macOS 和多数 Linux 发行版可用,Windows 没有该命令,需要到系统字体目录查找可用字体文件。Pillow 代码中使用 ImageDraw.textbbox 计算文字边界,该方法从 Pillow 8.0 开始提供,旧环境需要升级或改用旧接口。

核心概念先对齐:.mp4、.mov 是容器,H.264、AAC 是编码;视频帧分 I 帧、P 帧、B 帧,复制流裁剪的起点必须落在 I 帧,否则开头可能花屏或解码异常。FFmpeg 的 -ss 放在 -i 前是输入选项,靠文件索引快速定位,可能对齐到关键帧;放在 -i 后则从文件头解码到目标点,帧级精确但速度慢。工程上常用 -ss 前置快速定位配合 -c copy;需要精确起点或叠加滤镜时则重编码。判断标准只有一条:操作需要看到画面像素,比如水印、字幕烧录,就必须解码再编码;纯粹搬数据才可以复制流。

正式裁剪前先用 ffprobe 探素材。下面的探针函数把日志级别压到 error,避免无关输出混进 JSON;capture_output 配合 check=True 让非零退出码直接抛异常;encoding 指定 utf-8,避免 Windows 下含中文路径的素材解码失败:
  1. import json
  2. import subprocess
  3. from pathlib import Path
  4. def probe_media(path: Path) -> dict:
  5.     cmd = [
  6.         'ffprobe',
  7.         '-v', 'error',
  8.         '-print_format', 'json',
  9.         '-show_format',
  10.         '-show_streams',
  11.         str(path),
  12.     ]
  13.     result = subprocess.run(
  14.         cmd,
  15.         capture_output=True,
  16.         text=True,
  17.         encoding='utf-8',
  18.         check=True,
  19.     )
  20.     return json.loads(result.stdout)
复制代码
原始 JSON 层级较深,再抽出时长、分辨率、码率等常用字段。注意 format.duration 在部分录屏或未正确收尾的文件中可能为空,需要回退到视频流的 duration;fps 常是 30000/1001 这种分数形式,后续算总帧数要做除法转换:
  1. def summarize(info: dict) -> dict:
  2.     fmt = info.get('format', {})
  3.     streams = info.get('streams', [])
  4.     video = next((s for s in streams if s.get('codec_type') == 'video'), None)
  5.     audio = next((s for s in streams if s.get('codec_type') == 'audio'), None)
  6.     summary = {
  7.         'duration': float(fmt.get('duration', 0.0) or 0.0),
  8.         'size_bytes': int(fmt.get('size', 0) or 0),
  9.         'bit_rate': int(fmt.get('bit_rate', 0) or 0),
  10.         'format_name': fmt.get('format_name', ''),
  11.         'has_video': video is not None,
  12.         'has_audio': audio is not None,
  13.         'width': int(video.get('width', 0)) if video else 0,
  14.         'height': int(video.get('height', 0)) if video else 0,
  15.         'video_codec': video.get('codec_name', '') if video else '',
  16.         'fps': video.get('r_frame_rate', '') if video else '',
  17.     }
  18.     if summary['duration'] <= 0 and video:
  19.         summary['duration'] = float(video.get('duration', 0.0) or 0.0)
  20.     return summary
复制代码
批量扫描目录时可把结果写成 CSV,编码用 utf-8-sig,方便 Excel 直接打开中文不乱码。这张表不仅给人看,也应被主流程用于时间轴校验,配置里的结束时间超过素材时长时,应在裁剪前报错退出。

时间轴配置建议一条片段一个对象,字段包括 id、start、duration、title、cover_at、precise。用起点加时长而不是起点加终点,是因为 FFmpeg 的 -t 正好接收时长,也减少减法与精度损失;cover_at 必须小于 duration,否则抽帧可能落在黑屏;precise 控制是否重编码。裁剪函数根据 precise 拼两条路径,重编码分支可叠加滤镜并保证起点精确,复制流分支速度优先但起点会对齐关键帧:
  1. def build_clip_cmd(src: Path, dst: Path, start: float,
  2.                     duration: float, precise: bool) -> list:
  3.     base = [
  4.         'ffmpeg', '-hide_banner', '-y',
  5.         '-ss', f'{start:.3f}',
  6.         '-i', str(src),
  7.         '-t', f'{duration:.3f}',
  8.     ]
  9.     if precise:
  10.         tail = [
  11.             '-c:v', 'libx264',
  12.             '-preset', 'veryfast',
  13.             '-crf', '20',
  14.             '-pix_fmt', 'yuv420p',
  15.             '-c:a', 'aac',
  16.             '-b:a', '192k',
  17.             '-movflags', '+faststart',
  18.         ]
  19.     else:
  20.         tail = ['-c', 'copy', '-avoid_negative_ts', 'make_zero']
  21.     return base + tail + [str(dst)]
  22. def run_clip(src: Path, dst: Path, start: float,
  23.              duration: float, precise: bool) -> bool:
  24.     dst.parent.mkdir(parents=True, exist_ok=True)
  25.     cmd = build_clip_cmd(src, dst, start, duration, precise)
  26.     print(f'[CMD]', ' '.join(cmd))
  27.     proc = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
  28.     if proc.returncode != 0:
  29.         print(f'[ERROR] 裁剪失败:{dst.name}')
  30.         print(proc.stderr[-2000:])
  31.         return False
  32.     return True
复制代码
几个参数值得说明:-y 在批量重跑时避免输出文件已存在导致交互卡死;-pix_fmt yuv420p 是兼容性保险,规避部分播放器遇到 yuv444p 或 yuvj420p 时黑屏只有声音;-movflags +faststart 把索引移到文件头,方便网络边下边播;-avoid_negative_ts make_zero 用于复制流模式,防止输出时间戳从负数开始。错误日志只截取 stderr 尾部,是因为 FFmpeg 真正原因通常在最后几十行。

批量执行前先做整体校验,快速失败比跑到一半再报错更好。下面校验起点为负、区间越界、cover_at 超出片段范围:
  1. def validate(config: dict, media: dict) -> None:
  2.     total = media['duration']
  3.     for seg in config['segments']:
  4.         end = seg['start'] + seg['duration']
  5.         if seg['start'] < 0:
  6.             raise ValueError(f"片段 {seg['id']} 起点为负:{seg['start']}")
  7.         if end > total + 0.5:
  8.             raise ValueError(
  9.                 f"片段 {seg['id']} 区间越界:end={end:.2f}s,素材总长={total:.2f}s"
  10.             )
  11.         if seg.get('cover_at', 0) >= seg['duration']:
  12.             raise ValueError(f"片段 {seg['id']} 的 cover_at 超出片段范围")
复制代码
批量调度可用 ThreadPoolExecutor,因为每次 FFmpeg 调用本质是启动子进程并等待,Python 侧几乎没有 CPU 计算,属于 IO 等待,线程池足够且开销比进程池小。并发数不宜盲目开大,磁盘和编码器会成为瓶颈。

封面环节要在片段内选时间点抽帧,cover_at 不要等于或超过 duration。抽出的图片再用 Pillow 叠加标题文字,ImageDraw.textbbox 负责计算文字边界。中文字体是常见坑:系统没有可用字体时输出方块或直接报错,需要有字体路径配置和缺字降级策略。字幕环节可从同一份时间轴生成 SRT;软字幕封装是把字幕作为独立流放进容器,由播放器渲染;硬字幕烧录走 -vf subtitles,依赖 libass。若前面检查发现没有 subtitles 滤镜,应改用软字幕封装或更换带 libass 的 FFmpeg 构建。

高频问题可以按固定顺序排查:先确认 ffmpeg、ffprobe 版本和 subtitles 滤镜,再确认中文字体;接着跑 probe,检查 JSON 是否能解析、duration 是否回退成功;然后跑 validate,排除时间轴越界和 cover_at 越界;最后单条试跑,确认 -ss 位置、是否复制流、输出像素格式和 faststart 是否符合预期。复制流开头花屏优先怀疑起点未对齐关键帧;输出黑屏只有声音优先检查 yuv420p 兼容性;字幕烧录失败优先检查 libass;中文乱码优先检查子进程 encoding 和 CSV 的 utf-8-sig。

工程化上建议把输入、中间产物、输出分层,原始素材只读,重跑时只清理中间目录;日志记录每条命令、返回码和错误尾部;裁剪函数按输出路径判断是否已存在,配合 -y 做可重复执行;性能上先小批量试跑,再根据磁盘和 CPU 调整并发。这样一套脚本化的批量裁剪流程,才能把重复剪辑变成可配置、可验证、可回溯的代码任务。
回复

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python调用FFmpeg批量裁剪视频并生成封面字幕脚本

思路很清晰,尤其是把时间轴放进 JSON 配置、用 ffprobe 先做元数据校验这一点,批量处理时确实比在时间线上反复拖拽稳得多。补充几个实践里容易踩的点:precise 为 False 走复制流时,起点如果没落在 I 帧,除了开头花屏,还可能因为音画时间戳错位导致后续拼接异常,建议批量跑之前先对每个片段抽首帧或前几秒做一次快速校验;-t 接收时长虽然方便,但浮点时长最好统一保留到毫秒级并写进日志,方便回溯。配置校验除了 start 加 duration 不超过素材时长,还可以检查 duration 大于 0、cover_at 小于 duration 且不要落在片头黑场或转场附近。字幕烧录时注意滤镜参数里的路径转义,Windows 盘符冒号和中文路径容易出问题,必要时先单独生成字幕文件再烧录。另外整批任务建议记录每个片段的实际命令、ffprobe 摘要和退出码,出问题能直接定位到某一条配置。期待后续 build_clip_cmd 和封面字幕部分的完整实现。
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python调用FFmpeg批量裁剪视频并生成封面字幕脚本

思路很对,配置驱动加可回溯这点尤其重要,素材一多,时间线里拖出来的东西根本没法复现。ffprobe 那段的写法也稳,日志压到 error、check=True、utf-8 编码这几个坑都避开了。补充几个自己在类似脚本里踩过的点,供参考: 一是 -ss 前置加 -c copy 的时候,最好带上 -avoid_negative_ts make_zero,必要时加 -copyts,否则某些容器裁完会出现音画不同步或者时间戳为负,播放器表现各异。另外 -ss 前置的关键帧对齐会让实际起点比配置里早一点,如果不接受这个偏差,就在日志里把实际起点记下来,或者在配置里加个容差说明。 二是重编码分支的参数建议固定下来,libx264 配 crf 18 到 20、preset veryfast,音频 aac 128k 到 192k,加 -pix_fmt yuv420p 保证各种播放器和网页都能放,再加 -movflags +faststart 方便流式播放。同时显式写 -map 0:v:0 -map 0:a?,避免源文件里多条音轨或字幕轨被意外带进输出。 三是精确裁剪其实不一定要把 -ss 放到 -i 后面,前置 -ss 加重编码同样能做到帧级精确,而且快很多,画质损失是一样的。只有在极少数容器索引有问题的素材上才需要后置解码。 四是字幕烧录的路径转义是个高频坑,尤其 Windows 下盘符的冒号和
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python调用FFmpeg批量裁剪视频并生成封面字幕脚本

整体思路很对,用配置驱动、把时间轴固定下来重跑,比在时间线上手动拖动稳得多。几个我在实际批量运行中踩过、也补充给楼主的点: 一是 subprocess 的稳健性。批量运行时最怕某条 ffmpeg 卡住进程,建议给 subprocess.run 加 timeout,并统一加 -nostdin 和 -hide_banner,避免 ffmpeg 去读标准输入导致循环挂起、日志混进无关信息。 二是裁剪后的回读校验。除了裁剪前用 CSV 校验时间轴,裁剪完最好再用 probe_media 读一遍目标文件,把实际 duration 和配置的 duration 比对,阈值可以按一帧或 0.1 秒设定。不一致的记到失败清单里继续跑下一条,不要一条出错就整批中断。 三是流复制分支的小细节。-ss 前置配 -c copy 时,建议加 -avoid_negative_ts make_zero,音视频时间戳有负数时省事。如果既想快又想知道实际起点落在哪,可以先用 ffprobe -show_frames 找目标时间之后最近的关键帧,把真实起点写进日志,这样回看时能解释开头为什么多了零点几秒。 四是 precise 分支的编码参数建议统一固定下来,比如 libx264、crf 18、preset medium、pix_fmt yuv420p,音频 aac 192k,避免不同素材输出参数漂移导致后续拼接出问题
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-14 02:46 , Processed in 0.022195 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部