FFmpeg——从命令到工作流




2021-07-29

blog_main_img

你可能只是想干一件很朴素的事: - 把视频转成 `mp4` - 抽一张封面图 - 把音频单独导出来 - 裁一段片头 - 给视频压一压体积

FFmpeg 很像那种一开始看着有点凶、用熟以后又离不开的工具。

结果一打开命令行,参数就像瀑布一样砸下来。于是很多人对 FFmpeg 的印象会卡在两个极端里:

  • “它特别强”
  • “但我总记不住怎么写”

其实它没那么神秘。只要把几个核心概念理顺,FFmpeg 就会从“会背几条命令”变成“我知道该怎么组织媒体处理流程”。

官方文档把 ffmpeg 描述成一个可以处理音视频流的命令行工具,而 ffprobe 则负责把媒体信息用更适合人读或机器读的方式输出出来。ffmpeg Documentation ffprobe Documentation

这一对搭档的感觉很像:

  • ffprobe 先看清素材
  • ffmpeg 再动手处理素材

工作流总览

先把 FFmpeg 的基本气质看明白

很多工具是“点一下功能就出结果”,FFmpeg 不是。

它更像一个媒体处理引擎,核心思路大概是:

  1. 读输入
  2. 识别里面有哪些流
  3. 决定哪些流要保留、丢弃、复制、转码
  4. 必要时挂上滤镜
  5. 输出到新的容器或文件

官方文档里有一句话很关键:每个输入和输出在原则上都可以包含多个不同类型的基础流(elementary streams),比如视频流、音频流、字幕流等。ffmpeg Documentation

这个概念特别重要,因为很多初学者会把“一个文件”误当成“一个视频”。但在 FFmpeg 眼里,文件更像一个容器,里面装着很多流。

所以你后面写命令时,脑子里最好把素材拆成这几层:

  • 容器
  • 视频流
  • 音频流
  • 字幕流
  • 元数据

一旦你开始这么想,很多命令会突然顺起来。

先别急着转码,先让 ffprobe 说话

官方文档对 ffprobe 的描述很直接:它会收集多媒体流信息,并把结果以人类可读和机器可读的方式打印出来。ffprobe Documentation

这一步非常值钱,因为很多“FFmpeg 命令写错”的根源,不是命令不会写,而是素材根本没看清。

先看一个最基础的命令:

ffprobe input.mp4

如果你想把输出变得更适合机器处理,官方文档也明确说明了它支持不同 writer,比如 jsoncsvxml 等。ffprobe Documentation

一个非常常用的形式:

ffprobe -v quiet -print_format json -show_format -show_streams input.mp4

这个输出特别适合做下面几件事:

  • 看视频分辨率
  • 看音频编码格式
  • 看总时长
  • 看码率
  • 看流索引

而这些信息,后面会直接决定你到底该不该转码、转哪一路流、是不是能直接 copy。

最重要的一个分界线:到底是 copy 还是转码

这几乎是日常使用 FFmpeg 时最值得先想清楚的问题。

stream copy:能不重编码,就先别重编码

如果你只是:

  • 换容器
  • 抽某一路流
  • 简单裁剪

并且原始编码本身就是你能接受的,那优先考虑 -c copy

ffmpeg -i input.mkv -c copy output.mp4

这类命令的好处很直接:

  • 不重新编码
  • 没有额外画质损耗

但它也有前提:目标容器得能装下这些流,且编码本身兼容。

转码:当你真的要改编码、改尺寸、改码率时再动手

如果你要:

  • 降低体积
  • 改成 H.264 / H.265
  • 改采样率
  • 改分辨率
  • 给移动端做兼容输出

那就要真正进入转码阶段。

一个最常见的例子:

ffmpeg -i input.mov -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output.mp4

这里面最常见的几个参数可以先这样理解:

  • -c:v libx264:视频编码器
  • -crf 23:画质和体积的平衡杆
  • -preset medium:编码速度与压缩效率的平衡
  • -c:a aac:音频编码器
  • -b:a 128k:音频码率

不需要一次把所有编码器参数背熟,先知道“copy 和转码是两条完全不同的路”,已经能避开一大半混乱了。

抽流、抽帧、裁剪:这些命令其实特别生活化

把音频单独导出来

ffmpeg -i input.mp4 -vn -c:a copy output.aac

这里:

  • -vn 表示不要视频
  • -c:a copy 表示音频流直接复制

抽一张封面图

ffmpeg -i input.mp4 -ss 00:00:03 -frames:v 1 cover.jpg

这个命令特别适合:

  • 视频封面生成
  • 运营后台截图
  • 内容审核取样

裁剪一段片头或片段

ffmpeg -ss 00:00:10 -i input.mp4 -t 00:00:08 -c copy clip.mp4

这类操作很常见,但要注意:

  • 想要快,常常会尽量用 copy
  • 想要边界更准,有时要配合重新编码

所以“快”和“精确”不总是同一个方向。

过滤器才是 FFmpeg 真正的魔术区

官方的过滤器文档非常长,但有一个核心概念特别值得先吃透:

filtergraph 是一张由过滤器连接起来的有向图。
FFmpeg Filters Documentation

官方文档还给了一个很经典的图式例子:

testsrc,split[L1],hflip[L2];[L1][L2] hstack

它的味道其实非常像数据流管道:

  • 一个输入源进来
  • 中途 split 成两路
  • 其中一路做翻转
  • 最后两路再拼起来

这个思路一旦懂了,你后面看复杂滤镜链就不会只觉得它像乱码。

最常用的几个视频滤镜

缩放

ffmpeg -i input.mp4 -vf "scale=1280:720" output.mp4

裁剪

ffmpeg -i input.mp4 -vf "crop=1280:720:0:0" output.mp4

叠字

ffmpeg -i input.mp4 -vf "drawtext=text='hello':x=50:y=50:fontsize=36:fontcolor=white" output.mp4

多个视频横向拼接

ffmpeg -i left.mp4 -i right.mp4 -filter_complex "[0:v][1:v]hstack=inputs=2[v]" -map "[v]" output.mp4

音频滤镜也很值得认识

比如调音量:

ffmpeg -i input.mp3 -af "volume=1.5" louder.mp3

或者淡入淡出:

ffmpeg -i input.mp3 -af "afade=t=in:ss=0:d=2,afade=t=out:st=28:d=2" output.mp3

你会发现,-vf-af 的感觉就像:

  • -vf 处理视频流
  • -af 处理音频流

复杂一点时再上 -filter_complex

滤镜链一复杂,最难的反而不是滤镜本身

官方过滤器文档里有一大段在讲转义和 escaping,而且说得非常直白:滤镜描述在命令行里可能会遇到多层转义问题,甚至会变得很痛苦。FFmpeg Filters Documentation

这几乎是所有 FFmpeg 用户都迟早会撞上的坑。

尤其是:

  • drawtext
  • 包含冒号、逗号、引号的参数
  • 多层 filter_complex

最实用的建议反而很朴素:

  • 过滤链简单时,直接命令行写
  • 一旦变复杂,尽量拆变量、拆脚本
  • 文本内容能放文件就别硬塞命令行

你不需要靠硬扛转义来证明自己会用 FFmpeg。

滤镜和流

ffprobe + Python:这是做自动化最顺的一段

如果你只是手敲几个命令,命令行已经够用。

但一旦你要做:

  • 批量转码
  • 批量抽图
  • 自动生成封面
  • 批量检测分辨率、码率、时长

Python 就会非常舒服。

先用 Python 跑 ffprobe

from __future__ import annotations

import json
import subprocess
from pathlib import Path


def probe_media(path: str | Path) -> dict:
    cmd = [
        "ffprobe",
        "-v", "quiet",
        "-print_format", "json",
        "-show_format",
        "-show_streams",
        str(path),
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
    return json.loads(result.stdout)


meta = probe_media("input.mp4")
print(meta["format"]["format_name"])
print(meta["format"]["duration"])
print(meta["streams"][0]["codec_name"])

这段代码做的事很简单,但很有工程感:

  • ffprobe 的 JSON 输出收进来
  • 后面就可以按字段做判断

比如:

  • 视频是否超过 1080p
  • 时长是否太长
  • 封装格式是否符合要求
    Python 工作流图

再用 Python 调 ffmpeg

from __future__ import annotations

import subprocess
from pathlib import Path


def transcode_to_h264(src: str | Path, dst: str | Path) -> None:
    cmd = [
        "ffmpeg",
        "-y",
        "-i", str(src),
        "-c:v", "libx264",
        "-crf", "23",
        "-preset", "medium",
        "-c:a", "aac",
        "-b:a", "128k",
        str(dst),
    ]
    subprocess.run(cmd, check=True)


transcode_to_h264("input.mov", "output.mp4")

这里最关键的不是 Python 语法,而是你开始把媒体处理组织成函数了。

这意味着后面你可以很自然地补:

  • 批量处理
  • 日志记录
  • 失败重试
  • 输入规则校验

一个更像实战的小工具:批量抽封面

from __future__ import annotations

import subprocess
from pathlib import Path


def extract_cover(video_path: Path, output_dir: Path) -> None:
    output_dir.mkdir(parents=True, exist_ok=True)
    target = output_dir / f"{video_path.stem}.jpg"
    cmd = [
        "ffmpeg",
        "-y",
        "-i", str(video_path),
        "-ss", "00:00:02",
        "-frames:v", "1",
        str(target),
    ]
    subprocess.run(cmd, check=True)


for video in Path("./videos").glob("*.mp4"):
    extract_cover(video, Path("./covers"))

这种脚本在内容平台、短视频后台、媒体资源仓库里都非常常见。

真正把 FFmpeg 用顺以后,思路会变成这样

不是“我记住了几十条命令”,而是:

  1. 先用 ffprobe 看素材
  2. 判断是否能 copy
  3. 确定要不要转码
  4. 判断是否需要滤镜
  5. 必要时用 Python 包成稳定流程

这套顺序一旦建立起来,后面的命令就不会再像散弹。