本节摘要:本节掌握流水线的胶水。ffmpeg 的心智模型一句话:一切皆转码——拼接是转码、缩放是转码、抽帧是转码、烧字幕还是转码,理解了这一点,几十个参数就归了类。四件事的命令族逐个过:拼接用 concat demuxer(同参数片段可流复制零转码,异参数必须先统一再拼);转码定成片档(H.264 加 CRF 是默认答案);分辨率统一用缩放加补边(保宽高比、黑边填齐);时长控制有截断、补齐、变速三招。然后是一张十二行速查表,覆盖流水线高频场景(含第 3.2 节用过的抽尾帧)。最后是 Python 封装的纪律:错误信息不能吞——ffmpeg 的报错都在 stderr 尾部,静默失败会让上游段以为大功告成。
ffmpeg 的参数surface很大,但流水线里用到的只有一个模型:输入流经过滤镜(改像素)与编解码器(改格式),变成输出流。拼接是「多输入变一输出」,缩放是「滤镜改像素」,抽帧是「视频流变图片流」,烧字幕是「滤镜叠加图层」。带着这个模型读命令,参数不再是一堆咒语,而是「哪段进滤镜、哪段进编码器」的结构选择。
0.2 节的最小闭环已经用过 concat 清单。流水线版本多一个判断:片段参数一致吗?
# bash —— concat demuxer 拼接(同参数片段可流复制,零转码零损) cd build/04_clips printf "file 'shot_101.mp4'\nfile 'shot_102.mp4'\nfile 'shot_201.mp4'\n" > list.txt ffmpeg -f concat -safe 0 -i list.txt -c copy merged.mp4
-c copy 是流复制:不重新编码,秒级完成、无损。前提是所有片段的编码参数一致(编解码器、分辨率、帧率、像素格式)——第 3.1 节「全片统一」的纪律在这里兑现为速度。参数不一致时流复制会产出时间戳错乱的坏文件,正确做法是先统一转码(下节)再流复制拼接,宁可多花一遍编码时间。
「参数一致吗」不该靠肉眼判断,ffprobe 加十几行 Python 就是一台体检机:
# check_uniform.py —— 拼接前的片段参数体检(纯标准库,调用 ffprobe) import json import subprocess from pathlib import Path KEYS = ("codec_name", "width", "height", "r_frame_rate", "pix_fmt") def probe(path): proc = subprocess.run( ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=" + ",".join(KEYS), "-of", "json", str(path)], capture_output=True, text=True, check=True) return json.loads(proc.stdout)["streams"][0] def check(clips_dir): clips = sorted(Path(clips_dir).glob("shot_*.mp4")) if not clips: raise SystemExit("没有找到片段,先检查目录与生成段状态") base, base_name = probe(clips[0]), clips[0].name for clip in clips[1:]: info = probe(clip) diff = [k for k in KEYS if info.get(k) != base.get(k)] if diff: print("不一致:%s 与 %s 差在 %s" % (clip.name, base_name, "、".join(diff))) print("体检完成:%d 个片段,以 %s 为基准" % (len(clips), base_name)) if __name__ == "__main__": check("build/04_clips")
体检机输出的是「谁和谁差在哪」,不是「怎么修」——修复动作统一而简单:全部片段过一遍下一节的成片档转码,再回来流复制拼接。把体检放在拼接之前当门禁,坏消息会在最便宜的位置暴露;拼接之后才发现,返工的就是整条时间线。
| 参数 | 常用值(示意) | 含义与取舍 |
|---|---|---|
| -c:v | libx264 | 通用兼容的默认答案;分发平台基本全收 |
| -crf | 18 到 23 | 质量与体积的旋钮:小更精、大更清;中间值起步 |
| -preset | medium | 编码速度与压缩率:快preset出大文件 |
| -pix_fmt | yuv420p | 兼容性像素格式,部分播放器只认它 |
| -c:a 与 -b:a | aac 与 192k | 音轨档位(后半本音轨章节细化) |
# bash —— 统一转码为成片档(异参数片段先过这一遍再拼接) ffmpeg -i input.mov -c:v libx264 -crf 20 -preset medium -pix_fmt yuv420p -c:a aac -b:a 192k output.mp4
分辨率统一的原则是保宽高比加补边,不做暴力拉伸:先缩到目标内接尺寸,再补黑边到画布。时长控制三招:截断(-t)、补齐(克隆末帧)、变速(改 PTS)。
# bash —— 分辨率统一:缩放加补边到 1920 by 1080 画布(不拉伸) ffmpeg -i input.mp4 -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" out.mp4 # bash —— 时长控制三招:截断、克隆末帧补齐、轻微变速 ffmpeg -i in.mp4 -t 4.0 cut.mp4 ffmpeg -i in.mp4 -vf "tpad=stop_mode=clone:stop_duration=1.5" padded.mp4 ffmpeg -i in.mp4 -filter_complex "[0:v]setpts=0.8*PTS[v]" -map "[v]" faster.mp4
补齐比截断更常用:生成段给出的片段常常比分镜时长短零点几秒(模型时长档离散),克隆末帧把尾巴补到分镜要求值,剪辑段的时间线才对得上账。
对账本身也要有工具动作:分镜时长的合计是预期值,成片的实际时长用 ffprobe 读出来,两者差值超出容差(如 0.5 秒,示意值)就说明有片段短缺没补齐、或有转场交叠吃掉了时长:
# bash —— 读成片实际时长(秒),与分镜时长合计对账 ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 merged.mp4
对账不过关的处置顺序:先查各片段时长是否达到分镜值(短缺走 tpad 补齐),再查转场交叠是否重复扣减了时长——两类原因的修法完全不同,混在一起查会绕远路。
| 场景 | 命令要点 |
|---|---|
| 拼接(同参数) | -f concat -safe 0 -i list.txt -c copy |
| 统一转码 | -c:v libx264 -crf 20 -pix_fmt yuv420p |
| 分辨率统一 | scale 加 pad,force_original_aspect_ratio |
| 截断时长 | -t 秒数 |
| 补齐时长 | tpad 滤镜,stop_mode 等于 clone |
| 变速 | setpts 乘系数 |
| 抽指定时间帧 | -ss 时间 -frames:v 1 输出 png(第 3.2 节抽尾帧) |
| 烧字幕 | -vf subtitles 等字幕文件(0.2 节用过) |
| 查看元信息 | ffprobe 查流参数(拼接前检查参数一致性) |
| 提取音轨 | -vn -c:a copy(后半本音轨章用) |
| 帧序列合成 | -framerate 30 -i 帧模板(第 5.3 节用) |
| 静音音轨 | -f lavfi -i anullsrc(占位音轨,保容器声轨结构) |
# ff.py —— subprocess 封装 ffmpeg(失败必须带出 stderr 尾部) import subprocess def run_ffmpeg(args, cwd=None): cmd = ["ffmpeg", "-y"] + args proc = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) if proc.returncode != 0: raise RuntimeError("ffmpeg 失败:\n%s" % proc.stderr[-1200:]) return proc run_ffmpeg(["-i", "shot_101.mp4", "-c:v", "libx264", "-crf", "20", "shot_101_h264.mp4"], cwd="build/04_clips")
封装的要点只有一个:失败时把 stderr 的尾部带出来。ffmpeg 的报错几乎全在 stderr 的最后几十行,check=True 抛出的 CalledProcessError 默认不带输出,静默吞掉会让上游段误判成功——这是流水线里最难排查的一类假绿。参数一致性检查(拼接前 ffprobe 逐片段比对)建议一并封进去,1.1 节的失败隔离纪律在剪辑段的落点。
| 症状 | 根因 | 处置 |
|---|---|---|
| 流复制拼接后播放错乱、时长不对 | 片段参数不一致还强行复制 | 先统一转码再拼;拼接前跑参数体检 |
| 拼接后总时长与分镜对不上 | 片段短缺,或转场交叠吃掉时长 | 按对账流程分两头查:先补齐、再查交叠 |
| 烧出的字幕是乱码或方框 | 字幕文件编码不对,或滤镜未指定中文字体 | 字幕存 UTF-8,subtitles 滤镜带字体参数 |
| 画面绿屏、花屏 | 像素格式不是 yuv420p,或源片段损坏 | 转码统一像素格式;损坏片段回生成段重跑 |
| Python 封装显示成功但没有产物 | 输出路径写错,或没检查返回码 | 核对路径;失败必须抛出 stderr 尾部 |
| 命令长时间不结束 | 输出被接到管道上无人读取 | 用 capture_output 收走 stdout 与 stderr |
ffmpeg 的报错信息量很大,但几乎全部走 stderr——排查任何问题的第一步,都是先确认自己真的看得到 stderr。
命令行胶水在手,下一节上编排层——moviepy 读分镜 JSON,把片段、转场、字幕条自动排成时间线。